Reputation: 1312
In my quest to implement Alamofire 5 correctly and handle custom error model responses, I have yet to find an accepted answer that has an example.
To be as thorough as possible, here is my apiclient
class APIClient {
static let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(configuration: configuration, eventMonitors: [APILogger()])
}()
@discardableResult
private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:@escaping (Result<T, AFError>)->Void) -> DataRequest {
return sessionManager.request(route)
// .validate(statusCode: 200..<300) // This will kill the server side error response...
.responseDecodable (decoder: decoder){ (response: DataResponse<T, AFError>) in
completion(response.result)
}
}
static func login(username: String, password: String, completion:@escaping (Result<User, AFError>)->Void) {
performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
}
}
I am using it like this
APIClient.login(username: "", password: "") { result in
debugPrint(result)
switch result {
case .success(let user):
debugPrint("__________SUCCESS__________")
case .failure(let error):
debugPrint("__________FAILURE__________")
debugPrint(error.localizedDescription)
}
}
I have noticed that if I use .validate()
that the calling function will receive a failure however the response data is missing. Looking around it was noted here and here to cast underlyingError
but thats nil.
The server responds with a parsable error model that I need at the calling function level. It would be far more pleasant to deserialize the JSON at the apiclient level and return it back to the calling function as a failure.
{
"errorObject": {
"summary": "",
"details": [{
...
}]
}
}
Thanks to @GIJoeCodes comment I implemented this similar solution using the router.
class APIClient {
static let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(configuration: configuration, eventMonitors: [APILogger()])
}()
@discardableResult
private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:@escaping (_ response: T?, _ error: Error?)->Void) {
sessionManager.request(route)
.validate(statusCode: 200..<300) // This will kill the server side error response...
.validate(contentType: ["application/json"])
.responseJSON { response in
guard let data = response.data else { return }
do {
switch response.result {
case .success:
let object = try decoder.decode(T.self, from: data)
completion(object, nil)
case .failure:
let error = try decoder.decode(ErrorWrapper.self, from: data)
completion(nil, error.error)
}
} catch {
debugPrint(error)
}
}
}
// MARK: - Authentication
static func login(username: String, password: String, completion:@escaping (_ response: User?, _ error: Error?)->Void) {
performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
}
}
Called like this
APIClient.login(username: "", password: "") { (user, error) in
if let error = error {
debugPrint("__________FAILURE__________")
debugPrint(error)
return
}
if let user = user {
debugPrint("__________SUCCESS__________")
debugPrint(user)
}
}
Upvotes: 3
Views: 2481
Reputation: 12770
First, there's no need to use responseJSON
if you already have a Decodable
model. You're doing unnecessary work by decoding the response data multiple times. Use responseDecodable
and provide your Decodable
type, in this case your generic T
. responseDecodable(of: T)
.
Second, wrapping your expected Decodable
types in an enum is a typical approach to solving this problem. For instance:
enum APIResponse<T: Decodable> {
case success(T)
case failure(APIError)
}
Then implement APIResponse
's Decodable
to try to parse either the successful type or APIError
(there are a lot of examples of this). You can then parse your response using responseDecodable(of: APIResponse<T>.self)
.
Upvotes: 2
Reputation: 1850
This is how I get the errors and customize my error messages. In the validation, I get the errors outside of the 200..<300 response:
AF.request(
url,
method: .post,
parameters: json,
encoder: JSONParameterEncoder.prettyPrinted,
headers: headers
).validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON { response in
switch response.result {
case .success(let result):
let json = JSON(result)
onSuccess()
case .failure(let error):
guard let data = response.data else { return }
do {
let json = try JSON(data: data)
let message = json["message"]
onError(message.rawValue as! String)
} catch {
print(error)
}
onError(error.localizedDescription)
}
debugPrint(response)
}
Upvotes: 2