HTTP POST request using Swift Combine - swift

I'm fairly new to Combine declarative API. I'm trying to implement a generic network layer for a SwiftUI application. For all requests that receive data I understand how to structure the data flow.
My problem is that I have some HTTP POST requests that returns no data. Only a HTTP 200 on success. I can't figure out how to create a publisher that will handle a decoding that can fail since there could be not data in the body of the response. Here's what I tried:
func postResource<Resource: Codable>(_ resource: Resource, to endpoint: Endpoint) -> AnyPublisher<Resource?, NetworkError> {
return Just(resource)
.subscribe(on: queue)
.encode(encoder: JSONEncoder())
.mapError { error -> NetworkError in
return NetworkError.encoding(error)
}
.map { data -> URLRequest in
return endpoint.makeRequest(with: data)
}
.tryMap { request -> Resource? in
self.session.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpUrlResponse = response as? HTTPURLResponse else { throw NetworkError.unknown }
guard (200 ... 299).contains(httpUrlResponse.statusCode) else { throw NetworkError.error(for: httpUrlResponse.statusCode) }
return data
}
.tryMap { data -> Resource? in
return try? JSONDecoder().decode(Resource.self, from: data)
}
}
.mapError({ error -> NetworkError in
switch error {
case is Swift.DecodingError:
return NetworkError.decoding(error)
case let urlError as URLError:
return .urlError(urlError)
case let error as NetworkError:
return error
default:
return .unknown
}
})
.eraseToAnyPublisher()
}
The compiler is complaining with the following error on tryMap row:
Declared closure result 'Publishers.TryMap<URLSession.DataTaskPublisher, Resource?>' is incompatible with contextual type 'Resource?'
Anyone has an idea?
Thanks!

enum NetworkError: Error {
case encoding(Error)
case error(for: Int)
case decoding(Error)
case urlError(URLError)
case unknown
}
func postResource<Resource: Codable>(_ resource: Resource, to endpoint: Endpoint) -> AnyPublisher<Resource?, NetworkError> {
Just(resource)
.subscribe(on: queue)
.encode(encoder: JSONEncoder())
.mapError { error -> NetworkError in
NetworkError.encoding(error)
}
.map { data -> URLRequest in
endpoint.makeRequest(with: data)
}
.flatMap { request in // the key thing is here you should you use flatMap instead of map
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response -> Data in
guard let httpUrlResponse = response as? HTTPURLResponse else { throw NetworkError.unknown }
guard 200 ... 299 ~= httpUrlResponse.statusCode else { throw NetworkError.error(for: httpUrlResponse.statusCode) }
return data
}
.tryMap { data -> Resource? in
try? JSONDecoder().decode(Resource.self, from: data)
}
}
.mapError({ error -> NetworkError in
switch error {
case is Swift.DecodingError:
return NetworkError.decoding(error)
case let urlError as URLError:
return .urlError(urlError)
case let error as NetworkError:
return error
default:
return .unknown
}
})
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}

Related

Swift Combine to map URLSession.shared.dataTaskPublisher HTTP response errors

