API Gateway CORSエラーの完全解決ガイド:コンソール設定とLambdaレスポンスヘッダーの両方が必要な理由

フロントエンドからAPI Gatewayを呼び出した瞬間に Access-Control-Allow-Origin エラーが出る。コンソールで「CORS を有効化」をクリックしたのに、まだ失敗する。この状況は、API GatewayのCORS設定が「2箇所に分散している」という構造を理解していないと、何度設定し直しても同じ結果になる。

TL;DR:API Gateway CORSエラーの全体像

レイヤー担当する処理設定箇所
プリフライトリクエスト (OPTIONS)ブラウザが事前確認するリクエストへの応答API Gatewayコンソール「CORS を有効化」
実際のAPIレスポンス (GET/POST等)Lambda関数が返すレスポンスヘッダーLambda関数のコード内
認証付きリクエストCookieや認証ヘッダーを含む場合の追加設定両方に credentials 関連の設定が必要

CORSがAPI Gatewayでどう機能するか

ブラウザはクロスオリジンのAPIリクエストを送る前に、プリフライトリクエストと呼ばれるOPTIONSメソッドのHTTPリクエストを自動的に送信する。このプリフライトに対してAPI Gatewayが適切なCORSヘッダーを返さないと、ブラウザは実際のリクエストをブロックする。

問題の核心は、API Gatewayには2種類のエンドポイントタイプがあり、それぞれCORSの設定方法が異なる点にある。

  • REST API (v1):OPTIONSメソッドをAPI Gatewayレベルで処理し、Lambdaレスポンスにも別途ヘッダーが必要
  • HTTP API (v2):API Gatewayが組み込みのCORSサポートを持ち、設定がシンプル
sequenceDiagram participant B as ブラウザ participant AG as API Gateway participant L as Lambda B->>AG: OPTIONSリクエスト
(プリフライト) AG-->>B: 200 OK
Access-Control-Allow-Origin ヘッダー付き Note over B,AG: 「CORS を有効化」で設定される部分 B->>AG: GETリクエスト
(実際のAPIコール) AG->>L: Lambda呼び出し L-->>AG: レスポンス
(ヘッダーなしだとCORSエラー) AG-->>B: レスポンス転送 Note over L,B: Lambdaのコードにヘッダーが必要な部分
  1. ブラウザ → OPTIONSリクエスト送信:クロスオリジンリクエスト前の事前確認。OriginAccess-Control-Request-MethodAccess-Control-Request-Headers を含む。
  2. API Gateway → OPTIONSレスポンス:「CORS を有効化」設定により、Access-Control-Allow-Origin などのヘッダーを返す。
  3. ブラウザ → 実際のリクエスト送信:プリフライトが成功した場合のみ実行される。
  4. Lambda → レスポンス返却:このレスポンスにも Access-Control-Allow-Origin が含まれていないとブラウザがブロックする。ここを見落とすケースが最も多い。

REST API (v1) でのCORS設定手順

ステップ1:コンソールでOPTIONSメソッドを設定する

API Gatewayコンソールを開き、対象のリソース(例:/items)を選択する。「アクション」メニューから「CORS の有効化」をクリックすると、ダイアログが表示される。ここで設定した内容は、OPTIONSメソッドのモックレスポンスに反映される。つまり、プリフライトリクエストへの応答だけを設定している。

設定後、必ず「APIのデプロイ」を実行すること。コンソールで設定を変更しても、デプロイしなければ本番環境には反映されない。これを忘れて「設定したのに直らない」と30分悩むのはよくあるパターンだ。

# デプロイ状態を確認するCLIコマンド
aws apigateway get-deployments \
  --rest-api-id YOUR_API_ID \
  --region us-east-1
# 特定のステージの設定を確認
aws apigateway get-stage \
  --rest-api-id YOUR_API_ID \
  --stage-name prod \
  --region us-east-1

ステップ2:Lambdaレスポンスに必須ヘッダーを追加する

コンソールでCORSを有効化しても、Lambdaが返すレスポンスに Access-Control-Allow-Origin が含まれていなければ、実際のAPIコール(GET/POST等)はブラウザにブロックされる。プリフライトは通過するが、実際のレスポンスでCORSチェックが再度行われるためだ。

🔽 Python (Lambda) のレスポンス例 [クリックで展開]
import json

