API GatewayでLambdaのエラーハンドリング


題材とするアーキテクチャ

API Gatewayの返り値を整形する。

エラー時に400/500台のステータスコードを付してDescriptionをつけて返したい。

1 Exceptionクラスを継承して拡張した例外クラスを作る。
2 エラー時にraiseする。

Lambda(1)
import json
import boto3
import logging
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

dynamodb = boto3.resource('dynamodb')

# 例外クラスを拡張
class ExtendException(Exception):
    def __init__(self, statusCode, description):
        self.statusCode = statusCode
        self.description = description

    def __str__(self):
        obj = {
            "statusCode": self.statusCode,
            "description": self.description
        }
        return json.dumps(obj)

def lambda_handler(event, context):

    # validation check
    if not "key" in event or not isinstance(event["key"], int):
        raise ExtendException(400, "Bad Request")

    # 何らかの処理

    return 0

テスト

response
{
  "errorMessage": "{\"statusCode\": 400, \"description\": \"Bad Request\"}",
  "errorType": "ExtendException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 29, in lambda_handler\n    raise ExtendException(400, \"Bad Request\")\n"
  ]
}

3 API Gatewayのレスポンスをマッピング


Lambdaエラーの正規表現では.*statusCode: 400,.*を指定。
Lambdaから返されるエラーレスポンスのerrorMessageの部分のみ返している。

テスト

response
{
  "statusCode": 400,
  "description": "Bad Request"
}

別Lambdaからコールした時にエラー内容を表示する。

以下のようにしてエラーレスポンスを拾うことができる。

try:
    pass
except urllib.error.HTTPError as err:
    res = err.read().decode("utf-8")
Lambda(2)
import json
import urllib.request, urllib.error
import logging
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class ExtendException(Exception):
    def __init__(self, statusCode, description):
        self.statusCode = statusCode
        self.description = description

    def __str__(self):
        obj = {
            "statusCode": self.statusCode,
            "description": self.description
        }
        return json.dumps(obj)

request_url = "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1"
api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

def lambda_handler(event, context):
    try:
        headers = {'x-api-key': api_key, "Content-Type":"application/json"}
        method = "POST"

        # validation errorするようにAPIをコールする。
        request_json = {
            "notExistKey": 1
        }

        req = urllib.request.Request(url=request_url, method=method, headers=headers, data=json.dumps(request_json).encode())

        with urllib.request.urlopen(req) as res:
            body = res.read().decode("utf-8")

    except urllib.error.HTTPError as err:
        traceback.print_exc()
        res = err.read().decode("utf-8")
        res = json.loads(res)
        logger.info("------------------")
        logger.info(res)
        logger.info("------------------")
        raise ExtendException(500, "[ERROR] {} : {}".format(res["statusCode"], res["description"]))

    except urllib.error.URLError as err:
        traceback.print_exc()
        res = err.read().decode("utf-8")
        res = json.loads(res)
        logger.info("------------------")
        logger.info(res)
        logger.info("------------------")
        raise ExtendException(500, "[ERROR] {} : {}".format(res["statusCode"], res["description"]))
    except:
        traceback.print_exc()
        raise ExtendException(500, "Internal Error")

    return json.loads(body)

テスト

response
{
  "errorMessage": "{\"statusCode\": 500, \"description\": \"[ERROR] 400 : Bad Request\"}",
  "errorType": "ExtendException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 45, in lambda_handler\n    raise ExtendException(500, \"[ERROR] {} : {}\".format(res[\"statusCode\"], res[\"description\"]))\n"
  ]
}

追記 2020/08/06

Lambdaのタイムアウトをハンドリングする。

上記の設定ではLambda(1)でタイムアウトした際にステータスコード200が返ってしまう。

{
  "errorMessage": "2020-08-06T13:36:57.311Z 312a1819-af1a-4fc5-bb9c-f8ed8f573a9b Task timed out after 3.00 seconds"
}

統合レスポンスの正規表現では以下のように指定することでLambdaのタイムアウトをハンドリングできる。

.*"statusCode": 500,.*|.*Task timed out after.*