概要
ブラウザの JavaScript から API Gateway の REST API を呼ぶと、curl では普通に返ってくるのに、ブラウザでは No 'Access-Control-Allow-Origin' header is present on the requested resource のようなエラーで失敗することがあります。ページと API のオリジン(スキーム・ホスト・ポートの組み合わせ)が違うので、ブラウザが CORS(クロスオリジンリソース共有)のルールで応答の読み取りを止めているためです。
API Gateway のコンソールには「CORS を有効にする」というボタンがあり、押せば直りそうに見えます。ところが、バックエンドを Lambda プロキシ統合でつないでいる場合、ボタンを押してデプロイし直しても、GET や POST の応答には Access-Control-Allow-Origin が付きません。プロキシ統合では API Gateway が Lambda の応答をそのままクライアントに返すので、CORS のヘッダーは Lambda 関数が返す必要があるからです。
この記事では、CORS のシンプルなリクエストとプリフライトリクエストの違い、REST API で CORS を設定する場所(OPTIONS メソッドと実際のメソッドの応答)、プロキシ統合と非プロキシ統合の違いを整理します。実践では、CloudFront で配信したページから REST API を呼んで CORS エラーを起こし、コンソールで CORS を有効にしただけでは直らないことを確かめてから、Lambda 関数がヘッダーを返すように直して、ページから読めるようになるまでを体験します。

クラウドおさるよその山の猿がバナナを分けてくれと言いに来ても、門番は「この山から来た者には渡してよい」という札が無いと荷を通さないのでござる。今日は、その札を誰が書くのかを確かめるでござるよ。
この記事のメリット
- ブラウザで出る CORS エラーが、API の不具合ではなくブラウザが応答の読み取りを止めているものだと説明できるようになる
- シンプルなリクエストとプリフライト(
OPTIONS)リクエストの違いと、それぞれでどのヘッダーが要るかが分かる - Lambda プロキシ統合の REST API で、コンソールの「CORS を有効にする」だけでは直らない理由と、関数からヘッダーを返す直し方が分かる
curlでOriginヘッダーを付けて応答ヘッダーを確かめる、という切り分けができるようになる
執筆者とキャラクター紹介
執筆者:土肥(とひ)
株式会社ジェニュインの代表であり元エンジニア。これまでオンプレミスと AWS 環境のインフラ業務を担当。AWS 認定資格の学習で得た知識を、ハンズオンとともに解説するのが得意。直近では新人の教育も行っており、これから技術を学ぶ方が納得できる解説を心がけています。基礎編・Cloud Practitioner の記事では、解説役としても登場します。


クラウドおさる



はじめましてでござる。弊猿はクラウドおさる。IT インフラが異常に発達した猿山から、雲に乗って地上の AWS を学びに降りてきたのでござる。



基礎編や Cloud Practitioner の記事では、とひさんに素朴な疑問をぶつける役でござるな。Security - Specialty などの記事では、要所でワンポイントや注意点をひとこと添えるでござる。



