「CORSエラーさえ出なければ10分で終わるはずの修正が、2時間溶けた」——個人開発を10年以上続けてきた中で、この経験は何度あったかわからないくらいです。
アプリ開発でCORSエラーが出ると厄介なのは、エラーメッセージが「何が原因か」ではなく「何が拒否されたか」しか教えてくれない点にあります。Antigravityに「CORSを直してください」と伝えても、原因のある場所を正確に把握していなければ、見当外れなコードを生成してしまいます。実際に私の場合も、Next.jsのAPI Routeを通さずにフロントエンドからFirebaseのRESTエンドポイントを直接叩いていた部分があって、Antigravityがcorsパッケージを追加するコードを生成してくれたのに全く効かない、ということがありました。
ここではAntigravityを使ったアプリ開発でCORSエラーが起きる場所ごとの原因と、環境別の正しい対処を整理します。
CORSエラーが起きる「3つの場所」を特定する
まず最初にやることは、エラーが起きている場所を特定することです。CORSエラーは「フロントエンド → サーバー」の境界すべてで起きうるため、どの境界で詰まっているかを絞り込まないと対処が的外れになります。
ブラウザのDevToolsを開き、Networkタブでエラーになっているリクエストを確認してください。リクエストの宛先URLを見ることで、以下の3パターンのどれかに当てはまります。
パターン1: フロントエンドから外部APIを直接呼んでいる
Access to fetch at 'https://api.stripe.com/...' from origin 'http://localhost:3000' has been blocked by CORS policy というエラーです。Stripe・Twilio・SendGrid・Google Maps APIなど、プロバイダーが設定したCORSポリシーにフロントから直接当たっています。
パターン2: 同一アプリ内のAPIエンドポイントが別ポートで動いている
Access to fetch at 'http://localhost:8080/api/...' from origin 'http://localhost:3000' has been blocked by CORS policy というエラーです。フロントと同一ドメインのつもりが、ポートが異なるためにブラウザがクロスオリジンと判断しています。
パターン3: サーバーレス環境のレスポンスにCORSヘッダーがない
No 'Access-Control-Allow-Origin' header is present on the requested resource というエラーです。Cloudflare Workers・Firebase Functions・Supabase Edge Functionsのレスポンスにヘッダーが設定されていないケースです。
パターン1の解決: 外部APIはサーバー経由で呼ぶ
外部APIをフロントエンドから直接呼ぶことは、CORS問題だけでなくAPIキーの漏洩リスクも伴います。原則として、外部APIの呼び出しはサーバーサイドを経由させるべきです。
Next.js App RouterのRoute Handlerを使った中継の例です:
// app/api/send-email/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.json();
// サーバーサイドからSendGrid APIを呼ぶ(CORSの問題なし)
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: [{ email: body.to }],
from: { email: "noreply@example.com" },
subject: body.subject,
content: [{ type: "text/plain", value: body.message }],
}),
});
if (!response.ok) {
return NextResponse.json({ error: "送信に失敗しました" }, { status: 500 });
}
return NextResponse.json({ success: true });
}フロントエンドからは /api/send-email というローカルのエンドポイントを呼ぶだけになり、CORSは発生しません。
Antigravityにこのパターンで修正を依頼するときは以下のように伝えると精度が上がります:
現在、フロントエンドから直接 SendGrid API を呼んでいます。
これをNext.js Route Handler を経由する形にリファクタリングしてください。
APIキーは環境変数 SENDGRID_API_KEY から読み込みます。
エンドポイントは /api/send-email で、フロントからは fetch('/api/send-email', {...}) で呼び出します。
パターン2の解決: Next.jsのrewritesでプロキシ設定する
開発中にフロントとAPIサーバーが別ポートで動いているケースでは、Next.jsのrewrites設定が最もシンプルな解決策です。
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async rewrites() {
if (process.env.NODE_ENV === "development") {
return [
{
source: "/api/backend/:path*",
destination: "http://localhost:8080/api/:path*",
},
];
}
return [];
},
};
export default nextConfig;この設定により、フロントから /api/backend/users を呼ぶとNext.jsが http://localhost:8080/api/users にプロキシしてくれます。ブラウザから見ると同一オリジンへのリクエストになるため、CORSは発生しません。
開発環境のみに限定しているのは意図的です。本番環境でこの設定が意図せず動くと、外部からのリクエストがバックエンドに通り抜けてしまう可能性があります。
パターン3の解決: サーバーサイドにCORSヘッダーを追加する
Cloudflare WorkersやFirebase Functionsなどのサーバーレス環境では、レスポンスに明示的にCORSヘッダーを設定する必要があります。
Cloudflare Workersの場合:
// worker.ts
export default {
async fetch(request: Request): Promise<Response> {
const origin = request.headers.get("Origin") ?? "";
// プリフライトリクエスト(OPTIONSメソッド)への対応
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
},
});
}
// 通常のリクエスト処理
const data = { result: "success" };
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": origin,
},
});
},
};開発中は origin をそのまま返す形でもよいですが、本番では許可するオリジンを限定することをおすすめします:
const ALLOWED_ORIGINS = [
"https://myapp.com",
"https://www.myapp.com",
];
const allowedOrigin = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];Supabase Edge Functionsの場合:
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};// supabase/functions/my-function/index.ts
import { corsHeaders } from "../_shared/cors.ts";
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const data = { result: "success" };
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
});Antigravityに依頼する際は、どのサーバーレス環境を使っているかを明示してください:
Cloudflare Workers の worker.ts に CORS 設定を追加してください。
許可するオリジンは https://myapp.com のみです。
GET と POST メソッドを許可し、プリフライトリクエスト(OPTIONSメソッド)にも対応してください。
よくある見落とし: OPTIONSメソッドへの対応
CORSヘッダーを設定したのに解決しないときは、プリフライトリクエスト(OPTIONSメソッド)の対応漏れを疑ってください。
ブラウザは Content-Type: application/json や Authorization ヘッダーを含むリクエストを送る前に、OPTIONSメソッドで事前確認を行います。サーバーがOPTIONSに対して適切に応答していないと、Access-Control-Allow-Origin を設定していても動きません。
DevToolsのNetworkタブで、エラーになったリクエストの直前にOPTIONSメソッドのリクエストがあるかどうかを確認してみてください。OPTIONSリクエストが ERR_CONNECTION_REFUSED や 400 Bad Request になっていれば、そこが詰まっている場所です。
Antigravityに修正を依頼するときは「プリフライトリクエストにも対応してください」と明示することで、OPTIONSメソッドの処理も含めたコードが生成されます。
本番環境で再発させないために
開発中にCORSを解消できても、本番デプロイ後に再発するケースがあります。よくあるのは以下のパターンです:
開発中は localhost からアクセスしていたが、本番では別ドメインから呼ぶようになった、というのが最も多いです。許可オリジンを環境変数で管理しておくと、ステージング・本番で柔軟に切り替えられます:
// 環境変数で許可オリジンを管理
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS ?? "http://localhost:3000")
.split(",")
.map((o) => o.trim());もう一つは、CloudflareのCache設定がCORSヘッダーを含むレスポンスをキャッシュしてしまうケースです。異なるオリジンから来たリクエストに、別オリジン向けのキャッシュが返されると、CORSエラーになります。CORSヘッダーを含むレスポンスはキャッシュしない設定を入れておくとトラブルを防げます。
個人でアプリ開発を続けてきた経験から言うと、CORSトラブルは一度パターンを理解してしまえば怖くありません。「どこの境界で何が起きているか」を特定することさえできれば、解決策は限られています。Antigravityへの指示もそのパターンに沿って伝えることで、的確なコードが返ってくるようになります。
同じような詰まりを抱えている方の参考になれば幸いです。