Moya を使ってレスポンス body が空の時、ステータスコードによって成功・失敗を判断する方法

以前 Moya を使った時に、

  • API からステータスコードのみが送られる
  • レスポンスの body は空である

というケースに出会いました。
僕は、「はいはい、どうせ空のレスポンスを作って上げれば .success 返ってくるんでしょ」を思って実装したのですが、、、
テストしてみると全然成功してくれない。。。

こんなときの対処法を紹介します。
「こうすればもっとシンプルになるよ!」という意見あったら下さい!!!!!

最初にざっくり結論

今から、実装クラスを紹介してから対処法を紹介しますが、すぐ知りたいせっかちな方のために結論を。

  • パースを失敗させる
  • すると、MoyaError.objectMapping(Swift.Error, Response) という Error になる
  • この Response には response プロパティがある
  • そして、 response プロパティには statusCode: Int があるからそれを見ればいい!!

API の仕様

  • 成功時: status code: 200 – 204 が返ってくる
  • body: none である
  • post メソッド

という API です。

実装するクラス

まずは API Client から。
ApiClient という型を定義し、 ApiClientImpl がそれに準拠する実装クラスになっています。
Moya を使ったこと無い方はわかりにくいところもあると思いますが、

  • request メソッドで API にリクエストする
  • RequestCodable に準拠したパースしたい実体 (構造体) が入る
  • 非同期で動作
  • 結果は Result<Codable, Error> で返ってくる
  • .get, .post などどんなメソッドでもこの ApiClient が担う

という至って普通の API Client だと思います (多分)。
ちなみに、 Impl は「実装」を意味する “Implementation” から来ています。

import Moya

protocol ApiClient {
    var provider: MoyaProvider<MultiTarget> { get set }
}

class ApiClientImpl: ApiClient {
    func request<Request: ApiTargetType>(_ request: Request, completion: @escaping (Result<Request.Response, Error>) -> Void) {
        let target = MultiTarget(request)
        self.provider.request(target) { result in
            switch result {
            case .success(let response):
                let jsonDecoder = JSONDecoder()
                jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
                jsonDecoder.dateDecodingStrategy = .iso8601
                do {
                    let decodedResponse = try response.map(Request.Response.self, using: jsonDecoder)
                    completion(.success(decodedResponse))
                } catch {
                    completion(.failure(error))
                }
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

次にこの ApiClientImpl を呼び出す repository です。

import Moya

class Repository {
    var apiClient: ApiClient = ApiClientImpl()

    func post(completion: @escaping (Result<PostTarget.Response, Error>) -> Void) {
        let request = PostTarget()
        apiClient.request(request) { result in
            switch result {
            case .success:
                completion(.success(.init()))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

(ネスト深いのは許してくれ。。。。)

最後に PostTarget です。
URL や response, Moya のメソッドなどを定義しています。
もし GET メソッドであれば、この中の Response にパースしたい実体を紐付けます。

import Moya

struct  PostTarget: TargetType {
    typealias Response = PostEntity

    var baseURL: String {
        return "base.url"
    }

    var path: String {
        return "path/to/api"
    }

    var method: Moya.method {
        return .post
    }

    var parameters: [String: Any] {
        return ["testParam1": "testContent"]
    }
}

やってはいけない対処法 (失敗例)

まず、body: none なのでマッピングはできません。
僕はよわよわ iOS エンジニアなので、最初以下のような空のレスポンスを定義してました。

// この実体を空にすれば成功すると思ってた。。。。😭
struct PostEntity { }

response を定義した構造体を空にすれば Moya が
Moya 「あ、この request はレスポンスないんだね! (従順)」
なんて察してくれると思っていました。。。

が失敗。
Moya 「レスポンスがマッピングできんかったわ。失敗な。一昨日来やがれ。」
って言われました。(言われてない)

成功した対処法

コードからいきなり書いていきますが、後で軽く説明します。
まず、repository に以下のようなメソッドを作ります。

extension Repository {
    private func isSuccess(error: Error) -> Bool {
        if case MoyaError.objectMapping(_, let response) = error {
            return 200...204 ~= response.statusCode
        } else {
            return false
        }
    }
}

(なにやら response.statusCode とやらを比較していますねぇ)

そしてこれを、repository エラーハンドリング部の内、case .failure: に組み込みます。

// さっき書いたので細かい部分は略
apiClient.request(request) { result in
    switch result {
        case .success:
            completion(.success(.init()))
        case .failure(let error):
            if self.isSuccess(error: error) {
                completion(.success(.init()))
            } else {
                completion(.failure(error))
            }
        }
    }
}

これで、body が空でも、 repository がステータスコードを判定して .success を投げてくれるようになりました!!🎉

何をしているか

isSuccess(error: Error) -> Bool メソッドは、「ApiClient がエラーを投げてきても、ステータスコードから判断して成功 or 失敗を Bool で返してくれるメソッド」です。
軽く順序立てて説明してみます。(説明めっちゃ下手でごめん)

  1. まず、レスポンス body が空のものをパースしようとすると、パースできない (返ってくるパース対象がない) のでエラーとなります。
  2. これは、 Moya の map メソッドで失敗しています。
  3. この場合のエラーは、 MoyaError.objectMapping というエラーになります。
  4. なので、 if case MoyaError.objectMapping(_, let response) = error によって、ApiClient から返ってきたエラーが MoyaError.objectMapping かを判定します。
  5. 次に、この MoyaError.objectMappingResponse 型引数を持ち、Response 型には statusCode: Int というプロパティが定義されています。
  6. そして、この statusCode で判定すれば良いということになります。
  7. 今回は、 200 から 204 に入れば成功となるので、return 200...204 ~= response.statusCode を返しています。

おわり

これでMoya を使ってレスポンスが空かつ、ステータスコードによって成功・失敗を判断することができました。

でもなんか、そもそも ApiClient.failure を返すのが気に入ってないんですよね。。。
(その場しのぎ感がすごい)
なんかフラグを変えて、ステータスコードによって判定するようにしてくれたりしないのでしょうか。。。

初めて Moya 使ったので、もっといい方法あれば教えて下さい。

Licensed under CC BY-NC-SA 4.0
最終更新 2022/09/17 15:55 +0900
Built with Hugo
テーマ StackJimmy によって設計されています。