422のバリデーションエラーのレスポンスを正しくJSONにする。
Lambda統合リクエストではない場合、自分でマッピングする必要があります🐱
実現したいもの
この部分
function.py
# coding: utf-8 import json import sys import datetime import hashlib import logging import os from botocore.exceptions import ClientError # Lambda Layer import exceptions def lambda_handler(event, context): headers = config.headers errors = _validateHeaders(event) if len(errors) != 0: raise exceptions.ExtendException(422, errors, "", headers) if isinstance(event, dict) and 'body-json' in event.keys(): # CloudFront -> API Gateway経由の場合はbodyキーに格納されているので変換 body = event['body-json'] else: message = "Internal server error. key=body-json not found" raise exceptions.ExtendException(500, None, message, headers) ・・・
utils/exceptions.py
import json class ExtendException(Exception): def __init__(self, statusCode, errors, message, headers): self.statusCode = statusCode self.errors = errors self.message = message self.headers = headers def __str__(self): response = { "statusCode": self.statusCode, "items": self.errors, "message": self.message, "option_data": [], "title": None, "headers": self.headers } return json.dumps(response, ensure_ascii=False)
作成したマッピングテンプレート application/json
#set($errorObj = $util.parseJson($input.path('$.errorMessage'))) { "code": 422, "data": null, "error": "items": [ #foreach($item in $errorObj.items) {"$item.key": [ #set($i = 0) #foreach($message in $item.messages) "$i": "$message"#if($foreach.hasNext),#end #set($i = $i +1) #end ] }#if($foreach.hasNext),#end #end ], "message": "$errorObj.message", "option_data": [], "title": null }
TerraformのOpenAPI3定義例
openapi: "3.0.1" info: version: "2022-02-03T09:16:51Z" title: "sample-app api" schemes: - "https" paths: /v1/user/access/store: post: produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" "422": description: "422 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" "500": description: "500 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" x-amazon-apigateway-integration: httpMethod: "POST" uri: "${lambda_store_access_invoke_arn}" credentials: "${apigateway_role_arn}" responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" ".*422.*": statusCode: "422" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" responseTemplates: application/json: "#set($errorObj = $util.parseJson($input.path('$.errorMessage')))\n\ {\n \"code\": 422,\n \"data\": null,\n \"error\": \"items\": [\n\ #foreach($item in $errorObj.items)\n {\"$item.key\": [\n #set($i\ \ = 0)\n #foreach($message in $item.messages)\n \"$i\": \"\ $message\"#if($foreach.hasNext),#end\n #set($i = $i +1)\n \ \ #end\n ]\n }#if($foreach.hasNext),#end\n#end \n],\n \"message\"\ : \"$errorObj.message\",\n \"option_data\": [],\n \"title\": null\n\ }\n" ".*500.*|.*Task timed out.*|.*failed.*|.*Method completed with status.*|.*is not defined.*": statusCode: "500" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" responseTemplates: application/json: "#set($errorObj = $util.parseJson($input.path('$.errorMessage')))\n\ {\n \"code\": 500,\n \"data\": null,\n \"error\": \"items\": [\n\ #foreach($item in $errorObj.items)\n {\"$item.key\": [\n #set($i\ \ = 0)\n #foreach($message in $item.messages)\n \"$i\": \"\ $message\"#if($foreach.hasNext),#end\n #set($i = $i +1)\n \ \ #end\n ]\n }#if($foreach.hasNext),#end\n#end \n],\n \"message\"\ : \"$errorObj.message\",\n \"option_data\": [],\n \"title\": null\n\ }" requestTemplates: application/json: "#set($allParams = $input.params())\r\n{\r\n\"body-json\" : $input.json('$'),\r\n\"params\" : {\r\n#foreach($type in $allParams.keySet())\r\n #set($params = $allParams.get($type))\r\n\"$type\" : {\r\n #foreach($paramName in $params.keySet())\r\n \"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\r\n #if($foreach.hasNext),#end\r\n #end\r\n}\r\n #if($foreach.hasNext),#end\r\n#end\r\n},\r\n\"stage-variables\" : {\r\n#foreach($key in $stageVariables.keySet())\r\n\"$key\" : \"$util.escapeJavaScript($stageVariables.get($key))\"\r\n #if($foreach.hasNext),#end\r\n#end\r\n},\r\n\"context\" : {\r\n \"account-id\" : \"$context.identity.accountId\",\r\n \"api-id\" : \"$context.apiId\",\r\n \"api-key\" : \"$context.identity.apiKey\",\r\n \"authorizer-principal-id\" : \"$context.authorizer.principalId\",\r\n \"caller\" : \"$context.identity.caller\",\r\n \"cognito-authentication-provider\" : \"$context.identity.cognitoAuthenticationProvider\",\r\n \"cognito-authentication-type\" : \"$context.identity.cognitoAuthenticationType\",\r\n \"cognito-identity-id\" : \"$context.identity.cognitoIdentityId\",\r\n \"cognito-identity-pool-id\" : \"$context.identity.cognitoIdentityPoolId\",\r\n \"http-method\" : \"$context.httpMethod\",\r\n \"stage\" : \"$context.stage\",\r\n \"source-ip\" : \"$context.identity.sourceIp\",\r\n \"user\" : \"$context.identity.user\",\r\n \"user-agent\" : \"$context.identity.userAgent\",\r\n \"user-arn\" : \"$context.identity.userArn\",\r\n \"request-id\" : \"$context.requestId\",\r\n \"resource-id\" : \"$context.resourceId\",\r\n \"resource-path\" : \"$context.resourcePath\"\r\n }\r\n}" passthroughBehavior: "never" contentHandling: "CONVERT_TO_TEXT" type: "aws" options: consumes: - "application/json" produces: - "application/json" responses: "200": description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" "422": description: "422 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" "500": description: "500 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" Access-Control-Allow-Methods: type: "string" Access-Control-Allow-Headers: type: "string" x-amazon-apigateway-integration: responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,token,domain'" method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates: application/json: "{\"statusCode\": 200}" passthroughBehavior: "when_no_match" type: "mock" definitions: Empty: type: "object" title: "Empty Schema"
もくじ
CORSのチェック
access-control-allow-origin: *
access-control-allow-origin: web.example.net
このようなものがきちんと設定されているか確認します。
% curl -i -X OPTIONS https://example.net/web_api/v1/user/access/store HTTP/2 200 content-type: application/json content-length: 0 date: Wed, 20 Apr 2022 06:44:57 GMT x-amzn-requestid: 9eeb815f-cd75-4981-b8dd-79aea08f3fe9 access-control-allow-origin: * // ●OK access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,token,domain x-amz-apigw-id: Q3fMdHN_NjMFzxQ= access-control-allow-methods: OPTIONS,POST x-cache: Miss from cloudfront via: 1.1 xxxxx.cloudfront.net (CloudFront) x-amz-cf-pop: NRT12-C2 x-amz-cf-id: PevB3HkLj89apHpQLo6LA14Y6yKzRCWLWbImCtDBZ5qjc4KMguWkhg==
$ curl -i -X POST https://example.net/web_api/v1/user/access/store HTTP/2 422 content-type: application/json content-length: 299 date: Wed, 20 Apr 2022 06:46:13 GMT x-amzn-requestid: b5dad90e-9a12-45f6-8db3-9b20d8b5752f access-control-allow-origin: * // ●OK x-amz-apigw-id: Q3fYRHJltjMFWEg= x-amzn-trace-id: Root=1-625fac34-0c74736b0e78aa2534abf56a;Sampled=0 x-cache: Error from cloudfront via: 1.1 xxxxx.cloudfront.net (CloudFront) x-amz-cf-pop: NRT12-C2 x-amz-cf-id: YJmqfRzxRt8lITdTJ-L5tpPeXFnbXgiecwWt2QPvVG8wyrZeLKFAkQ== { "code": 422, "data": null, "error": "items": [ {"token": [ "0": "tokenの検証に失敗しました。" ] }], "message": "", "option_data": [], "title": null, "headers": { "Access-Control-Allow-Origin": "*", "Content-Type": "application/json" } }
参考
- API Gatewayのマッピングテンプレートの設定例
- 【AWS】API GatewayのMapping Templateで、Key=ValueペアをJSONに変換する
- マッピングテンプレートを使用して、API のリクエストおよびレスポンスパラメータとステータスコードをオーバーライドする