
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 のリクエストおよびレスポンスパラメータとステータスコードをオーバーライドする

![Target class [xxxxController] does not exist. Laravel8](https://www.yuulinux.tokyo/contents/wp-content/uploads/2019/01/laravel_20190131_1-150x150.jpg)