Given an API that for invalid requests, along with 400-range HTTP status code the server returns a JSON payload that includes a readable message. As an example, the server could return { "message": "Not Found" } with a 404 status code for deleted or non-existent content.
Without using publishers, the code would read,
struct APIErrorResponse: Decodable, Error {
let message: String
}
func request(request: URLRequest) async throws -> Post {
let (data, response) = try await URLSession.shared.data(for: request)
let statusCode = (response as! HTTPURLResponse).statusCode
if 400..<500 ~= statusCode {
throw try JSONDecoder().decode(APIErrorResponse.self, from: data)
}
return try JSONDecoder().decode(Post.self, from: data)
}
Can this be expressed succinctly using only functional code?
In other words, how can the following pattern be adapted to decode a different type based on the HTTPURLResponse.statusCode property, to return as an error, or more generally, how can the response property be handled separately from data attribute?
URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Post.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
you could try something like this approach:
func request(request: URLRequest) -> AnyPublisher<Post, any Error> {
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { (output) -> Data in
let statusCode = (output.response as! HTTPURLResponse).statusCode
if 400..<500 ~= statusCode {
throw try JSONDecoder().decode(APIErrorResponse.self, from: output.data)
}
return output.data
}
.decode(type: Post.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
I use a helper method for this:
extension Publisher where Output == (data: Data, response: HTTPURLResponse) {
func decode<Success, Failure>(
success: Success.Type,
failure: Failure.Type,
decoder: JSONDecoder
) -> AnyPublisher<Success, Error> where Success: Decodable, Failure: DecodableError {
tryMap { data, httpResponse -> Success in
guard httpResponse.statusCode < 500 else {
throw MyCustomError.serverUnavailable(status: httpResponse.statusCode)
}
guard httpResponse.statusCode < 400 else {
let error = try decoder.decode(failure, from: data)
throw error
}
let success = try decoder.decode(success, from: data)
return success
}
.eraseToAnyPublisher()
}
}
typealias DecodableError = Decodable & Error
which allows me to simplify the call sites like so:
URLSession.shared.dataTaskPublisher(for: request)
.decode(success: Post.self, failure: MyCustomError.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
workingdogsupport has provided a good literal translation (+1). And LuLuGaGa has illustrated a nice compositional style (+1).
I might expand upon the latter, though, and recommend pattern matching on the various status codes, e.g. 2xx codes for decoding success, 4xx for graceful web service errors, and a more general .badServerResponse (and includes the diagnostic information so that the developer working on the call point has a chance to figure out what went wrong) for anything else.
E.g., you might have an general extension (which doesn’t use any types particular to the app):
extension Publisher where Output == (data: Data, response: URLResponse) {
func decode<Success: Decodable, Failure: Decodable & Error>(
success: Success.Type = Success.self,
failure: Failure.Type = Failure.self,
decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<Success, Error> {
tryMap { data, response -> Success in
switch (response as! HTTPURLResponse).statusCode {
case 200..<300: return try decoder.decode(Success.self, from: data)
case 400..<500: throw try decoder.decode(Failure.self, from: data)
default: throw URLError(.badServerResponse, userInfo: ["data": data, "response": response])
}
}
.eraseToAnyPublisher()
}
}
Or, because I hate force-unwrapping:
extension Publisher where Output == (data: Data, response: URLResponse) {
func decode<Success: Decodable, Failure: Decodable & Error>(
success: Success.Type = Success.self,
failure: Failure.Type = Failure.self,
decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<Success, Error> {
tryMap { data, response -> Success in
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse, userInfo: ["data": data, "response": response])
}
switch response.statusCode {
case 200..<300: return try decoder.decode(Success.self, from: data)
case 400..<500: throw try decoder.decode(Failure.self, from: data)
default: throw URLError(.badServerResponse, userInfo: ["data": data, "response": response])
}
}
.eraseToAnyPublisher()
}
}
Regardless, I might then have an extension for this app that decodes your particular web service’s specific error struct:
extension Publisher where Output == (data: Data, response: URLResponse) {
func decode<Success: Decodable>(
success: Success.Type = Success.self,
decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<Success, Error> {
decode(success: success, failure: APIErrorResponse.self, decoder: decoder)
}
}
Then the app code can avail itself of the above (and infer the success type):
func postsPublisher(for request: URLRequest) -> AnyPublisher<Post, Error> {
URLSession.shared.dataTaskPublisher(for: request)
.decode()
.eraseToAnyPublisher()
}
Anyway, that results in a succinct call-point, with a reusable extension.

Using flatMap in RestManager with generic function not working

I am new to Combine, so I wanted to create class RestManager for networking with generic
fetchData function. Function is returning AnyPublisher<Result<T, ErrorType>, Never> where ErrorType is enum with .noInternetConnection, .empty and .general cases.
I tried to use URLSession with dataTaskPublisher and flatMap
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<Result<T, ErrorType>, Never> {
URLSession
.shared
.dataTaskPublisher(for: url)
.flatMap { (data, response) -> AnyPublisher<Result<T, ErrorType>, Never> in
switch response.result {
case .success(let data):
if let data = try? JSONDecoder().decode(T.self, from: data){
return Just(data).eraseToAnyPublisher()
}
case .failure(let error):
if let error = error as? URLError {
switch error.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return Fail(ErrorType.noInternetConnection).eraseToAnyPublisher()
case .cannotDecodeRawData, .cannotDecodeContentData:
return Fail(ErrorType.empty).eraseToAnyPublisher()
default:
return Fail(ErrorType.general).eraseToAnyPublisher()
}
}
}
}
.eraseToAnyPublisher()
}
But I am getting
Cannot convert return expression of type 'AnyPublisher<AnyPublisher<Result<T, ErrorType>, Never>.Output, URLSession.DataTaskPublisher.Failure>' (aka 'AnyPublisher<AnyPublisher<Result<T, ErrorType>, Never>.Output, URLError>') to return type 'AnyPublisher<Result<T, ErrorType>, Never>' error.
There are several major flows in your implementation.
Firstly, you shouldn't be using Result as the Output type of the Publisher and Never as its Failure type. You should be using T as the Output and ErrorType as Failure.
Second, you need tryMap and mapError, not flatMap.
Lastly, you are handling the result of dataTaskPublisher completely wrong. When dataTaskPublisher fails, it emits an error, so you need to handle that in mapError. When it succeeds, it emits its result as data, so you need to be decoding that, not response.
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { data, _ in
return try JSONDecoder().decode(T.self, from: data)
}
.mapError { error -> ErrorType in
switch error {
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}

Swift: Extending Combine operators

I'm trying to wrap a tryMap operator along the lines of this article.
extension Publisher where Output == Data {
func decode<T: Decodable>(as type: T.Type = T.self, using decoder: JSONDecoder = .init()) -> Publishers.Decode<Self, T, JSONDecoder> {
decode(type: type, decoder: decoder)
}
}
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
func processData(_: #escaping (Self.Output) throws -> Data) -> Publishers.TryMap<Self, Data> {
tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw URLError(.badServerResponse)
}
return element.data
}
}
}
While using it I'm getting a compiler error which I'm struggling with:
return urlSession
.dataTaskPublisher(for: request)
.processData // <- Value of type '(#escaping (URLSession.DataTaskPublisher.Output) throws -> Data) -> Publishers.TryMap<URLSession.DataTaskPublisher, Data>' (aka '(#escaping ((data: Data, response: URLResponse)) throws -> Data) -> Publishers.TryMap<URLSession.DataTaskPublisher, Data>') has no member 'decode'
.decode(as: InstantResponse.self)
.eraseToAnyPublisher()
What would be the correct way of doing it?
Thanks!
First of all, you aren't calling processData - you are missing the parentheses, which would actually execute the function. Second, your processData declaration is incorrect, it should not take a closure as its input argument, since you aren't using that closure anyways.
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
func processData() -> Publishers.TryMap<Self, Data> {
tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw URLError(.badServerResponse)
}
return element.data
}
}
}
return urlSession
.dataTaskPublisher(for: request)
.processData() // parentheses necessary here
.decode(as: InstantResponse.self)
.eraseToAnyPublisher()

