Claude CodeでCORSエラーの原因を切り分ける — preflightと資格情報
CORSエラーはJavaScriptから理由が見えません。ブラウザーの通信ヘッダーとcurl -X OPTIONSの結果をClaude Codeに渡し、欠落・ワイルドカード・プロキシを順に切り分けます。
CORSエラーは、原因が画面にもJavaScriptにも出ないエラーです。コードから分かるのは「失敗した」ことだけで、何が悪かったかはブラウザーの開発者ツールにしか載りません。
その開発者ツールの出力と、curl -X OPTIONSの結果をClaude Codeに渡すと、サーバー側のどのヘッダーが足りないかを短い手順で絞れます。この記事は、渡す材料の作り方、読み解く順番、プロキシ越しで見落としやすい点をまとめます。
CORSエラーでJavaScriptから見えないもの
CORSは、ブラウザーがオリジン間のリクエストを通してよいかをサーバーのヘッダーで判断する仕組みです。失敗の理由は複数ありますが、セキュリティ上の理由から、JavaScriptにはエラーが起きたという事実しか渡りません。具体的な理由を知る方法は、ブラウザーのコンソールを見ることだけです。
このため、catchしたエラーの文面をClaude Codeに貼っても手がかりは増えません。必要なのは、ブラウザーが実際に送った通信の中身です。
切り分けの対象は大きく3つに分かれます。
CORSエラーの原因になる3か所
preflightへの応答
OPTIONSへの応答に許可ヘッダーが無い、またはOPTIONS自体がエラーで返っている状態です。資格情報付きの組み合わせ
Cookieや認証情報を付けたリクエストに、ワイルドカード指定の応答を返している状態です。
途中経路でのヘッダー消失
プロキシやリダイレクトを挟み、サーバーが付けたヘッダーがブラウザーに届いていない状態です。
Claude Codeに渡す材料を2つ作る
1つ目は、ブラウザーの開発者ツール(Networkタブ)で失敗したリクエストを探し、OPTIONSと本体のリクエストの両方について、リクエストヘッダーとレスポンスヘッダーを貼れる形で控えたものです。Origin、Access-Control-Request-Method、Access-Control-Request-Headersが入っていることを確かめます。
2つ目は、同じ条件をブラウザーなしで再現するcurlの出力です。ブラウザーが送ったヘッダーをそのまま真似ます。
curl -s -i -X OPTIONS https://api.example.com/items \
-H 'Origin: https://app.example' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,authorization'-iでレスポンスヘッダーまで出し、OriginとAccess-Control-Request-*をブラウザーと同じ値にするのがポイントです。この出力をファイルに保存しておけば、パイプで渡せます。標準入力から読む方法は非対話モードの公式例にあります。
cat preflight.txt | claude -p \
'このOPTIONS応答でCORSが通らない理由を、足りないヘッダー名で答えて'標準入力の上限は10MBです。通信ログを丸ごと渡すときは、ファイルに書き出してパスをプロンプトに書くほうが安全です。
Claudeにcurlを実行させるとき
Claude Codeにcurlを実行させる場合、Bashのサンドボックスが有効だと、コマンドの通信はサンドボックスのプロキシを通ります。許可していないドメインへの初回アクセスでは承認を求められます。
ローカルの開発サーバーに対するcurlは別の事情があります。Linux・WSL2では、サンドボックス内のコマンドから見たlocalhostはそのコマンド専用で、ホスト側のサーバーには届きません。macOSではnetwork.allowLocalBindingの設定が関わります。ローカルサーバーへのcurlだけが失敗するときは、CORSではなくこの制限を先に疑ってください。詳しい回避策はsandboxから繋がらないときの原因にまとめています。
手元の検証サーバーで見た3種類の応答
3パターンの応答を返すだけの小さなNode.jsサーバーを立て、上と同じcurl -X OPTIONSを打って比べました。サーバー側のコードは単純で、パスごとにヘッダーを出し分けています。
| パス | ステータス | 返ったCORS関連ヘッダー |
|---|---|---|
/none | ステータス404 | 返ったCORS関連ヘッダーなし |
/wild | ステータス204 | 返ったCORS関連ヘッダーAllow-Origin: *、Allow-Credentials: true、Allow-Headers: * |
/ok | ステータス204 | 返ったCORS関連ヘッダーAllow-Origin: https://app.example、Vary: Origin、明示したAllow-Headers |
/noneは、OPTIONSを処理するルートが無く404で返った例です。Access-Control-*が1つも付かないので、ブラウザーは本体のリクエストを送らずに止めます。原因はヘッダーの書き方ではなく、OPTIONSを受けるルートが無いことです。
/wildは、見た目には許可が揃っています。ところが資格情報付きのリクエストでは、後述のとおり*が使えません。curlだけでは成功して見えるため、Claudeには「このリクエストは資格情報付きか」を必ず伝えてください。
/okは、リクエストのOriginをそのまま返し、Vary: Originを付け、許可ヘッダーを名前で列挙した例です。資格情報付きでも通る形です。
切り分けの順番
Claudeに調べさせるときは、順番を固定すると見落としが減ります。下の流れは、curlの出力と開発者ツールの内容があれば、上から機械的にたどれます。
preflightが通らないときの切り分け
- QOPTIONSへの応答が2xxで返っているか?答えがいいえのとき答えがはい
ルーティングや認証ミドルウェアが
OPTIONSを弾いています。CORSヘッダーの前に、OPTIONSを受けるルートを足します。 - QAccess-Control-Allow-Originがリクエストのoriginと一致しているか?答えがいいえのとき答えがはい
サーバーの許可オリジン設定に、リクエストのoriginが入っていません。直接アプリに打った応答では一致するのに公開URLで外れるなら、プロキシが値を書き換えています。
- QAllow-MethodsとAllow-Headersに実際のメソッドとヘッダーが含まれているか?答えがいいえのとき答えがはい
Access-Control-Request-Headersに並んだ名前が、応答のAllow-Headersに1つ足りないだけで失敗します。 - Q資格情報付きのリクエストなら、Allow-Credentials: trueと明示オリジンの組み合わせか?答えがいいえのとき答えがはい
*を明示オリジンに置き換えます。
ここまで揃えば
サーバー設定は通る形です。残るのはプロキシやリダイレクトによるヘッダー消失です。
preflightの応答では、Access-Control-Allow-Originのほかに、実際のリクエストで使うメソッドとヘッダーを許可するAccess-Control-Allow-MethodsとAccess-Control-Allow-Headersが必要です。ブラウザーのAccess-Control-Request-Headersに対するサーバー側の返答がAllow-Headersにあたります。
資格情報付きでワイルドカードが通らない理由
fetch()でcredentials: "include"を指定する、またはXMLHttpRequestでwithCredentialsをtrueにすると、Cookieや認証情報を付けたリクエストになります。この場合、応答のAccess-Control-Allow-Originに*は使えません。ブラウザーはレスポンスへのアクセスをブロックし、コンソールにCORSエラーを出します。
制約はAllow-Originだけではありません。資格情報付きのリクエストに答えるときは、次の4つのヘッダーでワイルドカードを使わず、具体的な名前を並べる必要があります。
Access-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Expose-Headers
加えて、Access-Control-Allow-Credentials: trueも要ります。応答にSet-Cookieがあっても、Allow-Originが*だとCookieは設定されません。
動的に許可オリジンを返す実装では、サーバーがOriginごとに値を変えるので、Vary: Originを付けておきます。付け忘れるとキャッシュ越しに別オリジンの応答が返る恐れがあります。
落とし穴
preflightにはCookieを付けられない
OPTIONSのpreflightに資格情報は含まれません。認証が必要なAPIで、認証ミドルウェアがOPTIONSにも401を返すと、本体のリクエストに進む前にCORSエラーになります。OPTIONSは認証の手前で受けて204を返す構成にします。
ブラウザー側にも注意点があります。別ドメインへのCookieは、CORSの設定とは別にサードパーティークッキーのポリシーに従います。サーバー側のヘッダーが完璧でも、ブラウザーの設定で送信や保存が止まることがあります。
プロキシとリダイレクトでヘッダーが消えるとき
アプリのコードは正しいのに、本番だけCORSが通らない場合は、途中経路を疑います。リバースプロキシやCDNがAccess-Control-*を落としたり、OPTIONSを上流に渡さずに自前で応答したりすると、サーバーに入れた設定がブラウザーに届きません。
見分ける方法は、同じcurlを2か所に打って応答を比べることです。
# アプリへ直接(プロキシを通らない経路)
curl -s -i -X OPTIONS http://127.0.0.1:8080/items \
-H 'Origin: https://app.example' \
-H 'Access-Control-Request-Method: POST'
# 公開URL(プロキシ・CDN経由)
curl -s -i -X OPTIONS https://api.example.com/items \
-H 'Origin: https://app.example' \
-H 'Access-Control-Request-Method: POST'2つの出力を並べてClaudeに渡し、「ヘッダーが増減している箇所はどこか」と尋ねます。直接のほうにはAccess-Control-Allow-Originがあり、公開URLのほうに無ければ、原因はプロキシ側の設定です。リバースプロキシの設定自体はCaddyの設定例のように、検証コマンドを完了条件にして進めると確かめやすくなります。
リダイレクトも落とし穴です。preflightのリクエストがオリジンをまたぐリダイレクトに当たると、ブラウザーによっては次のようなエラーで止まります。
Request requires preflight, which is disallowed to follow cross-origin redirects.基本の回避策は、サーバー側でpreflightもリダイレクトも起きない構成にすること、またはリクエストをpreflightの要らない単純リクエストに変えることです(両方行っても構いません)。
どちらも難しいときは、2段の手順があります。まず単純リクエストを送り、Response.url(XMLHttpRequestならresponseURL)で最終的なリダイレクト先を調べます。次に、そのURLへ本来のリクエストを送り直します。ただし、Authorizationヘッダーがあるためにpreflightが必要なリクエストは、この2段の手順でも回避できません。リクエスト先のサーバーを自分で直せない限り、手立てがない状態です。
httpからhttpsへ、末尾スラッシュなしからありへのリダイレクトが原因なら、フロントのURLを最終形に直すのが近道です。
ここまでの確認で、curlの出力が通る形なのにブラウザーだけ失敗するなら、リクエスト側の条件が違っています。ブラウザーが実際に送ったOriginやAccess-Control-Request-Headersを、curlに正確にそろえ直してください。
Claudeへの指示例
調べさせる範囲を限定し、実行した結果で確かめさせるのがコツです。次は、公式のバグ調査の型(エラーを共有→修正案を聞く→適用する)に、CORS用の観点を足した例です。
CORSエラーの原因を切り分けて。材料は preflight.txt(OPTIONS応答)と
network.txt(ブラウザーのNetworkタブのヘッダー)。
次の順で答えて。
1. OPTIONSのステータス
2. Allow-Originとリクエストのoriginの一致
3. Allow-Headersに足りない名前
4. 資格情報付きなのにワイルドカードを返していないか
修正案を出したら、curl -X OPTIONSを再実行して応答ヘッダーで確かめて。公式の手順でも、再現するコマンドを教え、エラーが毎回出るのか断続的なのかを伝えるよう勧めています。CORSでも同じで、「本番だけ」「特定のオリジンだけ」といった条件が絞り込みを早めます。
ブラウザーを動かせる環境なら、Claude in Chromeの拡張を使う方法もあります。公式の使い方では、Claudeがコンソールの出力を読んで原因を調べられます。ただし、ログは長くなりやすいので、探してほしいパターンを指定します。CORSなら「Access-Control」を含む行に絞るよう頼みます。
まとめ
CORSの原因探しは、サーバーが返すべきヘッダーと実際の応答の差分を見る作業です。ブラウザーの通信とcurl -X OPTIONSの出力が揃えば、Claude Codeは差分を挙げるところまで速く進められます。
curlは資格情報を付けなければワイルドカードでも通ってしまうので、成功を「直った」の根拠にしないのが落とし穴です。Claudeには資格情報の有無を添え、アプリ直通とプロキシ経由の応答も並べて渡します。この2点が抜けると、curlでは成功して見えるのにブラウザーでは失敗する、という状況で堂々巡りになります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。