def lambda_handler(event, context):
    # ビジネスロジック
    data = {"message": "success"}

    return {
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "https://your-frontend-domain.com",
            "Access-Control-Allow-Headers": "Content-Type,Authorization",
            "Access-Control-Allow-Methods": "GET,POST,OPTIONS"
        },
        "body": json.dumps(data)
    }
🔽 Node.js (Lambda) のレスポンス例 [クリックで展開]
exports.handler = async (event) => {
    const data = { message: 'success' };

    return {
        statusCode: 200,
        headers: {
            'Access-Control-Allow-Origin': 'https://your-frontend-domain.com',
            'Access-Control-Allow-Headers': 'Content-Type,Authorization',
            'Access-Control-Allow-Methods': 'GET,POST,OPTIONS'
        },
        body: JSON.stringify(data)
    };
};
ブラウザのCORSチェックは、プリフライト(OPTIONS)と実際のリクエストの両方で独立して行われる。片方だけ設定するのは、玄関の鍵を開けたまま部屋のドアに鍵をかけるようなものだ。

ステップ3:CLIでOPTIONSメソッドのレスポンスヘッダーを確認する

コンソールの設定が正しく反映されているか、CLIで直接確認できる。特に複数人で開発している環境では、誰かが設定を上書きしていないか確認する習慣をつけると良い。

# OPTIONSメソッドのインテグレーションレスポンスを確認
aws apigateway get-integration-response \
  --rest-api-id YOUR_API_ID \
  --resource-id YOUR_RESOURCE_ID \
  --http-method OPTIONS \
  --status-code 200 \
  --region us-east-1
# リソースIDを取得する
aws apigateway get-resources \
  --rest-api-id YOUR_API_ID \
  --region us-east-1

HTTP API (v2) でのCORS設定:よりシンプルなアプローチ

HTTP API (v2) では、API Gatewayレベルで一元的にCORSを設定できる。Lambdaのレスポンスにヘッダーを追加する必要はなく、API Gatewayが自動的にCORSヘッダーをレスポンスに付与する。

# HTTP API のCORS設定を確認
aws apigatewayv2 get-api \
  --api-id YOUR_API_ID \
  --region us-east-1
# HTTP API にCORSを設定するCLIコマンド
aws apigatewayv2 update-api \
  --api-id YOUR_API_ID \
  --cors-configuration AllowOrigins="https://your-frontend-domain.com",AllowMethods="GET,POST,OPTIONS",AllowHeaders="Content-Type,Authorization" \
  --region us-east-1

ただし、HTTP API (v2) でCORSを設定した場合でも、Lambdaのレスポンスに Access-Control-Allow-Origin を含めると、ヘッダーが重複してブラウザエラーになることがある。HTTP API を使う場合はLambda側のCORSヘッダーを削除すること。

実際の障害パターン:「設定したのに直らない」の正体