猿山にもデータセンターや監視当番があって、地上で起きることはだいたい猿山にも同じ仕組みがあるのでござる。例え話がつい猿山に寄るのは、弊猿の癖でござる。
技術解説
CORS エラーはブラウザが止めている
ブラウザは、ページのオリジンと違うオリジンへ JavaScript から送ったリクエストについて、応答に CORS のヘッダーが付いているかを確かめます。必要なヘッダーがサーバーから返されないと、ブラウザはスクリプトに応答を渡さず、CORS エラーになります。curl やアドレスバーから直接 API を開くと成功するのは、CORS がブラウザの中のスクリプトに対するルールだからです。
CORS エラーには、次のような種類があります。
| エラーの内容 | 足りないもの |
|---|---|
Access-Control-Allow-Origin ヘッダーが存在しない | 応答に、ページのオリジンを許可する Access-Control-Allow-Origin が無い |
メソッドが Access-Control-Allow-Methods でサポートされていない | プリフライトの応答で、使うメソッドが許可されていない |
Access-Control-Allow-Headers ヘッダーが存在しない | プリフライトの応答で、リクエストに付けるヘッダー(Content-Type など)が許可されていない |
どれに当たるかは、ブラウザの開発者ツールで、失敗したリクエストの要求と応答のヘッダーを見て確かめます。
シンプルなリクエストとプリフライト
API Gateway のドキュメントでは、次の条件をすべて満たすクロスオリジンのリクエストをシンプルなリクエストと呼んでいます。
GET・HEAD・POSTのリクエストだけを許可する API リソースに対して送られるPOSTの場合はOriginヘッダーを含む- ペイロードのコンテンツタイプが
text/plain・multipart/form-data・application/x-www-form-urlencodedのいずれか - カスタムヘッダーを含まない
それ以外のリクエスト、たとえば Content-Type: application/json を付けた POST は、シンプルではないリクエストです。このときブラウザは、実際のリクエストを送る前に OPTIONS メソッドでプリフライトリクエストを送り、サーバーの応答で許可されていることを確かめてから、実際のリクエストを送ります。
| リクエスト | ブラウザが送るもの | API が返す必要があるヘッダー |
|---|---|---|
シンプルなリクエスト(例: ヘッダーを付けない GET) | 実際のリクエストだけ | 実際の応答に Access-Control-Allow-Origin |
シンプルではないリクエスト(例: JSON を送る POST) | 先に OPTIONS、許可されたら実際のリクエスト | OPTIONS の応答に Access-Control-Allow-Origin・Access-Control-Allow-Methods・Access-Control-Allow-Headers、実際の応答に Access-Control-Allow-Origin |
プリフライトが通っても、実際の応答に Access-Control-Allow-Origin が無ければ、やはり失敗します。CORS の設定は「OPTIONS に答える」ことと「実際の応答にヘッダーを付ける」ことの 2 つに分かれる、という点が重要です。





門番に「通ってよいか」を先に聞くのがプリフライト、荷物に「この山の猿に渡してよい」と札を付けるのが実際の応答の Access-Control-Allow-Origin でござる。どちらか片方だけでは、荷は届かないのでござるな。
REST API で CORS を設定する場所
REST API のコンソールでは、リソースを選んで「CORS を有効にする」から、CORS を有効にするメソッドと、Access-Control-Allow-Headers・Access-Control-Allow-Origin の値を指定できます。Access-Control-Allow-Headers には、コンソールが 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token' を用意しています。
コンソールで CORS を有効にすると、次の設定が行われます。
- リソースに
OPTIONSメソッドが無ければ追加し、その200の応答で 3 つのAccess-Control-Allow-*ヘッダーを返すように設定する(プリフライトに答える) - 実際のメソッド(
GETなど)が、200の応答でAccess-Control-Allow-Originヘッダーを返すように設定する
設定は、API をデプロイし直すまで有効になりません。また、リソースで有効にしても、子リソースとそのメソッドには引き継がれません。
「ゲートウェイの応答」で DEFAULT 4XX と DEFAULT 5XX を選ぶと、リクエストがバックエンドに届かなかったとき(認可の失敗や、存在しないリソースパスへのリクエストなど)に API Gateway が返すエラーにも、CORS のヘッダーが付きます。これを選ばないと、本当の原因が 403 などのエラーなのに、ブラウザには CORS エラーとしか見えません。
プロキシ統合では、応答のヘッダーは関数が返す
2 つ目の「実際のメソッドの応答にヘッダーを付ける」設定は、非プロキシ統合のための仕組みです。非プロキシ統合では、API Gateway の統合レスポンスで、バックエンドの応答にヘッダーを足せます。
Lambda プロキシ統合では、API Gateway で統合レスポンスを設定してバックエンドの応答を書き換えることはできず、バックエンドの応答がそのままクライアントに返されます。このため、Access-Control-Allow-Origin などのヘッダーは Lambda 関数が戻り値の headers に入れて返す必要があります。
| 統合の種類 | OPTIONS(プリフライト) | 実際のメソッドの応答の Access-Control-Allow-Origin |
|---|---|---|
| 非プロキシ統合(Lambda カスタム統合・モック統合など) | コンソールの「CORS を有効にする」で作られる | コンソールの「CORS を有効にする」で統合レスポンスに足される(うまくいかないときは手で設定する) |
| Lambda プロキシ統合 | コンソールの「CORS を有効にする」で作られる | Lambda 関数が返す(コンソールの設定では付かない) |
Python の関数なら、次のように返します。Access-Control-Allow-Origin には、許可するオリジン(https://www.example.com の形。末尾に / を付けない)か、すべてのオリジンを許可する * を入れます。
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "https://www.example.com",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
},
"body": json.dumps({"message": "ok"}),
}エラーの応答(400 や 500 を返す分岐)にもヘッダーを付けておかないと、エラーのときだけブラウザには CORS エラーとして見え、本当のエラーの中身が読めません。



