CORSエラー(「No 'Access-Control-Allow-Origin' header is present」)は、http://localhost:5173 のようなあるオリジンのWebページが別のオリジンにあるAPIを呼び、レスポンスがそのオリジンを許可すると言っていなかったために、ブラウザがページにレスポンスを読ませなかったことを意味します。直すのはサーバー側です。ページのオリジンを入れた Access-Control-Allow-Origin を送り、ブラウザのプリフライトの OPTIONS リクエストには受け付けるメソッドとヘッダーで答えます。Expressなら数行のミドルウェアか、cors パッケージの app.use(cors()) で済みます。
レスポンスは商品の一覧で、Headersタブには access-control-allow-origin: https://app.example.com と表示されます。そのオリジンのページなら読んでよいということです。次に、ヘッダー Origin: https://app.example.com と Access-Control-Request-Method: PUT を付けて OPTIONS /api/products/1 を送ってください。これはブラウザが PUT の前に送るプリフライトで、返事は許可するメソッドとヘッダーを持つ、ボディのない 204 です。
ブラウザが確認するもの
同一オリジンのページなら何もいりません。クロスオリジンのリクエストでは、ブラウザは次のレスポンスヘッダーを読みます。
| ヘッダー | 意味 |
|---|---|
Access-Control-Allow-Origin | レスポンスを読んでよい1つのオリジン、またはすべてなら * |
Access-Control-Allow-Methods | 本番のリクエストで許可するメソッド(プリフライトのみ) |
Access-Control-Allow-Headers | ページが送ってよいリクエストヘッダー、たとえば Authorization(プリフライトのみ) |
Access-Control-Allow-Credentials | ページがCookieを送って返事を読んでよいなら true |
Access-Control-Max-Age | ブラウザがプリフライトの返事をキャッシュしてよい秒数 |
Vary: Origin | 返事が Origin リクエストヘッダーによって変わることをキャッシュに伝える |
単純なGETやフォーム形式のPOSTはそのままサーバーに届き、ブラウザは返事の Access-Control-Allow-Origin を確認します。サーバーはどちらにしてもリクエストを実行します。CORSが決めるのは、ページが結果を見られるかどうかだけです。JSONのボディ、Authorization ヘッダー、PUT や DELETE のようなメソッドを持つリクエストの前にはプリフライトの OPTIONS リクエストが送られ、プリフライトの返事が許可したときだけ本番のリクエストが送られます。だからCORSのミドルウェアは、どのルートや認証チェックよりも先に OPTIONS に答えなければなりません。
複数のオリジンを、Cookie付きで許可する
Access-Control-Allow-Origin に入るのは1つのオリジンか * で、リストは入りません。複数を許可するには、リクエストの Origin ヘッダーを許可リストと比べ、一致したらそれをそのまま返します。
最初のリクエストには、curlや同一オリジンのページからの呼び出しと同じく Origin ヘッダーがないので、{"origin":null} が返り、CORSのヘッダーは付きません。ヘッダー Origin: http://localhost:5173 を付けて GET /api/me を送ると、Headersタブにはそのオリジンがそのまま返されているのと、access-control-allow-credentials: true が表示されます。ヘッダーを Origin: https://evil.example に変えると、2つのCORSヘッダーはどちらも消えます。サーバーは答えますが、ブラウザならページにそれを読ませません。Access-Control-Allow-Credentials: true があるとページはCookieを送れます(fetch(url, { credentials: 'include' }) で)。ブラウザはこれと * の組み合わせを拒否するので、Cookieを使うなら常に正確なオリジンが必要です。
corsパッケージ
cors パッケージは同じことをオプションで行います。npm install cors でインストールします。
const cors = require('cors');
// Any origin, no cookies: fine for a public read-only API.
app.use(cors());
// Only your frontend, with cookies.
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// One route only.
app.get('/api/public', cors(), handler);
オプションなしで呼ぶと、cors() は Access-Control-Allow-Origin: * を送り、すべてのプリフライトに答えます。* はログインのいらない公開データだけに使いましょう。
よくある間違い
ルートや認証ミドルウェアの後にCORSを追加する。 プリフライトの OPTIONS リクエストには Authorization ヘッダーが付かないので、先に置かれた認証チェックが 401 で答え、ブラウザはCORSエラーを報告します。CORSのミドルウェアは最初に登録しましょう。
Cookie付きで * を設定する。 ブラウザはそのレスポンスを拒否します。代わりに特定のオリジンをそのまま返しましょう。
フロントエンドでCORSを直そうとする。 mode: 'no-cors' のようなリクエストのオプションは制限を外してくれません。読めない不透明なレスポンスが返るだけです。直せるのはサーバーのヘッダーだけです。
app.use() とミドルウェアの順番の仕組みについては、Expressのミドルウェアを参照してください。
よくある質問
APIがPostmanやcurlでは動くのに、ブラウザでは動かないのはなぜですか?
CORSを強制するのはブラウザだけです。Postman、curl、ほかのサーバーはCORSのヘッダーを無視するので、どちらにしてもレスポンスを読めます。ブラウザは、サーバーがそのオリジンを許可していない限り、別のオリジンのページのスクリプトにレスポンスを渡しません。
CORSはサーバーを守るセキュリティ機能ですか?
守るのはユーザーで、サーバーではありません。悪意のあるページが、ユーザーのCookieを使ってあなたのAPIのレスポンスを読むのを防ぎます。スクリプトやサーバーからなら誰でもAPIを呼べるので、認証と認可は引き続きあなたの仕事です。
何が違うオリジンになりますか?
オリジンはスキーム、ホスト、ポートの組み合わせです。http://localhost:5173 と http://localhost:3000 はポートが違うので別のオリジンで、https://example.com と https://api.example.com はホストが違います。
開発中にCORSを避けられますか?
できます。リクエストを同一オリジンにすればよいのです。フロントエンドの開発サーバー(たとえばViteの server.proxy)で /api をExpressのサーバーに転送するよう設定すると、ブラウザは1つのオリジンとだけやり取りするので、CORSのヘッダーは不要になります。