iOS
HTTP
Swift
swift4

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


環境

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")

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

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