プロキシ統合では、門番(API Gateway)は荷物をそのまま通すだけで、札を付け足してはくれないのでござる。コンソールで CORS を有効にして安心していると、GET も POST も相変わらず止められたままでござるぞ。
実践
CloudFront で配信した確認ページから、REST API dva-cors-api の /items を GET(シンプルなリクエスト)と POST(JSON を送るプリフライト付きのリクエスト)で呼びます。最初はどちらも CORS エラーになることを確かめ、コンソールで CORS を有効にしても OPTIONS しか直らないことを確かめてから、Lambda 関数が CORS のヘッダーを返すように直します。
前提条件
- リージョン:
ap-northeast-1(東京) - API Gateway・Lambda・CloudFront・CloudShell の操作権限を持つ IAM ユーザーまたはロールでコンソールにログイン済みであること
- ブラウザは Google Chrome など、開発者ツールを開けるものを使います
- 以下のリソースが作成済みであること
まだ用意していない場合は、「作り方の参考」の記事の手順で作れます。リソース名は、この記事の名前に読み替えてください。
| リソース種別 | リソース名 | 内容 | 作り方の参考 |
|---|---|---|---|
| Lambda 関数 | dva-cors-fn | Python 3.13。GET で品目の一覧、POST で受け取った JSON を返す。CORS のヘッダーは返さない | AWS Lambda で関数を作成しイベント駆動で実行する |
| REST API | dva-cors-api | リソース /items の GET と POST を dva-cors-fn に Lambda プロキシ統合。認可は「なし」。CORS は未設定。ステージ dev にデプロイ済み | Amazon API Gateway で REST API を作成しリクエストを処理する |
| S3 バケット | dva-cors-page-<アカウントID> | 確認ページ index.html を置いたバケット。パブリックアクセスはすべてブロック | Amazon S3 でバケットを作成しオブジェクトを管理する |
| CloudFront ディストリビューション | (説明 dva-cors page) | ページ用バケットをオリジンアクセスコントロール dva-cors-oac 経由で配信 | ― |
確認ページ index.html は、ページ自身のオリジンを表示し、「GET /items」「POST /items(JSON)」の 2 つのボタンで dva-cors-api を fetch で呼んで、結果(成功ならステータスと本文、失敗ならエラー)を表示するページです。POST は Content-Type: application/json を付けて送るので、プリフライトが発生します。
作成・変更するリソース一覧
| リソース種別 | リソース名 | 用途 |
|---|---|---|
| メソッド | dva-cors-api の /items の OPTIONS | 「CORS を有効にする」で作られる。プリフライトに答える |
| Lambda 関数のコード | dva-cors-fn | 応答に CORS のヘッダーを付けるように書き換える |
手順 1: ページから API を呼んで CORS エラーを確かめる
AWS マネジメントコンソールにログインし、リージョンが ap-northeast-1(東京)であることを確認します。上部の検索バーに CloudFront と入力し、「CloudFront」を選択します。ディストリビューションの一覧で、説明が dva-cors page のディストリビューションの「ドメイン名」をコピーします。
ブラウザの新しいタブで https://<コピーしたドメイン名>/ を開きます。「このページのオリジン」に https://dxxxxxxxxxxxxx.cloudfront.net の形のオリジンが表示されます。この値は手順 3 と手順 4 で使うので控えておきます。
「GET /items」と「POST /items(JSON)」を順にクリックします。結果欄にはクリックした方の結果だけが表示され、どちらも「失敗: Failed to fetch」になることを確認します。


