API GatewayバックのLambda関数にアプリケーションからカスタムヘッダーを渡す際にハマったこと
こんにちは、CX事業本部 IoT事業部の若槻です。
今回は、API GatewayバックのLambda関数にアプリケーションからカスタムヘッダーを渡す際にハマったので対処した話です。
実装
CDKコード
Lambda+API Gateway(Lambdaプロキシ統合)のバックエンド構成をAWS CDKで実装しました。
import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import * as lambdaNodejs from '@aws-cdk/aws-lambda-nodejs'; import * as apigateway from '@aws-cdk/aws-apigateway'; export class AwsCdkAppStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const getDataFunction = new lambdaNodejs.NodejsFunction(this, 'getData', { entry: 'src/lambda/handlers/get-data.ts', runtime: lambda.Runtime.NODEJS_14_X, }); const restApi = new apigateway.RestApi(this, 'RestApi', { restApiName: 'restApi', }); const methodOptionsWithCors: apigateway.MethodOptions = { methodResponses: [ { statusCode: '200', responseModels: { 'application/json': apigateway.Model.EMPTY_MODEL, }, responseParameters: { 'method.response.header.Access-Control-Allow-Methods': false, 'method.response.header.Access-Control-Allow-Headers': false, 'method.response.header.Access-Control-Allow-Origin': false, }, }, ], }; const corsIntegration = new apigateway.MockIntegration({ integrationResponses: [ { statusCode: '200', responseParameters: { 'method.response.header.Access-Control-Allow-Headers': "'Content-Type'", 'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,POST,PUT,GET,DELETE,PATCH'", 'method.response.header.Access-Control-Allow-Origin': "'*'", }, responseTemplates: { 'application/json': '{}', }, }, ], passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_MATCH, requestTemplates: { 'application/json': '{"statusCode": 200}', }, }); const dataResource = restApi.root.addResource('data'); dataResource.addMethod( 'GET', new apigateway.LambdaIntegration(getDataFunction) ); dataResource.addMethod('OPTIONS', corsIntegration, methodOptionsWithCors); } }
Lambdaコード
LambdaではカスタムヘッダーdeviceId
が指定されていればステータスコード200、未指定ならステータスコード400を返します。
import { APIGatewayEvent } from 'aws-lambda'; const headers = { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Origin': '*', }; export const handler = async (event: APIGatewayEvent) => { console.log(event.headers); //deviceIdヘッダーが未指定なら400エラーを返す if (event.headers['deviceId'] == undefined) { console.log('deviceIdが指定されていません'); return { statusCode: 400, headers: headers, body: JSON.stringify({ timestamp: new Date(), }), }; } return { statusCode: 200, headers: headers, body: JSON.stringify({ timestamp: new Date(), method: event.httpMethod, }), }; };
API Gatewayコンソールでのテスト
cdk deploy
でRest APIとLambda関数をデプロイしたら、API Gatewayコンソールで動作確認をしてみます。
deviceId
ヘッダーを指定した場合は200
が返ります。
deviceId
ヘッダーを指定しない場合は400
が返ります。
コンソールからはちゃんと動作していることを確認できました。
Webアプリ(React)からアクセスしてみる
前述のRest APIにリクエストをする下記のReactアプリを実装しました。
import React, { useEffect } from 'react'; import Axios from 'axios'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT; const deviceId = 'device01'; let headers = { 'Content-Type': 'application/json', deviceId: deviceId, }; const App: React.FC = () => { useEffect(() => { Axios.get(`${apiEndpoint}/data`, { headers: headers, }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, []); return <div>Hello World.</div>; }; export default App;
ハマり1:Access-Control-Allow-Headersへの未追加
アプリにブラウザでアクセスすると下記のエラーとなりました。
Request header field deviceid is not allowed by Access-Control-Allow-Headers in preflight response.
APIへのプリフライトリクエストに対するAccess-Control-Allow-Headers
レスポンスヘッダーにdeviceid
が追加されている必要があるようです。
プリフライトリクエストとは、ブラウザからのCORSリクエストがCORSプロトコルに則っているかどうかをAPIサーバーがチェックするためのリクエストです。
そこでCDKコードを下記のように修正します。
const corsIntegration = new apigateway.MockIntegration({ integrationResponses: [ { statusCode: '200', responseParameters: { 'method.response.header.Access-Control-Allow-Headers': + "'Content-Type,deviceId'", - "'Content-Type'",
ハマり2:ブラウザからのアクセスではヘッダーが小文字となる
前述の修正済みのRest APIに対して再度アプリからアクセスすると、次は400エラーとなりました。
Lambdaのログを確認するとデバイスIDヘッダーがdeviceId
ではなくdeviceid
(小文字のみ)となっています。そのためLambdaが400エラーをレスポンスしていたようです。
ブラウザのネットワークログを確認すると、ブラウザからのリクエスト時点でdeviceid
となっています。
ドキュメントによるとHTTPヘッダーは大文字と小文字を区別しない(case-insensitive)とのこと。そのためリクエスト時にヘッダーが小文字のみのdeviceid
に変換され、Lambda側で大文字含むdeviceId
をパースできなかったようです。
HTTP ヘッダーは、大文字小文字を区別しないヘッダー名とそれに続くコロン (:)、 値で構成されます。
そこでLambdaコードを下記のようにヘッダーを小文字で扱うように修正します。
export const handler = async (event: APIGatewayEvent) => { console.log(event.headers); + if (event.headers['deviceid'] == undefined) { - if (event.headers['deviceId'] == undefined) { console.log('deviceIdが指定されていません');
するとエラー無くリクエストを実行することができました。
バックエンドのE2Eテストでは問題を見つけられなかった
今回ハマった事象ですが、バックエンドを実装した際に作成したE2Eテストでは問題を見つけられませんでした。理由としては、そのE2EテストではAxiosをスクリプトで実行してRest APIにリクエストをするのですが、今回ハマった事象はブラウザでアクセスすることが条件であったためです。よってアプリケーションを実装する際に初めて問題が見つかった、という経緯がありました。
おわりに
API GatewayバックのLambda関数にアプリケーションからカスタムヘッダーを渡す際にハマったので対処した話でした。
Webの基本的なところの理解がまだまだだということを思い知らされました。またブラウザを使うことを前提としたより本番利用に近い形でのテストを実装する必要性も感じました。
参考
- CORS Policy 違反と,Preflight request (OPTIONS) について
- express - Request header field Access-Control-Allow-Headers is not allowed by itself in preflight response - Stack Overflow
以上