Search code examples
swiftxcodealamofirealamofire5

Custom validation error no longer working in Alamofire 5


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?


Solution

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