ブラウザの開発者ツール(Chrome では F12 キー)を開き、「Console」タブを確認すると、Access to fetch at '…' from origin '…' has been blocked by CORS policy で始まる CORS エラーが表示されています。GET では No 'Access-Control-Allow-Origin' header is present on the requested resource、POST では Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource と、どちらも Access-Control-Allow-Origin ヘッダーが無いことが理由として示されます。POST の方は、プリフライトの段階で止まっていることも読み取れます。
手順 2: curl で応答のヘッダーを確かめる
API 自体が動いているかを、ブラウザを通さずに確かめます。コンソール上部の CloudShell のアイコンから CloudShell を開きます。API の ID と、手順 1 で控えたオリジンを変数に入れます。<api-id> は API Gateway の API の一覧の「ID」で確認できます。
URL=https://<api-id>.execute-api.ap-northeast-1.amazonaws.com/dev/items
ORIGIN=https://dxxxxxxxxxxxxx.cloudfront.netブラウザと同じように Origin ヘッダーを付けて、GET の応答ヘッダーを表示します。
curl -s -D - -o /dev/null -H "Origin: $ORIGIN" $URLHTTP/2 200 が返り、API は正常に応答していることを確認します。応答ヘッダーに access-control-allow-origin が無いことも確認します。
続けて、ブラウザが POST の前に送るプリフライトと同じリクエストを送ります。
curl -s -D - -o /dev/null -X OPTIONS -H "Origin: $ORIGIN" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" $URL/items には OPTIONS メソッドが無いので、HTTP/2 403(x-amzn-errortype: MissingAuthenticationToken)が返り、access-control-allow-* のヘッダーも付いていないことを確認します。