How to decode the body of an error in Alamofire 5?

I'm trying to migrate my project from Alamofire 4.9 to 5.3 and I'm having a hard time with error handling. I would like to use Decodable as much as possible, but my API endpoints return one JSON structure when everything goes well, and a different JSON structure when there is an error, the same for all errors across all endpoints. The corresponding Codable in my code is ApiError.
I would like to create a custom response serializer that can give me a Result<T, ApiError> instead of the default Result<T, AFError>. I found this article that seems to explain the general process but the code in there does not compile.
How can I create such a custom ResponseSerializer?
I ended up making it work with the following ResponseSerializer:
struct APIError: Error, Decodable {
let message: String
let code: String
let args: [String]
}
final class TwoDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
private lazy var successSerializer = DecodableResponseSerializer<T>(decoder: decoder)
private lazy var errorSerializer = DecodableResponseSerializer<APIError>(decoder: decoder)
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Result<T, APIError> {
guard error == nil else { return .failure(APIError(message: "Unknown error", code: "unknown", args: [])) }
guard let response = response else { return .failure(APIError(message: "Empty response", code: "empty_response", args: [])) }
do {
if response.statusCode < 200 || response.statusCode >= 300 {
let result = try errorSerializer.serialize(request: request, response: response, data: data, error: nil)
return .failure(result)
} else {
let result = try successSerializer.serialize(request: request, response: response, data: data, error: nil)
return .success(result)
}
} catch(let err) {
return .failure(APIError(message: "Could not serialize body", code: "unserializable_body", args: [String(data: data!, encoding: .utf8)!, err.localizedDescription]))
}
}
}
extension DataRequest {
#discardableResult func responseTwoDecodable<T: Decodable>(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), of t: T.Type, completionHandler: #escaping (Result<T, APIError>) -> Void) -> Self {
return response(queue: .main, responseSerializer: TwoDecodableResponseSerializer<T>()) { response in
switch response.result {
case .success(let result):
completionHandler(result)
case .failure(let error):
completionHandler(.failure(APIError(message: "Other error", code: "other", args: [error.localizedDescription])))
}
}
}
}
And with that, I can call my API like so:
AF.request(request).validate().responseTwoDecodable(of: [Item].self) { response in
switch response {
case .success(let items):
completion(.success(items))
case .failure(let error): //error is an APIError
log.error("Error while loading items: \(String(describing: error))")
completion(.failure(.couldNotLoad(underlyingError: error)))
}
}
I simply consider that any status code outside of the 200-299 range corresponds to an error.
ResponseSerializers have a single requirement. Largely you can just copy the existing serializers. For example, if you wanted to parse a CSV (with no response checking):
struct CommaDelimitedSerializer: ResponseSerializer {
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> [String] {
// Call the existing StringResponseSerializer to get many behaviors automatically.
let string = try StringResponseSerializer().serialize(request: request,
response: response,
data: data,
error: error)
return Array(string.split(separator: ","))
}
}
You can read more in Alamofire's documentation.

