Using Alamofire 4, we had an API response validator in place we invoked like so:
func request<Endpoint: APIEndpoint>(_ baseURL: URL, endpoint: Endpoint, completion: @escaping (_ object: Endpoint.ResponseType?, _ error: AFError?) -> Void) -> DataRequest where Endpoint.ResponseType: Codable {
let responseSerializer = APIObjectResponseSerializer(endpoint)
let request = self.request(baseURL, endpoint: endpoint)
.validate(APIResponseValidator.validate) << VALIDATOR PASSED HERE
.response(responseSerializer: responseSerializer) { response in
completion(response.value, response.error)
}
return request
}
It looks like this:
static func validate(request: URLRequest?, response: HTTPURLResponse, data: Data?) -> Request.ValidationResult {
// **INSERT OTHER FAILURE CHECKS HERE**
// Verify server time is within a valid time window.
let headers = response.allHeaderFields
guard let serverTimeString = headers["Date"] as? String, let serverTime = DateUtils.headerDateFormatter().date(from: serverTimeString) else {
Log.error("APIValidation: no Date in response header")
return .failure(APIError.appTimeSettingInvalid))
}
// **INSERT OTHER FAILURE CHECKS HERE**
return .success(Void())
}
The appropriate error would make it back to the request completion handler,
▿ APIError
▿ appTimeSettingInvalid
and we could update the UI with the right error, everyone was happy.
But now with Alamofire, it's this:
▿ Optional<Error>
▿ some : AFError
▿ requestRetryFailed : 2 elements
▿ retryError : AFError
▿ responseValidationFailed : 1 element
▿ reason : ResponseValidationFailureReason
▿ customValidationFailed : 1 element
▿ error : APIError
▿ appTimeSettingInvalid << Original custom error
▿ originalError : AFError
▿ responseValidationFailed : 1 element
▿ reason : ResponseValidationFailureReason
▿ customValidationFailed : 1 element
▿ error : APIError
▿ appTimeSettingInvalid << Original custom error
Which I need to access like this:
if let underlyingError = (error as? AFError)?.underlyingError as? AFError,
case let AFError.requestRetryFailed(_, originalError) = underlyingError,
case let AFError.responseValidationFailed(reason) = originalError,
case let .customValidationFailed(initialCustomError) = reason {
showAlert(initialCustomError)
}
This seems absurd. What am I missing? Why did the custom validation fail when nothing has changed about the method, and why is it wrapped in a layer of other errors? Why retry a request when the validation is going to fail the same way?
How do I get my custom error back, consistently, across all my requests?
In Alamofire 5, all errors are returned contained in an AFError
instance, including custom validation errors. This allows our Response
types to contain typed errors and provides a consistent error type. However, the validation API still returns Error
instances, unfortunately, so there is an additional layer to peel back. You can use the convenience asAFError
property to perform the cast and the underlyingError
property to grab any underlying errors. Use of switch
statements can also make the extraction easier. You can also mapError
on responses to extract the specific error types you want.
As for retry, it's likely your retrier hasn't been updated to extract the errors in such a way that retry is properly avoided with your custom error type.