graph TD A["CORSエラー発生"] --> B{"OPTIONSは
成功しているか?"} B -->|No| C["API Gatewayの
CORS設定を確認"] C --> D["コンソールで
CORS を有効化"] D --> E["APIをデプロイ"] B -->|Yes| F{"GETレスポンスに
ヘッダーがあるか?"} F -->|No| G["Lambdaのコードに
CORSヘッダーを追加"] F -->|Yes| H{"withCredentials
を使用しているか?"} H -->|Yes| I["Access-Control-Allow-Origin
に具体的なオリジンを指定"] H -->|No| J["ブラウザキャッシュを
クリアして再確認"] G --> K["解決"] I --> K E --> K

本番で最も多く遭遇するのは次のパターンだ。コンソールで「CORS を有効化」をクリック → デプロイ → プリフライトは通過 → しかし実際のGETリクエストでCORSエラー。ブラウザのDevToolsを見ると、OPTIONSは200を返しているが、GETのレスポンスに Access-Control-Allow-Origin がない。

最初はAPI Gatewayの設定を疑って何度もコンソールをいじる。しかし問題はLambdaのコードにある。プリフライトはAPI Gatewayのモックレスポンスが処理するが、実際のメソッドはLambdaが処理するため、Lambdaのレスポンスにもヘッダーが必要になる。

エラーログには何も出ない。Lambdaは正常に実行されて200を返している。ブラウザのコンソールだけがCORSエラーを表示している。この状況でLambdaのログを見ても手がかりがないため、問題の切り分けに時間がかかる。

CORSエラーはサーバー側のエラーではなく、ブラウザがサーバーのレスポンスを拒否しているエラーだ。Lambdaのログが正常でも、ブラウザが期待するヘッダーがなければブロックされる。

診断:curlでプリフライトと実際のリクエストを個別に確認する

ブラウザを介さずにcurlで直接確認することで、どのレイヤーで問題が起きているかを特定できる。プリフライトと実際のリクエストを別々にテストするのがポイントだ。

# プリフライトリクエストをシミュレート
curl -v -X OPTIONS \
  https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/prod/items \
  -H 'Origin: https://your-frontend-domain.com' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: Content-Type'
# 実際のGETリクエストをシミュレート
curl -v -X GET \
  https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/prod/items \
  -H 'Origin: https://your-frontend-domain.com' \
  -H 'Content-Type: application/json'

OPTIONSのレスポンスに Access-Control-Allow-Origin があり、GETのレスポンスにない場合、問題はLambdaのレスポンスヘッダーにある。両方ともヘッダーがない場合、API Gatewayのデプロイが完了していないか、設定が正しくない。

認証付きリクエスト(withCredentials)の追加設定

フロントエンドで credentials: 'include'withCredentials: true を使う場合、追加の設定が必要になる。

  • Access-Control-Allow-Origin*(ワイルドカード)は使用できない。具体的なオリジンを指定する必要がある。
  • Lambdaのレスポンスに Access-Control-Allow-Credentials: true を追加する必要がある。
  • API Gatewayコンソールの「CORS を有効化」ダイアログでも、対応する設定を行う必要がある。
🔽 認証付きリクエスト対応のLambdaレスポンス例 [クリックで展開]
import json

def lambda_handler(event, context):
    data = {"message": "success"}

    return {
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "https://your-frontend-domain.com",
            "Access-Control-Allow-Headers": "Content-Type,Authorization",
            "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
            "Access-Control-Allow-Credentials": "true"
        },
        "body": json.dumps(data)
    }

IAMポリシー:Lambda実行ロールの確認

CORSエラーとは直接関係ないが、Lambda関数がAPI Gatewayから呼び出せない場合、CORSエラーと似た症状(502 Bad Gateway)が出ることがある。API GatewayがLambdaを呼び出すためのリソースベースポリシーが正しく設定されているか確認する。

# Lambda関数のリソースベースポリシーを確認
aws lambda get-policy \
  --function-name YOUR_FUNCTION_NAME \
  --region us-east-1

まとめとネクストステップ:API Gateway CORSエラーの解決チェックリスト

API Gateway CORSエラーの解決は、設定箇所が2つある(またはHTTP APIでは1つ)という構造を理解することから始まる。以下のチェックリストで順番に確認すること。

  1. REST API (v1) か HTTP API (v2) かを確認する
  2. REST API の場合:コンソールで「CORS を有効化」→ 必ずデプロイする
  3. REST API の場合:Lambdaのレスポンスに Access-Control-Allow-Origin を追加する
  4. HTTP API の場合:API GatewayレベルでCORSを設定し、Lambda側のCORSヘッダーは削除する
  5. curlでプリフライトと実際のリクエストを個別にテストして、どのレイヤーで問題が起きているかを特定する
  6. 認証付きリクエストの場合は、ワイルドカードを使わず具体的なオリジンを指定する

詳細な設定オプションについては、AWS公式ドキュメント「Enabling CORS for a REST API resource」および「Configuring CORS for an HTTP API」を参照すること。

用語集

用語説明
CORS (Cross-Origin Resource Sharing)異なるオリジン間でのHTTPリクエストを制御するブラウザのセキュリティ機能
プリフライトリクエスト実際のリクエスト前にブラウザが自動送信するOPTIONSメソッドのHTTPリクエスト
REST API (v1)API Gatewayの旧世代API。OPTIONSメソッドとLambdaレスポンスの両方にCORS設定が必要
HTTP API (v2)API Gatewayの新世代API。API Gatewayレベルで一元的にCORSを設定できる
Access-Control-Allow-Originどのオリジンからのリクエストを許可するかを指定するHTTPレスポンスヘッダー

コメント

このブログの人気の投稿

EC2 SSH接続タイムアウトの原因と修正方法 — セキュリティグループのインバウンドルール完全ガイド

S3パブリックアクセス拒否の原因と解決策:バケットレベルの「Block Public Access」が優先される仕組み

EC2インスタンスIDをメタデータから取得する方法 — IMDSv2が安全な理由