◉ANTIGRAVITY LABEN
記事一覧/アプリ開発
▣ アプリ開発/2026-05-14中級

開発中にCORSエラーが出てAntigravityのAPIテストが止まる — 環境別の原因特定と解決法

Antigravityで開発中にCORSエラーが出てAPIテストが進まなくなる問題を解決します。Next.js・Cloudflare Workers・Supabaseなど環境別に原因を特定し、Antigravityへの修正依頼方法まで整理します。

corsapi13troubleshooting89next.js4cloudflare6app-dev52

「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への指示もそのパターンに沿って伝えることで、的確なコードが返ってくるようになります。

同じような詰まりを抱えている方の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

▣ アプリ開発2026-09-10
iOS 27 の配信日が決まってから、私が最初に grep したのは AGENTS.md でした
iOS 27 と iPadOS 27 の配信は9月14日です。版数の文字列はソースだけでなく、エージェントが読む設定ファイルにも残ります。両方を数えるスクリプトと、keyword 検索では拾えない一行の話を書き残します。
▣ アプリ開発2026-09-08
年齢レーティングの質問票に、記憶ではなくリポジトリの宣言で答える
2026年9月から必須になった App Store の年齢レーティング質問票を、リポジトリ側の宣言ファイルと標準ライブラリだけの照合スクリプトで毎回同じ答えにする手順を、実際の出力とともにお伝えします。
▣ アプリ開発2026-09-04
iOS 27 の RC から正式リリースまでの5日間 — 画面より先に、初期化の順番と権利の復元を確かめます
OS が入れ替わる直前の短い窓で、エージェントに渡す検証範囲を画面まわりではなく起動経路で切るまでの記録です。ATT と広告 SDK の順序、権利の復元、寸法のハードコード分岐を機械で洗い出す監査スクリプトを添えています。