Safariでローカルの開発環境でだけAjax(fetch)がCORSエラーになる件

ローカル環境(WSL)に構築した開発用サイト(http://192.168.1.xx:8000/ 的なの)をデバッグしていたんだけど、なぜか iOS の Safari でだけ場所の検索機能(OpenStreetMap の APIを使って地名から場所を取得する機能)が使えない。
PC や Android だと普通に動くほか、iOS でも本番サーバーだとうまく動く。

macOS の Safari でも同様の症状で、Web インスペクタで調べたらコンソールに
Cross-origin redirection to https://nominatim.openstreetmap.org/search?format=json&limit=1&q=%E5%8C%97%E5%8D%83%E4%BD%8F denied by Cross-Origin Resource Sharing policy: Origin http://192.168.1.xx:8000 is not allowed by Access-Control-Allow-Origin.」と CORS エラーが表示されていた。
access-control-allow-origin ヘッダは付与されてるはずなんだけど…

本番サーバーだと動くし PC や Android ならローカル開発環境でも普通に動くので実害はないんだけど、モヤモヤするので色々調べていたら、Chrome と Safari で挙動が違うことを発見。

挙動の違い

Chrome は開発者ツール → Network 、Safari は Web インスペクタ → ネットワーク から確認する。
ローカル環境の場合、Chrome の場合は

  1. http://nominatim.openstreetmap.org/search?format=json&limit=1&q=%E5%8C%97%E5%8D%83%E4%BD%8F にアクセスする
  2. API 側が HTTPS へのリダイレクトを要求するヘッダー(Non-Authoritative-Reason: HSTS)をつけて「HTTPSで接続しろ」と返してくる
  3. HTTP でのアクセス時の返答には CORS 用の access-control-allow-origin: * ヘッダはつけられていない
  4. ブラウザ側(307 リダイレクト扱い)で https://nominatim.openstreetmap.org/search?format=json&limit=1&q=%E5%8C%97%E5%8D%83%E4%BD%8F にアクセスする
  5. 普通に json が返ってくるのでそれを JavaScript 側で読み込んで処理する
  6. HTTPS でのアクセス時の返答には access-control-allow-origin: * ヘッダがつけられている

といった流れだが、Safari の場合は

  1. http://nominatim.openstreetmap.org/search?format=json&limit=1&q=%E5%8C%97%E5%8D%83%E4%BD%8F にアクセスする
  2. 先述の CORS エラーでアクセスに失敗する
  3. この時レスポンスヘッダは空扱いになっている

と行った具合。
試しに Safari → 開発 → [クロスオリジンの制限を無効にする] にチェックを入れて CORS を無効化してからリロードすると結果がかわり、

  1. http://nominatim.openstreetmap.org/search?format=json&limit=1&q=%E5%8C%97%E5%8D%83%E4%BD%8F にアクセスする
  2. HTTP ステータスコードで 301 リダイレクトが返ってくる
  3. HTTP でのアクセス時の返答には CORS 用の access-control-allow-origin: * ヘッダはつけられていないほか、Non-Authoritative-Reason: HSTS もついていない(Safari 側が独自に解釈している可能性もある)
  4. Location ヘッダに記述されている URL https://nominatim.openstreetmap.org/search?format=json&limit=1&q=%E5%8C%97%E5%8D%83%E4%BD%8F にアクセスする
  5. 普通に json が返ってくるのでそれを JavaScript 側で読み込んで処理する
  6. HTTPS でのアクセス時の返答には access-control-allow-origin: * ヘッダがつけられている

のようになり、307 リダイレクトが 301 リダイレクトになっている事・ヘッダーに Non-Authoritative-Reason: HSTS がついていない事を除き Chrome と同じ結果になった。

結論

307 Internal Redirect は疑似リダイレクト

Chrome では HTTP から HTTPS のリダイレクト時に 307 Intenal Redirect が行われていることになっているが、Stack Overflow 曰く「307リダイレクトは通信をわかりやすくするための疑似的な応答で、実際は一度 HTTP から HTTPS に 301 リダイレクトされると以降は HTTP の URL が指定されても強制的に HTTPS の URL で通信されるようになっている(HTTP ではアクセスしない)」ということらしい…
この仕組みを HSTS (HTTP Strict-Transport-Security) と呼ぶそう。

また、Non-Authoritative-Reason: HSTS は HSTS による疑似リダイレクトであることを示すために Chrome 自身が出しているヘッダーで、実際に使われている訳ではない模様。

HSTS Preload List

通常、HSTS は明示的にヘッダーに Strict-Transport-Security ヘッダーを付けない限り、HSTS での通信は行われない。
アクセス先の URL
には Strict-Transport-Security ヘッダーは付与されていないので、通常 HSTS での通信は行われず、HTTP で一度アクセスして301リダイレクトした URL のレスポンスが返ってくるはず。

調べてみるとこんな記事を発見。曰く、
ブラウザが HSTS ホストとして認識していないホストにアクセスするときは、HTTP でアクセスせざるを得ないことが有り得ます。 この対策として、各ブラウザはそのリリース段階で多数のホストを HSTS ホストとして登録しています。上に挙げた twitter.com とか wikipedia.org とかはまさにその例です。 こうやって、一度たりとも HTTP を許さない方法を Preloaded HSTS とか呼んだりします。
とのこと。

Preloaded HSTS のリストは HSTS Preload List として管理されており、そのうち Chrome 用の HSTS Preload List は https://hstspreload.org/ にて確認できる。
HSTS Preload List に登録されたサイトはブラウザにハードコードされる。ブラウザ内の HSTS Preload List に載っているサイト(ex:Wikipedia)にアクセスしようとすると HTTPS に対応している事が確実なので http:// と URL に指定しても強制的に HTTPS でアクセスされる、というものらしい。

早速 nominatim.openstreetmap.org を入れてみると、「(HSTS Preload List に登録されていて)プリロードされています」という表示になった。
また、openstreetmap.org だけでも同様の結果だったので、サブドメインも含む openstreetmap.org 全体が HSTS Preload List に登録されていると考えられそう。

Firefox 用の HSTS Preload List はソースコードから参照できるが、世界中のドメインが載っている分ロードがめちゃくちゃ重いので興味本位で覗くのはおすすめしない(一応登録されてることは確認できた)。
一度ローカルに落としてからの方が手っ取り早そう…

適当に nicovideo.jp を入れてみると「プリロードされていません」と表示されるので、有名なサイトなら全部載っているというものではないみたい。
ちなみにここで自分のドメインを入力してテストに合格した場合、その場で自分のドメインを Chrome 用の HSTS Preload List に登録することもできる(ブラウザに自分のサイトのドメインがハードコードされると考えると感慨深いものがありそう、やらないけど)

この記事によると、ブラウザのソースコードにハードコードされるという関係上、HSTS Preload List からの削除申請は最低でも数ヶ月かかるらしい(無闇にやらないほうが良さそう)
そもそも Google がリダイレクト用の HTTP 通信すら絶対許さないマンなのがよく分からないけど…(それくらいいいじゃんと思ってしまう)

HSTS Preload List に登録するためには HSTS ヘッダーがついていることが条件になるはずだが、OpenStreetMap はそこそこ有名なので OpenStreetMap 側は何もしてないけど最初から HTTPS のサイトということで登録されていた可能性もありそう(参考)。

…ここからが重要だが、どうも「HSTS 自体は主要なブラウザは全て対応しているが、HSTS Preload List は Chrome と Firefox のみの導入、さらに先ほどの https://hstspreload.org/ から登録できるのは Chrome(Chromium)用の List だけで、Firefox の List は別管理」らしいということ。

つまり、ここで
HSTS Preload List が導入されている(そして HSTS Preload List に openstreetmap.org が載っている)Chrome と Firefox は OpenStreetMap の API に HTTP ではアクセスせず、最初から HTTPS でアクセスする(疑似リダイレクト)ため CORS エラーを回避できるが、HSTS Preload List を導入していない Safari などの他のブラウザは HTTP で一度アクセスしてから 301 リダイレクトしようとするため何らかの理由で CORS エラーが発生してしまう
という可能性が浮上してくる。

OpenStreetMap API の HTTP ヘッダ

これはもしや…? と思い(Chromium になる前の)旧 Edge で場所の検索機能を使ってみたが、Safari 同様に CORS エラーが発生することが判明。

curl で HTTP と HTTPS のレスポンスヘッダを比較してみると、HTTPS でのアクセス時は CORS 対策用の access-control-allow-origin: * と access-control-allow-methods: OPTIONS,GET ヘッダが付与されているが、HTTP でのアクセス時は リダイレクト先を指定する Location ヘッダはあるものの、先程の CORS 対策用ヘッダが付与されていない事が判明。

CORS のエラーはどうやらリダイレクトの場合でも適用されるらしく、HTTP では一切アクセスせず直で HTTPS にアクセスする Chrome・Firefox は CORS エラーにならないが、
HSTS Preload List を導入していない( or HSTS Preload List に openstreetmap.org が載っていない)Safari・旧 Edge などのブラウザだと、一旦 HTTP でアクセスしてからリダイレクトするためリダイレクトページへのアクセス自体で CORS エラーが発生してしまい、301リダイレクトそのものが実行されず JS 側でレスポンスが受け取れないため場所の検索機能が使えなくなる、というのが真相らしい(長かった…)

本番サーバーでは普通に動くのもこれで説明がつく。
アクセスする際の URL 指定は

fetch('//nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURI(address))
.then(function (response) {
    return response.json();
}).then(function (geocode) {
    // 処理
});

のように //nominatim.openstreetmap.org とプロトコルを省略したコードになっていたため、サイトに HTTP でアクセスしている場合は HTTP で、HTTPS でアクセスしている場合は HTTPS でアクセスされる。

開発用のローカルサイトは HTTP で、本番サーバーのサイトは HTTPS で運用されており、
本番サーバーのサイトは常に HTTPS でリクエストが飛ぶため、元凶である Safari・旧Edge のみの301リダイレクトがそもそも発生しなかったことが要因と考えられる。

まとめ

HTTPS のみの API を叩く時は必ず URL に https:// と指定しよう!!

今回は使っている API がたまたま HTTP でアクセスした時に CORS ヘッダを返さないというのが大本の原因だったが、そもそも HTTPS アクセスオンリーの API を叩くときは最初から https:// で指定した方が良さそう(リダイレクトが無駄だし)

HTTP でアクセスした時に CORS ヘッダを返さない公開 API はあまりないだろうし相当レアケースなんだろうけど、HSTS や CORS などなどかなり勉強になったので無駄ではなかったという事にしておく…(かなり疲れた)

コメント