CORSエラーの直し方(そしてなぜ起きるのか)
CORSエラーは、あなたのWebページが別のオリジン(別のドメイン、ポート、プロトコル)からレスポンスを読もうとしたのに、そのサーバーが許可を示すヘッダーを返さなかったことを意味します。利用者を守るため、ブラウザはそのレスポンスをブロックします。直すには、サーバー側で、あなたのサイトのオリジンに対するAccess-Control-Allow-Originヘッダーを返し、OPTIONSのプリフライトリクエストを処理します。もしくは、同じオリジンにある自分のバックエンドを経由してリクエストを送ることで、そもそもこの問題を避けられます。
リクエストは多くの場合サーバーに問題なく届くため、Web開発の中でも特に分かりにくいエラーの一つです。このガイドでは、何が起きているのか、そして各パターンの直し方を説明します。
「オリジン」とは
オリジンとは、プロトコル、ドメイン、ポートの組み合わせです。
| URL | オリジン |
|---|---|
https://myapp.com/dashboard | https://myapp.com |
https://api.myapp.com/users | https://api.myapp.com(サブドメインが違うため、別のオリジン) |
http://localhost:5173 | http://localhost:5173 |
http://localhost:3000 | http://localhost:3000(ポートが違うため、別のオリジン) |
ブラウザは同一オリジンポリシーに従います。ページのJavaScriptは、自分と同じオリジンのレスポンスは自由に読めますが、他のオリジンのものは読めません。CORS(Cross-Origin Resource Sharing)は、サーバーがそれを許可するための仕組みです。「私のレスポンスは、これらのオリジンのページが読んでよい」と宣言します。
次の2つの点は、多くの人を驚かせます。
- リクエストは多くの場合サーバーに届いています。 CORSがブロックするのは、通常、あなたのコードがレスポンスを読むことであって、サーバーがリクエストを受け取ることではありません。エラーが表示されていても、POSTは保存されている可能性があります。
- 強制するのはブラウザだけです。 curl、Postman、サーバーサイドのコードはCORSをまったく無視します。「Postmanでは動く」というのは、CORSについて何も教えてくれないのです。
ステップ1:正確なエラーを読む
開発者ツールを開き(右クリック → 検証)、コンソールに移動して、エラーメッセージ全文を読みましょう。Chromeの文言から、どの問題かが分かります。
| メッセージに含まれる文言 | 何が問題か |
|---|---|
No 'Access-Control-Allow-Origin' header is present | サーバーがヘッダーをまったく送っていない、またはリクエストがあなたのCORSのコードに届く前に失敗している(404や500など) |
The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin | サーバーが、あなたのものとは別のオリジンを許可している |
Response to preflight request doesn't pass access control check | OPTIONSのプリフライトが失敗した |
...must not be the wildcard '*' when the request's credentials mode is 'include' | Cookieを送っているのに、サーバーが*で応答した |
Request header field authorization is not allowed | サーバーが、そのヘッダーをAccess-Control-Allow-Headersに含めていない |
そのうえでネットワークタブを開き、失敗したリクエスト(およびその前にあるOPTIONSリクエスト)を見つけ、そのステータスコードとレスポンスヘッダーを確認します。
まずステータスコードを確認しましょう。 「CORSエラー」の多くは、実際にはサーバーエラーが姿を変えたものです。サーバーがクラッシュしたり404を返したりすると、そのエラーレスポンスにはCORSヘッダーが含まれないことが多いため、ブラウザは本当の問題ではなくCORSとして報告します。500や404を直せば、CORSエラーも消えることがあります。
ステップ2:そもそもそのAPIをブラウザから呼ぶべきかを決める
ヘッダーを追加する前に問いましょう。ブラウザがこのAPIを直接呼ぶべきか?
- 秘密のキーを必要とするサードパーティのAPI(OpenAI、Stripeのシークレットキー、多くのメールサービスなど)は、ブラウザから呼ぶべきではありません。そのキーはすべての訪問者から見えてしまいます。代わりにバックエンドから呼び出しましょう。サーバー間のリクエストにはCORSが適用されないため、CORSの問題は消えます。APIキーを安全に保つ方法をご覧ください。
- 自分では管理していないサードパーティのAPIで、CORSヘッダーを送っていないものは、あなた側では直せません。バックエンドから呼びましょう。
- 自分のAPIなら、適切に設定すれば直せます。以下に進みましょう。
ステップ3:サーバー側で直す
サーバーは、エラーレスポンスを含むすべてのレスポンスに、正しいヘッダーを追加しなければなりません。
実際のリクエストについては、次を返します。
Access-Control-Allow-Origin: https://myapp.com(フロントエンドの正確なオリジン。末尾のスラッシュなし)- 値がどのオリジンからのリクエストかによって変わる場合は、
Vary: Origin
プリフライトのOPTIONSリクエストについては、成功のステータス(200または204)と次を返します。
Access-Control-Allow-Origin: https://myapp.comAccess-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETEAccess-Control-Allow-Headers: Content-Type, Authorization(送っているカスタムヘッダーすべて)- 任意で
Access-Control-Max-Age: 600。ブラウザがプリフライトの結果をしばらくキャッシュするようにします
ほとんどのフレームワークには、これを行うパッケージや設定があります。Expressではcorsパッケージ、FastAPIではCORSMiddleware、ASP.NET CoreではAddCors、Springでは@CrossOriginまたはCORSの設定です。典型的なExpressの設定は次のとおりです。
import cors from "cors";
app.use(cors({
origin: ["https://myapp.com", "http://localhost:5173"],
credentials: true,
}));
CORSの処理は、ルートや認証の前に置いてください。ログイン情報を持たないプリフライトのリクエストが、401で拒否されないようにするためです。
AIビルダーを使う場合は、設定を正確に説明しましょう。
https://myapp.comのフロントエンドがhttps://api.myapp.comのAPIを呼び出していて、「Response to preflight request doesn't pass access control check」が出ています。APIのCORSを、https://myapp.comとプレビューのオリジンだけを許可するように設定し、Content-TypeとAuthorizationヘッダーを付けてGET、POST、PATCH、DELETEを許可し、OPTIONSを認証の前に処理してください。エラーレスポンスにもCORSヘッダーを含めてください。
ステップ4:Cookieと認証情報を扱う
フロントエンドが別のオリジンにCookie(たとえばログインセッション)を送る場合、両側が合意している必要があります。
- フロントエンドは、
fetchでcredentials: "include"を設定するか、axiosでwithCredentials: trueを設定します。 - サーバーは
Access-Control-Allow-Credentials: trueを送ります。 - サーバーの
Access-Control-Allow-Originは、*ではなく正確なオリジンでなければなりません。
クロスサイトのCookieには、そのCookieにSameSite=None; Secureも必要です。さらに、一部のブラウザはサードパーティCookieをより厳しく制限しています。これも、ステップ5の方法を優先すべき理由の一つです。
ステップ5:同一オリジンの構成でCORSを避ける
しばしば最もすっきりした解決策は、そもそもクロスオリジンのリクエストを送らないことです。APIを、ページと同じオリジンで提供します。たとえばhttps://myapp.com/api/...です。
- 開発環境では、多くの開発サーバーがプロキシできます。Viteの server.proxy設定では、
/apiを別のポートのバックエンドに転送できるため、ブラウザは常に1つのオリジンとだけ通信します。 - 本番環境では、ホストのリライトやプロキシの設定によって
/apiをバックエンドに振り分けるか、フロントエンドとAPIを1つのサーバーで提供します。
こうするとフロントエンドは、fetch("/api/orders")のような相対パスを呼び出せます。クロスオリジンのリクエストも、オリジンをまたぐプリフライトも、Cookieの面倒もありません。また、プレビューと本番で異なるAPIのドメインをハードコードする必要もなくなります。
フロントエンドが完全なURLの代わりに、相対パスの/apiでバックエンドを呼ぶように変更し、/apiがAPIサーバーに転送されるように開発用のプロキシを設定してください。そのうえで、もう必要ないCORSの設定をすべて削除してください。
やってはいけないこと
fetchでmode: "no-cors"を使う。 何も回避できません。コードには、空で読めないレスポンスが渡されるだけです。- CORSを無効にするブラウザ拡張機能。 変わるのは自分のブラウザだけです。すべての訪問者は、今もエラーを見ます。
- 本番のアプリでの公開の「CORSプロキシ」サービス。 そのサービスは、経由するすべてのデータを読み取り、改変できます。また、サービスが止まることもあります。
- ログインを使うAPIでの
Access-Control-Allow-Origin: *。 認証情報とは、とにかく動きません。非公開のデータには誤ったデフォルトです。 - 認証情報を許可した状態で、どの
Originヘッダーもそのまま返す。 これを行うと、どのWebサイトでも、利用者に代わってログイン済みのリクエストを送れてしまいます。許可リストを使いましょう。
AIで作ったアプリでよくある原因
| 状況 | 直し方 |
|---|---|
| フロントエンドが秘密のキーを使ってサードパーティのAPIを呼んでいる | 呼び出しをバックエンドに移す |
| プレビューでは動くが、公開ドメインで失敗する | 公開ドメインとカスタムドメインのオリジンを許可リストに加える、または相対の/apiパスを使う |
| POST、PUT、DELETEのときだけエラーになる | プリフライトが処理されていない。OPTIONSの処理と許可するヘッダーを追加する |
| ログインを追加したらエラーが出た | 認証情報には、正確なオリジンとAllow-Credentialsが必要 |
| 「CORSエラー」だが、ネットワークタブには500が表示されている | まずサーバーのエラーを直す |
APIのURLがlocalhostにハードコードされている | 相対パスを使うか、環境ごとの設定にする |
MythexでのCORS
MythexのプロジェクトにWebサイトとAPIサーバーの両方がある場合、どちらも同じプロジェクト内のサービスとして動き、プレビューで切り替えられます。フロントエンドが、開発時にプロキシされる相対の/apiパスを通じてバックエンドを呼ぶよう、エージェントに頼むと、CORSの問題のほとんどを避けられます。どうしてもクロスオリジンの呼び出しが必要な場合は、コンソールの正確なエラーと、関係するオリジンをエージェントに伝えてください。公開アプリはプレビューとアドレスが異なるため、公開アプリでも結果を確認してください。ドキュメントのマルチサービスアプリとAPIバックエンド、そしてAIで作ったアプリのデバッグ方法とフロントエンドとバックエンドの違いのガイドもご覧ください。
CORSのチェックリスト
- コンソールのエラー全文と、失敗したリクエストのステータスコードを読んだ
- 隠れた404や500ではないことを確認した
- 秘密のキーを使うAPIは、ブラウザではなくバックエンドから呼んでいる
- サーバーが、許可する正確なオリジンを指定したAccess-Control-Allow-Originを送っている
- OPTIONSのプリフライトに、認証の前に許可されたメソッドとヘッダーで応答している
- エラーレスポンスにもCORSヘッダーが含まれている
- 認証情報:正確なオリジン、Allow-Credentials: true、正しいCookieの設定
- プレビュー、公開、カスタムドメインのオリジンをすべてカバーしている、または相対の/apiパスを使っている
- no-cors、公開プロキシ、どのオリジンでも反映するといった近道を使っていない
よくある質問
CORSエラーとはどういう意味ですか?
ひとつのオリジンから読み込まれたページが、別のオリジンのレスポンスを読もうとしたのに、そのサーバーのレスポンスに読み取りを許可するヘッダーが含まれていなかった、という意味です。ブラウザは、あなたのコードがそのレスポンスを読むのをブロックします。直し方はフロントエンドのコードではなく、サーバー側、またはリクエストの経路にあります。
フロントエンドからCORSエラーを直せますか?
適切には直せません。mode: 'no-cors'のような設定は、レスポンスをあなたのコードから隠すだけです。サーバーに正しいAccess-Control-Allow-Originヘッダーを送らせるか、自分のバックエンドや同一オリジンのプロキシからAPIを呼び出すことで直します。
Access-Control-Allow-Origin: * は安全ですか?
誰が読んでもよい公開データ、たとえばログイン不要の公開APIであれば問題ありません。Cookieやその他の認証情報とは一緒に使えません。非公開のAPIでは、許可する正確なオリジンを列挙すべきです。
Postmanやcurlではリクエストが成功するのに、ブラウザでは失敗するのはなぜですか?
CORSはブラウザだけが強制する仕組みです。PostmanやcurlはCORSを適用しないため、そこでは成功するリクエストも、ページのJavaScriptが送るとブロックされることがあります。
プリフライトリクエストとは何ですか?
単純ではないリクエスト、たとえばJSONのPOSTや、Authorizationヘッダーを付けたリクエストでは、ブラウザが最初にOPTIONSリクエストを送り、どのメソッドやヘッダーが許可されているかをサーバーに尋ねます。そのOPTIONSリクエストが失敗するか、正しいヘッダーがないと、本来のリクエストは送信されません。