AWS

API Gatewayのエラーレスポンス マッピングテンプレート

AWS

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"
  }
}

 

 

 

参考

 

 

Amazonおすすめ

iPad 9世代 2021年最新作

iPad 9世代出たから買い替え。安いぞ!🐱 初めてならiPad。Kindleを外で見るならiPad mini。ほとんどの人には通常のiPadをおすすめします><

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)