はじめに
はじめまして。
TIG DXユニットの宮本達朗です。2020年7月の新卒入社です。
業務でのフロントエンド開発時に、おなじみCORSエラーでハマってしまったのでそこで学んだ切り分け方法を共有したいと思います。
CORSエラーのトラブルシュートのために必要なこと
- CORSについて知る
- CORSが制定された背景を深く知る必要は(トラブルシュートのためには)ないですが、CORSってざっくりどんな仕組みなんだっけ? を知る必要はあります。
- 特に
プリフライトリクエスト
という概念を理解することが重要です。 - (CORS含め、HTTPに関する知識を体系的に学びたい方にはこちらの書籍がおすすめです。)
- CORSエラーについて知る
- なぜエラーが出るのか大まかに知っておきましょう。
- CORSエラーの場合分け(本記事の主題)
- 手元で発生したCORSエラーを解決するためにどこから手をつければいいかを理解しましょう。
本記事を読むことで上記の理解が深まれば幸いです。
CORSとは
オリジン間リソース共有(Cross-Origin Resource Sharing) 略してCORSです。
詳細はこちらの記事にもあります。
ざっくりいうと、
「クライアント側のオリジンとサーバー側のオリジンが異なる場合のリクエストでは、セキュリティを担保するために、ブラウザに以下2つのルールを守らせる仕組み」
がCORSです。
ポイントはブラウザに対するルールである点です。
(今日のブラウザはほぼ全てこのルールを遵守するよう作られているようです)
- ブラウザさんは、送りたいリクエスト(メインリクエスト※)の前に「サーバーさん、こういうメインリクエストを今から送ろうとしてるんですが…いいですよね?」という確認用の別リクエスト(プリフライトリクエスト)を、とある条件を満たす場合を除いて、必ずサーバー側に送信しましょう。
- 説明の簡単化のために「とある条件」については後述しています。
- ブラウザさんは、送ったリクエストに対するレスポンスのHTTPヘッダーを都度適切にチェックして、場合によっては「メインリクエスト送信」や「メインリクエストに対するレスポンスをJavaScript側へ引き渡すこと」をブロックしましょう。
- このブロックこそがCORSエラー!
ちなみに、同じリクエストでもCURLコマンドなら正常に疎通できるのにWeb UIからだとCORSエラーになる…と嘆く場面があるかもしれませんが、これはCORSがブラウザ単体に対する仕組みであるためなのです。
※説明上、「実際に送りたいリクエスト」と「ブラウザ判断でのプリフライトリクエスト」とを区別するために前者を「メインリクエスト」と呼ぶことにします。
ブラウザがやっていること
CORSの仕組みに則ったブラウザが裏でやっている(=ユーザーは意識していない)ことは以下です。
- (とある条件を満たす場合を除いて)プリフライトリクエストを送信する
- レスポンスのHTTPヘッダーをチェックする
プリフライトリクエストとは一体どんなもので、ブラウザはレスポンスのどのようなHTTPヘッダーをチェックしているのか、具体例で説明します。
シチュエーション
オリジンhttps://www.past.example.com
のクライアント側から、それとは異なるオリジンhttps://api.future.example.com
のサーバーに対してPUTリクエストを送信しようとしているシチュエーションを考えます。
プリフライトリクエストとそのレスポンス
プリフライトリクエスト
ブラウザはメインリクエストの前に、サーバー側に以下のような内容で自動でプリフライトリクエストを送ります。
プリフライトリクエストは必ずOPTIONSメソッドで送られます。
OPTIONS /api HTTP/1.1 |
上記は、
オリジンhttps://www.past.example.com
から
HTTPメソッドPUT
でリクエストを送ろうとしているのですがいいですかね?
ちなみにxxxx
というHTTPヘッダーを使うつもりのようです。
という内容です。
プリフライトリクエストに対するレスポンス
プリフライトリクエストを受信したサーバー側は以下のようなレスポンスを返します。
HTTP/1.1 200 OK |
上記は、
オリジンhttps://www.past.example.com
からなら、HTTPメソッドGET
, PUT
, OPTIONS
でのリクエストに限ってはOKです。その時使ってもいいHTTPヘッダーはxxxx
です。
ちなみに、今から10
秒間だけならこのレスポンスをキャッシュしていいですよ。
(10秒以上経ったら再度プリフライトリクエストを送り直してください)
という内容です。
プリフライトリクエストのレスポンスでチェックされるHTTPヘッダー
プリフライトリクエストのレスポンスを受け取ったブラウザはHTTPヘッダーをチェックします。
サーバー側へのアクセス許可を示す以下のHTTPヘッダー
Access-Control-Allow-Origin |
をチェックして、これから送るメインリクエストがサーバー側から
- アクセスを許可されているオリジンから送られるか?
- 許可されているHTTPメソッドか?
- 許可されているHTTPヘッダーだけを利用しているか?
を確認し、もしOKならばメインリクエストの送信を実行します。
ちなみにAccess-Control-Allow-Origin
に指定できるのは1つのオリジンまたはワイルドカード(*)のみです。
※ただしワイルドカードはセキュリティ上の理由から利用を避けた方が良いようです。
メインリクエストとそのレスポンス
メインリクエスト
プリフライトリクエストのレスポンスに問題がなかった場合、ブラウザはメインリクエストを送ります。
(この場合はPUTリクエスト)。
メインリクエストに対するレスポンス
サーバー側は送られたPUTリクエストに対するレスポンスを返します。
ただし、レスポンスには
Access-Control-Allow-Origin: https://www.past.example.com |
のようにHTTPヘッダーAccess-Control-Allow-Origin
が付与されている必要があります。
メインリクエストのレスポンスでチェックされるHTTPヘッダー
ここでブラウザが確認するHTTPヘッダーは上記Access-Control-Allow-Origin
です。
ブラウザは
「Access-Control-Allow-Origin
に記載のオリジンであればJavaScript側にレスポンス内容を引き渡して良い」
と解釈し、クライアント側のオリジンと一致しているかどうかを確認します。
オリジンが一致していた場合、レスポンスが無事引き渡されます。
CORSエラーの発生ポイント
上記を踏まえると、CORSエラーが発生するポイントは以下2つです。
- プリフライトリクエストに対するレスポンスのHTTPヘッダーをブラウザがチェックした結果、「このあとメインリクエストを送信してはダメ」と判断したとき
- つまり以下のHTTPヘッダーが適切ではなかった場合
- Access-Control-Allow-Origin
- Access-Control-Allow-Method
- Access-Control-Allow-Headers
- つまり以下のHTTPヘッダーが適切ではなかった場合
- メインリクエストに対するレスポンスのHTTPヘッダーをブラウザがチェックした結果、「このレスポンスはJavaScript側に引き渡してはダメ」と判断したとき
- つまり以下のHTTPヘッダーが適切ではなかった場合
- Access-Control-Allow-Origin
- つまり以下のHTTPヘッダーが適切ではなかった場合
ここで、プリフライトリクエストもメインリクエストも同一のサーバーへのリクエストなのだから、結局はサーバー側で「所望のHTTPヘッダーを返す」設定をミスしているのがCORSエラーの原因なのね、と思われるかもしれませんが、実はそうとは言い切れないです。
というのも、CORSの仕組みに則っているブラウザは単純に「レスポンスのHTTPヘッダー」だけを見るため、リクエストがインフラ要因によってサーバーや意図したリソースに届かなかった場合や、認証などで弾かれた場合(=リクエストが失敗した場合)にもHTTPヘッダーはチェックされるからです。
その結果、サーバーでは適切にレスポンスのHTTPヘッダーを設定しているのに「CORSエラー」となることがあります。
したがって、リクエスト種別(プリフライト or メイン)とHTTPステータスコード(200 or NOT)での場合分けが必要になります。
トラブルシュート
やっと本題ですが、リクエスト種別とHTTPステータスコードで場合分けした以下4パターンについて、どこから手をつけるべきかを整理していきます。
- プリフライトリクエストの応答が200ではなく、CORSエラーが発生したケース。
- プリフライトリクエストの応答は200でOKだったが、メインリクエストを送る前にCORSエラーが発生したケース。
- メインリクエストの応答が200ではなく、CORSエラーが発生したケース。
- メインリクエストの応答は200でOKだったが、CORSエラーが発生したケース。
実際の切り分けの際には上から順に疑ってみてください。
パターン1: プリフライトリクエストの応答が200ではない
プリフライトリクエストの応答が200ではないため、CORSエラーが発生したケースです。
基本的にはサーバー側の実装ミスもしくはインフラ側の設定ミスが疑われます。
サーバー側でAPIのハンドラーを実装したはいいものの、プリフライトリクエストに対する処理を実装・設定し忘れているケースが考えられます。
(特に初回疎通の際などにハマりやすい)。
サーバー側でプリフライトリクエストのハンドリングが実装されているか確認しましょう。
それでもCORSエラーが解決しない場合、インフラ側の設定ミスを疑っていいと思います。愚直にプリフライトリクエストがどこまで到達したのかを確認しましょう。プリフライトリクエストがOPTIONSメソッドであるための考慮漏れがあるあるかもしれません。
AWSを例に出すと、WAFで弾かれていたり、APIGateWayでOPTIONSメソッドが定義されておらず403だったり、Authorizerで弾いて403だったり…などが考えられるかと思います。
WAFで弾かれていた実例はこちらの記事にありますのでよければご覧ください。
パターン2: プリフライトリクエストの応答が200だがCORSエラー
プリフライトリクエストの応答は200でOKだったが、メインリクエストを送る前にCORSエラーが発生したケースです。
基本的にはサーバー側の実装ミスが疑われます。
プリフライトリクエストに対する応答での
Access-Control-Allow-Origin |
が適切に実装されているのかを見直しましょう。
パターン3: メインリクエストの応答が200ではない
メインリクエストの応答が200ではないため、CORSエラーが発生したケースです。
こちらも基本的にはインフラ側の設定ミスが疑われます。
プリフライトリクエストが200でないケースと同様に、愚直にメインリクエストがどこまで到達したのかを確認しましょう。
AWSを例に出すと、WAF、APIGateway、Authorizerの他、S3の設定ミスなども考えられるかと思います。
パターン4: メインリクエストの応答が200だがCORSエラー
メインリクエストの応答は200でOKだったが、CORSエラーが発生したケースです。
基本的にはサーバー側の実装ミスが疑われます。
メインリクエストに対する応答での
Access-Control-Allow-Origin |
が適切に実装されているのかを見直しましょう。
単純リクエスト
ここまで、プリフライトリクエスト→メインリクエストの順にリクエストが送られるのがさも当たり前かのように話をしてきましたが、実は例外があります。
それが「メインリクエストが単純リクエストに該当する」とみなされる場合です。
メインリクエストが単純リクエストに該当する場合、ブラウザは「プリフライトリクエストは送信せずにいきなりメインリクエストを送ってOK」と判断し、そのように実行されます。
本記事ではCORSエラーの解決に焦点を当てていますので、単純リクエストについて詳しく知りたい方はこちらをご参考ください。
まとめ
- CORSとは、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザに指示するための仕組み
- CORSエラーとは、ブラウザ側がレスポンスのHTTPヘッダーをチェックした結果、「指定リソースにアクセスする許可がない」と判定し、後続処理をブロックした場合に出るエラー
- 切り分けパターンとしては以下がある
- プリフライトリクエストの応答が200ではなく、CORSエラーが発生したケース。
- プリフライトリクエストの応答は200でOKだったが、メインのリクエストを送る前にCORSエラーが発生したケース。
- メインリクエストの応答が200ではなく、CORSエラーが発生したケース。
- メインリクエストの応答は200でOKだったが、CORSエラーが発生したケース。
お付き合いいただきありがとうございました。