API Gateway の CORS エラーを解決する方法 ── Lambda プロキシ統合ではヘッダーを関数から返す

目次

概要

ブラウザの 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 関数がヘッダーを返すように直して、ページから読めるようになるまでを体験します。

プリフライトは API Gateway が答え、実際の応答のヘッダーは Lambda 関数が返す
プリフライトは API Gateway が答え、実際の応答のヘッダーは 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 つに分かれる、という点が重要です。

JSON を送る POST は、プリフライト → 実際の POST の 2 往復で CORS を確かめる
JSON を送る POST は、プリフライト → 実際の POST の 2 往復で CORS を確かめる
クラウドおさる

門番に「通ってよいか」を先に聞くのがプリフライト、荷物に「この山の猿に渡してよい」と札を付けるのが実際の応答の 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 も相変わらず止められたままでござるぞ。

新卒・未経験の方へ
クラウドおさる

AWS を仕事にしたい方へ ── 新卒・未経験から

この記事を書いているのは、AWS の設計・構築を仕事にしている現役エンジニアです。運営元の株式会社ジェニュインでは、新卒・未経験の方も募集しています。異業種や文系からインフラエンジニアになり活躍しているメンバーもいます。

  • ✓システム開発・WEB エンジニア / インフラ構築などの職種で、未経験から応募できます
  • ✓未経験から入社した社員の声を含む社員アンケートを掲載
  • ✓応募フォームから 1 分で応募できます

実践

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-fnPython 3.13。GET で品目の一覧、POST で受け取った JSON を返す。CORS のヘッダーは返さないAWS Lambda で関数を作成しイベント駆動で実行する
REST APIdva-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" $URL

HTTP/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-* のヘッダーも付いていないことを確認します。

CloudShell の実行結果(GET は 200 だが access-control-allow-origin が無い、OPTIONS は 403 でヘッダーも無い)
CloudShell の実行結果(GET は 200 だが access-control-allow-origin が無い、OPTIONS は 403 でヘッダーも無い)
クラウドおさる

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。末尾に / を付けない)
CORS を有効にする画面(ゲートウェイの応答・メソッド・Access-Control-Allow-Origin を設定した状態)
CORS を有効にする画面(ゲートウェイの応答・メソッド・Access-Control-Allow-Origin を設定した状態)

「保存」をクリックします。/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 の応答をそのまま返しています。

CloudShell の実行結果(OPTIONS には Access-Control-Allow-* が付いたが、GET には access-control-allow-origin が無いまま)
CloudShell の実行結果(OPTIONS には Access-Control-Allow-* が付いたが、GET には access-control-allow-origin が無いまま)

確認ページに戻って「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 に入った本文が表示されることを確認します。

確認ページで POST が成功した状態(HTTP 201 と `added` の本文が表示されている)
確認ページで POST が成功した状態(HTTP 201 と `added` の本文が表示されている)

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

CloudShell の実行結果(GET の応答に access-control-allow-origin が付いている)
CloudShell の実行結果(GET の応答に access-control-allow-origin が付いている)
クラウドおさる

「プロキシ統合の 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 をデプロイし直す

参照先

キャリア相談
クラウドおさる

独学の先に、AWS の仕事がある ── ジュニアおさるのキャリア相談

CLF には受かったけれど、次に何をすればいいのか分からない。独学で AWS を学ぶジュニアおさるの相談に、株式会社ジェニュイン代表の土肥が答えています。

  • ✓資格はゴールか、CLF の次に何から手を付けるか
  • ✓EC2 の次に作ってみる構成と、IaC・コスト管理
  • ✓実務経験ゼロでも、面接で話せる「これまで」の作り方
新卒・未経験の方へ
クラウドおさる

AWS を仕事にしたい方へ ── 新卒・未経験から

この記事を書いているのは、AWS の設計・構築を仕事にしている現役エンジニアです。運営元の株式会社ジェニュインでは、新卒・未経験の方も募集しています。異業種や文系からインフラエンジニアになり活躍しているメンバーもいます。

  • ✓システム開発・WEB エンジニア / インフラ構築などの職種で、未経験から応募できます
  • ✓未経験から入社した社員の声を含む社員アンケートを掲載
  • ✓応募フォームから 1 分で応募できます

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

株式会社ジェニュイン(https://genuine-pt.jp)の代表取締役.

2018年〜インフラエンジニアとしてキャリアをスタートし、オンプレミスのネットワーク・サーバ環境で3年半、クラウド環境で4年半の8年間エンジニアとして従事。
2021年に佐藤氏の創業した株式会社Luxyを引き継ぎ、代表に就任。
その後アガルートグループ内の株式会社ジェニュインと合併。
合併後同社代表に就任。

目次