getting error message from server during API call

I have an app where I used RxSwift for my networking by extending ObservableType this works well but the issue I am having now is when I make an API request and there is an error, I am unable to show the particular error message sent from the server. Now how can I get the particular error response sent from the server
extension ObservableType {
func convert<T: EVObject>(to observableType: T.Type) -> Observable<T> where E: DataRequest {
return self.flatMap({(request) -> Observable<T> in
let disposable = Disposables.create {
request.cancel()
}
return Observable<T>.create({observer -> Disposable in
request.validate().responseObject { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
if !disposable.isDisposed {
observer.onNext(value)
observer.onCompleted()
}
case .failure(let error):
if !disposable.isDisposed {
observer.onError(NetworkingError(httpResponse: response.response,
networkData: response.data, baseError: error))
observer.onCompleted()
}
}
}
return disposable
})
})
}
}
let networkRetryPredicate: RetryPredicate = { error in
if let err = error as? NetworkingError, let response = err.httpResponse {
let code = response.statusCode
if code >= 400 && code < 600 {
return false
}
}
return true
}
// Use this struct to pass the response and data along with
// the error as alamofire does not do this automatically
public struct NetworkingError: Error {
let httpResponse: HTTPURLResponse?
let networkData: Data?
let baseError: Error
}
response from the server could be
{
"status" : "error",
"message" : " INSUFFICIENT_FUNDS"
}
or
{
"status" : "success",
"data" : " gghfgdgchf"
}
my response is handled like this
class MaxResponse<T: NSObject>: MaxResponseBase, EVGenericsKVC {
var data: T?
public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
switch key {
case "data":
data = value as? T
default:
print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
}
}
public func getGenericType() -> NSObject {
return T()
}
}
the error is
return ApiClient.session.rx.request(urlRequest: MaxApiRouter.topupWall(userId: getUser()!.id!, data: body))
.convert(to: MaxResponse<Wall>.self)
In the official Alamofire docs it is mentioned that validate(), without any parameters:
Automatically validates status code within 200..<300 range, and that
the Content-Type header of the response matches the Accept header of
the request, if one is provided.
So if you do not include Alamofire's validate() you are saying that no matter the status code, if the request did get through, you will consider it successful, so that's why it shows nothing in the failure block.
However if you prefer to use it, yes, it will give you an ResponseValidationFailureReason error, but you still have access to the response.data. Try printing it, you should see the expected error response from the server:
if let responseData = response.data {
print(String(data: responseData, encoding: .utf8))
}