新しいフロントエンドをリリースし、コンソールを開くと、そこに赤色のCORSエラーが表示され、リクエストが「CORSポリシーによってブロックされた」と告げられます。ApidogやcurlではAPIが正常に動作するのに、ブラウザはJavaScriptに応答を渡してくれません。イライラしますか?はい。謎めいていますか?エラーの原因がどこにあるかを知れば、そうではありません。
ほとんどのチュートリアルが隠している核心は次のとおりです。CORSエラーはブラウザによって強制されますが、原因はサーバーにあります。ブラウザが応答をブロックするのは、サーバーが正しいAccess-Control-Allow-Originヘッダーを送信しなかったためです。したがって、修正はほとんどの場合、フロントエンドコードではなく、サーバーの設定で行われます。
このガイドでは、CORSが何をするのか、プリフライトリクエストがどのように機能するのか、最も一般的な6つのCORSエラーメッセージとその正確な修正方法、そしてExpress、Spring Boot、Nginxの動作設定について説明します。また、ブラウザ外からデバッグする方法も紹介します。これは、「サーバーの設定ミス」と「ブラウザによるブロック」を区別する最速の方法です。
CORSエラーとは何か(そしてそうでないもの)
CORSはCross-Origin Resource Sharing(クロスオリジンリソース共有)の略です。デフォルトでは、ブラウザは同一オリジンポリシーを強制します。これは、https://app.example.comで実行されているJavaScriptが、スキーム、ホスト、またはポートが異なるため、https://api.example.comからの応答を読み取ることができないことを意味します。CORSは、サーバーが意図的にこのルールを緩和するために使用するメカニズムです。詳細については、MDN CORSドキュメントに記載されており、基盤となるアルゴリズムはFetch仕様で定義されています。
ほとんどの混乱を解消する3つのポイント:
- ブラウザが強制する。 CORSチェックを適用するのはブラウザだけです。サーバー間の呼び出し、curl、デスクトップAPIクライアントはこれを完全に無視します。
- サーバーが設定する。 ブラウザは、サーバーが送信する応答ヘッダーに基づいて判断します。ヘッダーがなければ、アクセスはできません。
- リクエストは通常、それでもサーバーに到達する。 シンプルなリクエストの場合、サーバーはすべてを処理して応答します。その後、ブラウザはその応答をJavaScriptから隠します。CORSはAPIを取り囲むセキュリティウォールではありません。ユーザーを、クッキーを使ってクロスオリジンデータを読み取る悪意のあるページから保護するものです。
したがって、CORSエラーが表示されたら、フロントエンドの回避策を探すのではなく、エラーメッセージを読み、サーバー上の欠落している、または間違ったヘッダーを修正してください。
プリフライトリクエストの構造
特定のクロスオリジンリクエストの前に、ブラウザは偵察役としてプリフライトと呼ばれるOPTIONSリクエストを送信します。これは、リクエストがGET、HEAD、またはPOST以外のメソッドを使用する場合、Authorizationのようなカスタムヘッダーを送信する場合、またはapplication/jsonのようなContent-Typeを使用する場合に発生します。
プリフライトは次のようになります:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
ブラウザは次のように尋ねています:「app.example.com上のページが、これらのヘッダーを使用してここにPOSTしようとしています。許可されますか?」正しいサーバーの応答は次のとおりです。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
いずれかの情報が欠けている場合、ブラウザは実際の(本命の)リクエストが発火する前にキャンセルします。APIエンドポイントは実行されず、ログにはOPTIONSのヒットのみが表示され、コンソールにはCORSエラーが表示されます。Access-Control-Max-Ageはブラウザにこの判断をキャッシュするよう指示し(ここでは86400秒)、繰り返しのリクエストではプリフライトがスキップされます。
この二段階のやり取りを覚えておいてください。CORSデバッグの半分は、「プリフライトが失敗したのか、それとも実際のリクエストが失敗したのか」という一つの疑問に集約されます。
最も一般的な6つのCORSエラーとその修正方法
ブラウザは驚くほど正確なCORSエラーメッセージを生成します。あなたのエラーを以下のリストと照合してください。
1. 「Access-Control-Allow-Origin」ヘッダーが存在しません
定番のエラーです。サーバーがCORSヘッダーを全く含まない応答を送信しました。ブラウザには評価するものがなかったため、アクセスをブロックしました。
修正方法: サーバーがAccess-Control-Allow-Originを、特定の要求元オリジン、または公開の認証情報不要なAPIの場合は*と共に送信するように設定してください:
Access-Control-Allow-Origin: https://app.example.com
一つの落とし穴:エラー応答では、成功応答にCORSヘッダーが含まれている場合でも、CORSヘッダーがスキップされることがよくあります。APIが500を返し、ミドルウェアが200番台の応答のみを装飾する場合、コンソールには実際のサーバーエラーではなくCORSエラーが表示されます。403 Forbiddenや500ページを含むすべての応答にCORSヘッダーが付加されていることを確認してください。
2. ワイルドカード「*」は認証情報と一緒に使用できません
メッセージには、「リクエストの認証情報モードが『include』の場合、『Access-Control-Allow-Origin』ヘッダーの値はワイルドカード『*』であってはならない」と書かれています。
フロントエンドはcredentials: 'include'を使ってクッキーや認証ヘッダーを送信していますが、サーバーはAccess-Control-Allow-Origin: *で応答しています。Fetch仕様はこの組み合わせを禁止しています。ワイルドカードと認証情報を組み合わせると、インターネット上の任意のサイトが認証された応答を読み取ることができてしまうからです。
修正方法: ワイルドカードの代わりに正確なオリジンを返し、認証情報ヘッダーを追加してください:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
受信したOriginを許可リストと照合してから返すようにしてください。認証情報が有効な状態で任意のオリジンを反射させると、保護全体が無効になります。
3. プリフライトリクエストへの応答がアクセス制御チェックに合格しません
サーバーがOPTIONSリクエストを処理しませんでした。おそらく、ルートがPOSTのみを定義しているため、OPTIONSが404または405を返しているのかもしれません。あるいは、プリフライトにトークンが含まれていないため(ブラウザはプリフライトに認証情報を決して付加しません)、認証ミドルウェアが401で拒否したのかもしれません。
修正方法: OPTIONSリクエストを明示的に処理し、認証が実行される前に、CORSヘッダーの完全なセットを含む2xx応答を返してください。ほとんどのフレームワークでは、CORSミドルウェアを最初にマウントすることで解決します。手動で記述する場合は次のようになります:
app.options('/v1/orders', (req, res) => {
res.set({
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
});
res.sendStatus(204);
});
4. ヘッダー値が提供されたオリジンと一致しません
サーバーはAccess-Control-Allow-Originヘッダーを送信しますが、誤ったオリジンを指定しています。よくある原因としては、http://localhost:5173からテストしているのに本番環境のオリジンがハードコードされている場合、httpとhttpsの違いで許可リストの比較が失敗する場合、または不要な末尾のスラッシュ(https://app.example.com/は有効なオリジン値ではありません)などが挙げられます。
修正方法: リクエストのOriginヘッダーと許可リストを正確に比較し、一致するものを返し、Vary: Originを送信してください。これにより、キャッシュやCDNが、あるオリジンのヘッダーを別のオリジンに提供するのを防ぎます:
const allowed = ['https://app.example.com', 'http://localhost:5173'];
if (allowed.includes(req.headers.origin)) {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Vary', 'Origin');
}
5. リクエストヘッダーフィールドまたはメソッドが許可されていません
類似する2つのメッセージ:「プリフライト応答のAccess-Control-Allow-Headersで、リクエストヘッダーフィールドauthorizationは許可されていません」と「Method PUTはAccess-Control-Allow-Methodsで許可されていません」。
プリフライトは成功しましたが、その応答はリクエストが必要とするものをカバーしていませんでした。AuthorizationヘッダーまたはX-Request-Idを追加しましたが、サーバーの許可リストにはそれが記載されていませんでした。
修正方法: プリフライト応答を拡張し、フロントエンドが送信するすべてのヘッダーとメソッドを含めてください:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
ここでのヘッダー名は大文字と小文字を区別しません。メソッドは大文字と小文字を区別し、大文字で記述します。
6. プリフライトリクエストの際にはリダイレクトは許可されていません
プリフライトが301または302を返すURLに到達し、ブラウザはプリフライト中にリダイレクトに従うことを拒否します。典型的な原因としては、http URLがhttpsにリダイレクトされる場合、フレームワークが「親切にも」リダイレクトする末尾のスラッシュの欠落、またはゲートウェイが/v1/ordersを/v1/orders/にバウンスさせる場合などがあります。
修正方法: フロントエンドを最終的なURLに直接向けてください。最初からhttpsを使用し、ルーターの末尾スラッシュの慣例に合わせ、手動のOPTIONS呼び出しで、エンドポイントが3xxではなく2xxで応答するかどうかを確認してください。
サーバー設定例
一般的な3つのスタックにおける正しいCORS設定を次に示します。
Express
ヘッダーを手動で設定する代わりに、公式のcorsミドルウェアを使用してください:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400
}));
認証ミドルウェアの前にこれをマウントしてください。そうすることで、プリフライトがトークン不足で拒否されることがなくなります。Python開発者は、Flaskアプリの同一のヘッダーロジックをラップするFlask-CORS拡張機能から同じパターンを得ることができます。
Spring Boot
WebMvcConfigurerによるグローバル設定:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/v1/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(86400);
}
}
Spring Securityを使用していますか?セキュリティフィルターチェーンでも.cors(Customizer.withDefaults())を呼び出してください。そうしないと、MVC設定がそれらを見る前にセキュリティ層がプリフライトをブロックします。完全なオプションセットについては、Spring CORSドキュメントを参照してください。
Nginx
Nginxがアプリの前にリクエストを終端する場合、エッジでプリフライトに応答します:
location /v1/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}
alwaysフラグが重要です。これがないと、Nginxは4xxおよび5xx応答でadd_headerディレクティブを破棄し、失敗したリクエストごとに最初のエラーを再発生させます。そして、CORSを担当するレイヤーを1つ選択してください。Nginxとアプリケーションの両方がヘッダーを追加すると、ブラウザはAccess-Control-Allow-Origin: *, *のような重複を見て応答を拒否します。
Apidogを使ってブラウザ外でCORSをデバッグする
コンソールエラーは、ブラウザが何かをブロックしたことを示しています。しかし、サーバーが何を送信したかは教えてくれません。真実を見る最速の方法は、ブラウザをループから外すことです。
ApidogはデスクトップAPIクライアントなので、そのリクエストはブラウザのCORSチェックの対象になりません。これにより、クリーンな実験ができます。フロントエンドが行っていたのと同じリクエストをApidogから送信します。もしそれが成功すれば、APIロジックは問題なく、問題は純粋にCORSヘッダーの欠落にあります。もしそこでも失敗するなら、それはCORSのふりをした通常のAPIバグであり、一般的なAPIテスト手法が適用されます。
ApidogでのCORSデバッグセッションは次のようになります:
- 実際のリクエストを再生します。 ブラウザのネットワークタブから失敗したリクエストをコピーし、同じメソッド、ヘッダー、ボディでApidogで再作成します。ステータスとボディを確認してください。ここで500が発生した場合、CORSはそもそも問題ではありませんでした。
- プリフライトを手動でテストします。 新しいリクエストを作成し、メソッドを
OPTIONSに設定し、ブラウザが送信するであろうヘッダーを追加します:Origin: https://app.example.com、Access-Control-Request-Method: POST、およびAccess-Control-Request-Headers: authorization, content-type。これを送信します。 - 応答ヘッダーを検査します。 応答ペインで、
Access-Control-Allow-Origin、Access-Control-Allow-Methods、およびAccess-Control-Allow-Headersを探します。それぞれの値をフロントエンドが必要とするものと比較してください。ヘッダーの欠落、誤ったオリジン、または3xxステータスがあれば、コンソールでの推測なしにすぐに判明します。 - 修正を検証します。 サーバー設定を変更した後、保存した同じ
OPTIONSリクエストを再送信し、ヘッダーの更新を確認します。フロントエンドの再デプロイも、キャッシュクリアの儀式も不要です。
このワークフローは、「APIクライアントでは動作するが、ブラウザでは失敗する」という永遠の議論を数秒で解決します。これはPostman CORSテストの疑問の背後にあるのと同じパズルです。クライアントが動作するのはCORSをスキップするためです。ブラウザが失敗するのは、サーバーが魔法の言葉を唱えていないためです。Apidogを無料でダウンロードし、通常のエンドポイントテストの隣にOPTIONSリクエストを保存しておけば、将来のCORS問題はワンクリックで解決できます。
30秒でわかるCORSチェックリスト
バグを報告する前に、このリストを確認してください:
- 失敗した応答に
Access-Control-Allow-Originが全く含まれていますか? - その値は、あなたのページのオリジン(スキーム、ホスト、ポート、末尾スラッシュなし)と正確に一致していますか?
- クッキーまたは認証を使用していますか?特定のオリジンと
Access-Control-Allow-Credentials: trueを確認し、決して*を使用しないでください。 OPTIONSが、あなたのリクエストをカバーするメソッドとヘッダーを含む2xxを返していますか?- プリフライトURLにリダイレクトはありますか?
- エラー応答(401, 403, 500)は、成功応答と同じCORSヘッダーを含んでいますか?
10回中9回は、これらの6つの項目の中に答えがあります。Apidogで手動のOPTIONSリクエストを使って確認し、サーバー設定を修正して、開発に戻ってください。
よくある質問
なぜブラウザでのみCORSエラーが発生するのですか?
なぜなら、CORSを強制するのはブラウザだけだからです。同一オリジンポリシーは、悪意のあるページが認証済みのデータを読み取ることからユーザーを保護するため、ブラウザはすべてのクロスオリジン応答でAccess-Control-Allow-Originをチェックします。curl、バックエンドサービス、デスクトップクライアントにはそのようなルールはありません。ブラウザ以外ではどこでもリクエストが成功する場合、サーバーがCORSヘッダーを欠落させているか、誤って設定しているかのどちらかです。API自体は健全です。
CORSはPostmanまたはApidogに適用されますか?
いいえ。PostmanやApidogはデスクトップアプリケーションであり、ブラウザのサンドボックス内で実行されるウェブページではないため、それらのリクエストはCORSを完全にバイパスします。これこそが、CORSデバッグに役立つ理由です。ブラウザのフィルタリングなしで、サーバーの生の応答ヘッダーを表示してくれるからです。Postman CORSテストの混乱は通常ここから始まります。デスクトップクライアントでの成功したリクエストは、ブラウザの動作について何も証明しませんが、失敗しているレイヤーを特定するのに役立ちます。
CORSエラーはセキュリティ機能ですか、それともバグですか?
機能です。CORSエラーは、ブラウザがその役割を果たしていることを意味します。つまり、サーバーが許可しない限り、クロスオリジン応答データをスクリプトに公開することを拒否しています。ブラウザでフラグや拡張機能を使ってCORSを無効にしても、あなたのマシン上では症状が隠れるだけで、他のすべてのユーザーは依然として問題に直面します。代わりにサーバーヘッダーを修正してください。
Access-Control-Allow-Origin: *をどこでも使用できますか?
クッキーや認証を伴わない、公開された読み取り専用APIの場合のみ可能です。認証情報が含まれている場合、ワイルドカードは拒否され、それはウェブ上のあらゆるオリジンにデータが公開されていることを意味します。認証を伴うものについては、オリジンの許可リストを保持し、一致するオリジンを返し、Vary: Originを送信して、共有キャッシュが応答を分離するようにしてください。