API は荷物をちゃんと渡しているのに、札が付いていないから門番が止めているのでござる。curl で Origin を付けて応答のヘッダーを見るのが、いちばん早い見分け方でござるな。
手順 3: コンソールで CORS を有効にする
上部の検索バーに API Gateway と入力し、「API Gateway」を選択します。API の一覧から dva-cors-api を開き、「リソース」で /items を選択します。「リソースの詳細」の「CORS を有効にする」をクリックし、以下を設定します。
- ゲートウェイの応答:
DEFAULT 4XXとDEFAULT 5XXを選択 - Access-Control-Allow-Methods:
GET・POST・OPTIONSを選択 - Access-Control-Allow-Headers: デフォルトの
Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Tokenのまま - Access-Control-Allow-Origin: 手順 1 で控えたオリジン(例
https://dxxxxxxxxxxxxx.cloudfront.net。末尾に/を付けない)


「保存」をクリックします。/items の下に OPTIONS メソッドが追加されたことを確認します。このとき、画面上部に「4 メソッドの CORS ヘッダーを更新できませんでした」という赤いバナーが表示されます。詳細には GET と POST のメソッドレスポンス・統合レスポンスへの Access-Control-Allow-Origin の追加が並びます。Lambda プロキシ統合の GET と POST には、コンソールからヘッダーを足せないためです。OPTIONS は作成されているので、そのまま次に進みます。
続けて「API をデプロイ」をクリックし、ステージで dev を選んで「デプロイ」をクリックします。CORS の設定は、デプロイして初めて反映されます。
手順 4: OPTIONS だけが直ったことを確かめる
CloudShell に戻り、手順 2 の 2 つのコマンドをもう一度実行します。
OPTIONS は 200 になり、access-control-allow-origin・access-control-allow-methods・access-control-allow-headers が返るようになったことを確認します。一方で、GET の応答には相変わらず access-control-allow-origin が無いことを確認します。GET は Lambda プロキシ統合なので、API Gateway は Lambda の応答をそのまま返しています。


確認ページに戻って「GET /items」と「POST /items(JSON)」をクリックしても、どちらも「失敗: Failed to fetch」のままです。POST はプリフライトを通るようになりましたが、実際の POST の応答に Access-Control-Allow-Origin が無いので、ブラウザが読み取りを止めています。
手順 5: Lambda 関数から CORS のヘッダーを返す
上部の検索バーに Lambda と入力し、「Lambda」を選択します。関数の一覧から dva-cors-fn を開き、「コード」タブのエディタで lambda_function.py を次の内容に置き換えます。ALLOWED_ORIGIN の値は、手順 1 で控えたオリジンに置き換えます。
import json
ALLOWED_ORIGIN = "https://dxxxxxxxxxxxxx.cloudfront.net" # 確認ページのオリジン(末尾に / を付けない)
CORS_HEADERS = {
"Access-Control-Allow-Origin": ALLOWED_ORIGIN,
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
}
ITEMS = [
{"id": 1, "name": "banana"},
{"id": 2, "name": "mango"},
]
def response(status, body):
# 正常の応答にもエラーの応答にも、同じ CORS のヘッダーを付ける
return {
"statusCode": status,
"headers": {"Content-Type": "application/json", **CORS_HEADERS},
"body": json.dumps(body, ensure_ascii=False),
}
def lambda_handler(event, context):
if event["httpMethod"] == "POST":
try:
item = json.loads(event.get("body") or "{}")
except json.JSONDecodeError:
return response(400, {"message": "body must be JSON"})
return response(201, {"added": item})
return response(200, {"items": ITEMS})「Deploy」をクリックします。関数のコードを変えただけなので、API Gateway のデプロイは不要です。
手順 6: ページから読めるようになったことを確かめる
確認ページに戻り、「GET /items」をクリックします。結果欄に「成功: HTTP 200」と品目の一覧が表示されることを確認します。続けて「POST /items(JSON)」をクリックし、「成功: HTTP 201」と、送った JSON が added に入った本文が表示されることを確認します。


CloudShell で、手順 2 の GET のコマンドをもう一度実行します。応答ヘッダーに access-control-allow-origin: https://dxxxxxxxxxxxxx.cloudfront.net が付いたことを確認します。このヘッダーは、API Gateway ではなく Lambda 関数が返しています。





「プロキシ統合の API で CORS エラーが直らない」と言われたら、コンソールのボタンではなく、関数が headers で Access-Control-Allow-Origin を返しているかを疑うのでござる。プリフライトの OPTIONS と実際の応答のヘッダーは別物だという点も、取り違えやすいでござるよ。
まとめ
| 確認すること | どこで設定するか |
|---|---|
プリフライト(OPTIONS)に Access-Control-Allow-Origin・-Methods・-Headers を返しているか | リソースで「CORS を有効にする」(OPTIONS メソッドが作られる)。設定後は API をデプロイする |
実際のメソッドの応答に Access-Control-Allow-Origin が付いているか(プロキシ統合) | Lambda 関数の戻り値の headers。エラーの応答にも付ける |
実際のメソッドの応答に Access-Control-Allow-Origin が付いているか(非プロキシ統合) | 「CORS を有効にする」でメソッドレスポンス・統合レスポンスに足される。足りなければ統合レスポンスを手で設定する |
API Gateway 自身が返すエラー(403 など)でも CORS エラーに見えないか | 「ゲートウェイの応答」で DEFAULT 4XX・DEFAULT 5XX を選ぶ |
- CORS エラーは、サーバーが CORS のヘッダーを返さないため、ブラウザがスクリプトに応答を渡さないことで起きる。
curlでOriginを付けて応答ヘッダーを見ると切り分けられる Content-Type: application/jsonを付けたPOSTなどのシンプルではないリクエストでは、ブラウザが先にOPTIONSのプリフライトを送る- Lambda プロキシ統合では、API Gateway はバックエンドの応答を書き換えられない。実際の応答の CORS のヘッダーは関数が返す
- CORS の設定はリソースごとで、子リソースには引き継がれない。変更したら API をデプロイし直す
参照先
- API Gateway での REST API の CORS – Amazon API Gateway
- API Gateway コンソールを使用してリソースで CORS を有効にする – Amazon API Gateway
- API Gateway API の CORS をテストする – Amazon API Gateway
- API Gateway での Lambda プロキシ統合 – Amazon API Gateway
- API Gateway でのゲートウェイレスポンスタイプ – Amazon API Gateway
- API Gateway による CORS エラーのトラブルシューティング | AWS re:Post












