2016/09/24: APIKit 3向けに内容をアップデートしました。

Web APIのエラーレスポンス

大抵のWeb APIでは、エラーレスポンスが定義されています。たとえば、GitHub APIではHTTPステータスコードに応じて次のようなレスポンスが返されます。

400 Bad Request

{"message":"Problems parsing JSON"}

422 Unprocessable Entity

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ]
}

今回は、このようなエラーをリクエストの呼び出し側に伝える方法を説明します。なお、この説明はDefining Request Protocol for Web Serviceのエラーの扱いをもう少し詳しく説明したものとなります。

サービス用のリクエストプロトコル

APIKitでは、特定のサービス(Web API)向けのリクエストの特徴をまとめるために、サービス用のリクエストプロトコルを定義します。今回はGitHub APIを例としているので、baseURLのデフォルト値がhttps://api.github.comとなっているGitHubRequestを定義しました。

protocol GitHubRequest: Request {

}

extension GitHubRequest {
    var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }
}

本記事のタイトルの”レスポンスに応じた独自のエラーを投げる”という動作は、このリクエストプロトコル上に定義します。動作を定義する前に、まずは投げる対象のエラーの型を定義します。

エラーレスポンスの構造体

GitHub APIのエラーレスポンスは次のような構造体で表します。

// 話を単純にするために422の`errors`は省略。
struct GitHubError: Error {
    let message: String

    init(object: Any) {
        let dictionary = object as? [String: Any]
        message = dictionary?["message"] as? String ?? "Unknown error occurred"
    }
}

レスポンスに応じた独自のエラーの生成

APIKitでは、intercept(object:urlResponse:)でレスポンスのAnyを横取りすることができます。次の例では、HTTPステータスコードが200..<300なら通常通りにレスポンスのAnyを返し、それ意外ならAnyからGitHubErrorを生成してthrowしています。

extension GitHubRequest {
    func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
        guard (200..<300).contains(urlResponse.statusCode) else {
            throw GitHubError(object: object)
        }

        return object
    }
}

リクエストの結果の受け取り

APIKitではリクエストの送信は、Sessionsend(_:handler:)で行い、その結果はResult<Request.Response, SessionTaskError>として受け取ります。エラーの型であるSessionTaskErrorは、次のように定義された列挙型です。

public enum SessionTaskError: Error {
    case connectionError(Error)
    case requestError(Error)
    case responseError(Error)
}

SessionTaskErrorはエラーの発生箇所を表し、実際に何が起きたかはそれぞれの連想値が表します。intercept(object:urlResponse:)で投げたエラーはレスポンス由来のエラーなので、responseErrorの連想値に入ります。

独自のエラーのマッチング

いざリクエストを実行してみると、通信に失敗したり、JSONが壊れていたり、サーバーが変なレスポンスを返したりと結果はさまざまです。その中から、想定内のエラーレスポンスGitHubErrorをマッチングするには、以下のようにswitch文を書きます。

let request = GitHubAPI.SearchRepositoriesRequest(query: "APIKit")

Session.send(request) { result in
    switch result {
    case .success(let response):
        print("Response: ", response)

    case .failure(.responseError(let gitHubError as GitHubError)):
        print("GitHub error: \(gitHubError.message)")

    case .failure(let error):
        print("Unknown error: \(error)")
    }
}

コード中のgitHubErrorという定数の型はGitHubErrorになっているので、GitHubErrorのプロパティであるmessageにもアクセスできるというわけです。

こうして、独自に定義したエラーをリクエストの呼び出し側に伝えることができました。

まとめ

AnyへのサブスクリプトやHTTPURLResponseのステータスコードの比較など、より原始的(?)な型を扱うコードはミスを犯しやすいです。今回の例では、こういった類のコードをプロトコル側にまとめることができました。結果として、send(_:handler)を実行するUIViewControllerなどは安全な操作だけで済むようになり、アプリの品質も上がるかもしれません。