環境
xcode10.1
iOS12.1
Swift4.2
まえがき
商品の検索結果をTableViewで表示する際にページングを行いたく、レスポンスヘッダーに含まれる
X-Total-Pages
の値を取得していろいろ使おうとしていた。
https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
でレスポンスヘッダーが取得できるらしい。
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
そのため
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")
という暫定的な対策をとりました。
間違っていたり、原因が違っていた場合は教えてもらえるとありがたいです。