CORS 強かった…
Next.js + typescript + prisma + PostgreSQL で Bearer 認証付き API を作って、サーバに Deploy したら、CORS エラーでアプリ側から全然呼べなかった。アプリ側は、React.js で axios を使っていて、build したものが Deploy されている。
"Access-Control-Allow-Origin": "*"
を header に付けてやれば良いだけでしょと舐めてたが、そんな簡単には解決しなかった。にわか知識では突破できない CORS。強かった。
Preflight Request の理解不足が今回のハマりポイントだった。
API の概要
- header の
"Authorization": "Bearer {access_token}"
で認証 - Request の
Content-Type
は、application/json
- 認証が必要な API は、Bearer 認証をする
middleware
を通過する - API が提供する Method は、
GET
とPOST
のみ
とても一般的な構成じゃないかと思う。開発中は、Local で POSTMAN から呼び出して確認していた。ブラウザでは、POSTMAN や curl で確認した通りに動くわけではないので、注意が必要だ。という教訓。
熟読すべき資料
これが完全に分かれば、ハマりポイントはほぼない。けど、難しい。
ググると出てくるけど使えなかった解決策
Proxy 使う
でも、React で Build すると使えないから注意。というもの。確かに Proxy 使うと CORS エラーは解決するんだけど、Build するから使えなかった。
Preflight リクエストにならないようにする
GET と POST しか使ってないから Simple Request じゃないの?と思ってましたが、今回は Authorization ヘッダーや、application/json な Content-Type を使うので、Preflight Request が走ってしまうので、使えなかった。
header で Bearer 認証しなければ、これらで解決できる。うっかり、訳も分からず、Local での開発で、Proxy 設定しちゃってたりすると、発見が遅れる。
今回のポイント
Authorization ヘッダーが付いているので、GET だろうが POST だろうが、Preflight Request を回避する方法はない(fetchAPI で mode: no-cors とかはなしで)ため、Preflight Request をどう捌くかがポイントになる。
Header をセットする
以下、コードは Next.js だが、まぁどれでも似たようなものでしょう。きっと。
Access-Control-Allow-Origin
res.setHeader("Access-Control-Allow-Origin", "*");
なお、*
はよくないのだが、どこから呼ばれるか分からん API だし、token を発行する際の認証、token による認証を行うので、ノーガードではないからいったん良しとした。
ただし、Cookie や認証ヘッダー、TLSクライアント証明書などを送る場合、Access-Control-Allow-Credentials
を true
としてセットする必要があり、それをセットすると、ワイルドカードは使えなくなり、個別に設定する必要が生じる。ここで言う認証ヘッダーは、今回の Authorization ヘッダーとは別物で、例えば、Cookie を送信することでセッションを共有したいみたいな場合に、送信元の axios 等で withCredentials = true などとすると、対応が必要になる。今回は必要ないので、ワイルドカードのまま。
Access-Control-Allow-Headers
今回の場合、Authorization
と Contet-Type
と Origin
を追加する必要がある。
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Authorization, Accept"
);
Content-Type
は、CORS-safelisted request header(Accept
, Accept-Language
, Content-Language
, Content-Type
)なので、明示的に宣言する必要は、通常は、ないのだが、暗黙の Content-Type
には制約があり、
application/x-www-form-urlencoded
multipart/form-data
text/plain
この3種類しか受け付けない。今回は、application/json
を送りたいので、明示的に宣言する必要がある。
Origin
, X-Requested-With
は、Preflight Request
で送られてくるらしいので、付ける必要がある。
Accept
も同様に制約があるので、心当たりはないが、あちこち見たところ付いているので、念の為付けておいた。
と、なんだか曖昧になっているが、実際に Preflight Request の Request Header を見てみると、
-
Origin
送られてきている -
Accept
送られてきている -
X-Requested-With
送られてきていない
ので、X-Requested-With
は、必要なければ、付けなくても良いかも知れない。実際の動作的には、Authorization, Content-Type だけ追加しておけば動く。
他にも、x-oath-token とか、そんなようなカスタム header を送る場合は、カンマ区切りで使うものを列挙する必要がある。
Access-Control-Allow-Methods
GET, POST, HEAD, OPTIONS だけの場合は不要。なので、今回は不要。DELETE とか PUT とか使う場合は、許可する必要がある。必要なものをカンマ区切りで列挙する。例えば、
res.setHeader(
"Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE, PUT"
);
こんな感じで。
CORS 対策とは関係ないが、クリックジャッキング対策で、X-FRAME-OPTIONS: DENY
などを付けておくと安心。
Preflight Request の OK Status を返す
Header をセットして、これで完璧!と思いきや、
Access to XMLHttpRequest at 'http://localhost:3000/api/xxx' from origin 'http://localhost:3001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
こんなエラーが出てしまう。ショックのあまり、エラーメッセージをきちんと読めず、再度 CORS 対策をググって、合ってると思うんだけどなーと時間を無駄にすることになる。私は超無駄にした。
エラーメッセージには、「Preflight request が Access Control チェックを通らなかった: HTTP ok ステータスがない。」と書いてある。
Preflight Request に対するレスポンスを設定していないのが原因。
確認してみると、Preflight Request に対するレスポンスは、今回の私の作った API の場合、401 Unauthorized
が返っていた。Preflight Request は、リクエスト送って良いですか確認のリクエストなので、送信元でセットしたつもりのカスタムヘッダーの Authorization は送られない。送られてこないので、認証エラーとなり、API 側でセットした 401 ステータスが返る。
という訳で、Preflight Request には、OK と返してあげないといけない。Preflight Request の Method は、OPTIONS
なので、その場合は、200 を返してあげる。OK だけど、Access-Control-Allow-xxx で設定したのに反した場合は、ブラウザは本リクエストを送ってこないので、OK を返して良い。
(追記): 当初 200 返したけど、中身がないので、204 の方が正しいですね。
if (req.method === "OPTIONS") {
return res.status(204).end();
}
Preflight Request 内で、何か確認する必要がある場合は、ここで処理を書けば良いが、そこまで分かってる人は、この記事に来たりはしないだろう。
CORS、完全に理解した
Chrome で Inspect して、Network タブで確認する。