swiftalamofirealamofire5

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?


Solution

  • 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.