LoginSignup
4
1

More than 5 years have passed since last update.

URLResposne.allHeaderFieldsでレスポンスヘッダーの値を取得しようとしたらつまずいた

Posted at

環境

xcode10.1
iOS12.1
Swift4.2

まえがき

商品の検索結果をTableViewで表示する際にページングを行いたく、レスポンスヘッダーに含まれる
X-Total-Pagesの値を取得していろいろ使おうとしていた。
https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
でレスポンスヘッダーが取得できるらしい。

api.swift(APIKit使用)
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
  // 検索結果がない場合は0が入るため、forced unwrappingにしていた
  let totalPages =  urlResponse.allHeaderFields["X-Total-Pages"] as! String
}

といった実装をしていて、しばらく問題がなかったが、ある日リリースビルドしたアプリをTestFlight上で確認したところ、検索を行った際にクラッシュしていて、原因を調べてみたらこの箇所がnilになっていた。

レスポンスを確認してみても0またはそれ以上の値が入っているし、サーバー担当に確認してみても、ここが空になることはないという。

原因1: サーバーのレスポンスヘッダーに含まれているキーが小文字になっていた

リリースビルドの方
x-total-pages 64
開発の方
X-Total-Pages 64

そのため

api.swift(APIKit使用)
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
  // "X-Total-Pages"というキーがないためリリースビルドではnilになっていた
  let totalPages =  urlResponse.allHeaderFields["X-Total-Pages"] as! String
}

自分が調べたところによると、ネットワークによっては小文字を返す場合がある(らしい)、サーバーのバージョン違い、アプリのバージョン違いなど考えられるそうです。
調べた分だとバージョン違いに関してはなさそうでした。
なぜ小文字になったのか、原因が特定できていないため、ご存知の方がいたら教えていただけるとありがたいです。

原因2: Swiftのバグ (仕様?)

大文字小文字の違いで値が取得できずnilになったとは言いましたが、
前提として、HTTPHeaderは大文字小文字を区別しないはずです。

これはドキュメントの
https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
にも書かれています。

(google翻訳したやつ)この辞書のキーは、サーバーから受け取ったヘッダーフィールド名です。一般的に使用されているHTTPヘッダーフィールドのリストについては、RFC 2616を参照してください。
HTTPヘッダーは大文字と小文字を区別しません。コードを単純化するために、特定のヘッダーフィールド名は標準形式に正規化されています。たとえば、サーバーがcontent-lengthヘッダーを送信すると、自動的にに調整されますContent-Length。

とドキュメントにも書かれていますが、実際allheaderfieldsは大文字小文字を区別してしまっています。
これはSwiftのバグ(仕様?)で、ドキュメントも更新されずそのままのようです。
https://bugs.swift.org/browse/SR-2429

2016年ごろからあるらしいですね。

対策

URLResponseにすべて小文字として取得できるものをはやしました。

// https://stackoverflow.com/questions/40152483/httpurlresponse-allheaderfields-swift-3-capitalisation/40152676#40152676
extension HTTPURLResponse {
    func find(header: String) -> String? {
        let keyValues = allHeaderFields.map { (String(describing: $0.key).lowercased(), String(describing: $0.value)) }
        if let headerValue = keyValues.filter({ $0.0 == header.lowercased() }).first {
            return headerValue.1
        }
        return nil
    }
}

// usage 存在しない場合nilが入るのでよしなに処理する
let total = urlResponse.find(header: "X-Total-Pages") 

という暫定的な対策をとりました。

間違っていたり、原因が違っていた場合は教えてもらえるとありがたいです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1