Custom Error Response Handling with Moya + RxSwift

Scott Storch picture Scott Storch · Aug 27, 2016 · Viewed 6.9k times · Source

I'm using Moya with RxSwift for networking in an iOS app, and I'd like to be able to consume my API's custom error responses when my Observers get calls to onError.

The API always returns error responses in the following JSON format:

{
   "error": {
      "message": "Error message goes here",
      "code": "String error code"
   }
}

The goal is to achieve something similar to the following code snippet, where the error passed in onError is my custom error type instead of the Moya.Error type:

observable
    .subscribe(
        onNext: { response in
            // Do typical onNext stuff
        },
        onError: { error in
            // Get and consume my custom error type here:
            let customError = error as! MyCustomErrorModel
            print("My API's error message: \(customError.error?.message)")
            print("My API's error code: \(customError.error?.code)")
        })

I'm able to successfully intercept and deserialize these errors into my custom error model using a custom PluginType (pasted below, from this SO question), but I do not know how to finally pass these models along to the Observer.

import Foundation
import Moya
import ObjectMapper
import Result

struct CustomAPIErrorPlugin: PluginType {

// Called immediately before a request is sent over the network (or stubbed).
func willSendRequest(request: RequestType, target: TargetType) { }

// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceiveResponse(result: Result<Moya.Response, Moya.Error>, target: TargetType) {
    let responseJSON: AnyObject
    if let response = result.value {
        do {
            responseJSON = try response.mapJSON()
            if let errorResponse = Mapper<MyCustomErrorModel>().map(responseJSON) {
                print("Custom error code from server: \(errorResponse.error?.code)")
            }
        } catch {
            print("Failure to parse JSON response")
        }
    } else {
        print("Network Error = \(result.error)")
    }
}

Answer

TheiOSChap picture TheiOSChap · Apr 20, 2017

I would suggest to extend the ObservableType since that ends up being the cleanest solution when we talk about handling api error responses. Something as below (not tested ...)

extension ObservableType where E == Response {
  func filterSuccess() -> Observable<E> {
    return flatMap { (response) -> Observable<E> in
        if 200 ... 299 ~= response.statusCode {
            return Observable.just(response)
        }

        if let errorJson = response.data.toJson(), 
           let error = Mapper<MyCustomErrorModel>().map(errorJson) {
            return Observable.error(error)
        }

        // Its an error and can't decode error details from server, push generic message
        let genericError = MyCustomErrorModel.genericError(code: response.statusCode, message: "Unknown Error")
        return Observable.error(genericError)
    }
}

This is how you use it

provider.request(.test)
        .filterSuccess()
        .mapJSON()
        .subscribe { [unowned self] e in
            switch e {
            case .next(let json as JSON):
            case .error(let error as MyCustomErrorModel):
            // Handle your custom error here
            default: break
            }
    }.disposed(by: disposeBag)