J'invoque une méthode AWS Lambda basée sur Python à partir d'API Gateway en mode non proxy. Comment dois-je gérer correctement les exceptions, de sorte qu'un code d'état HTTP approprié soit défini avec un corps JSON utilisant des parties de l'exception.
Par exemple, j'ai le gestionnaire suivant:
def my_handler(event, context):
try:
s3conn.head_object(Bucket='my_bucket', Key='my_filename')
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == "404":
raise ClientException("Key '{}' not found".format(filename))
# or: return "Key '{}' not found".format(filename) ?
class ClientException(Exception):
pass
Dois-je lever une exception ou retourner une chaîne? Alors, comment dois-je configurer la réponse d'intégration? Évidemment, j'ai RTFM mais le FM est FU.
Il y a un certain nombre de choses que vous devez savoir sur Lambda, API Gateway et comment ils fonctionnent ensemble.
Lorsqu'une exception est levée à partir de votre gestionnaire/fonction/méthode, l'exception est sérialisée dans un message JSON. À partir de votre exemple de code, sur un 404 de S3, votre code lancerait:
{
"stackTrace": [
[
"/var/task/mycode.py",
118,
"my_handler",
"raise ClientException(\"Key '{}' not found \".format(filename))"
]
],
"errorType": "ClientException",
"errorMessage": "Key 'my_filename' not found"
}
Les "réponses d'intégration" mappent les réponses de Lambda aux codes HTTP. Ils permettent également de modifier le corps du message lors de leur passage.
Par défaut, une réponse d'intégration "200" est configurée pour vous, qui transmet toutes les réponses de Lambda au client telles quelles, y compris les exceptions JSON sérialisées, en tant que réponse HTTP 200 (OK).
Pour obtenir de bons messages, vous pouvez utiliser la réponse d'intégration "200" pour mapper la charge utile JSON à l'un de vos modèles définis.
Pour les exceptions, vous souhaiterez définir un code d'état HTTP approprié et probablement supprimer la trace de pile pour masquer les éléments internes de votre code.
Pour chaque code d'état HTTP que vous souhaitez renvoyer, vous devrez ajouter une entrée "Réponse d'intégration". La réponse d'intégration est configurée avec une correspondance d'expression régulière (en utilisant Java.util.regex.Matcher.matches()
pas .find()
) qui correspond à errorMessage champ. Une fois la correspondance établie, vous pouvez ensuite configurer un modèle de mappage de corps pour formater sélectivement un corps d'exception approprié.
Étant donné que l'expression régulière ne correspond qu'au champ errorMessage de l'exception, vous devrez vous assurer que votre exception contient suffisamment d'informations pour permettre à différentes réponses d'intégration de correspondre et définissez l'erreur en conséquence. (Vous ne pouvez pas utiliser .*
Pour faire correspondre toutes les exceptions, car cela semble correspondre à toutes les réponses, y compris les non-exceptions!)
Pour créer des exceptions avec suffisamment de détails dans leur message, error-handling-patterns-in-Amazon-api-gateway-and-aws-lambda blog vous recommande de créer un gestionnaire d'exceptions dans votre gestionnaire pour remplir la détails de l'exception dans une chaîne JSON à utiliser dans le message d'exception.
Mon approche préférée consiste à créer une nouvelle méthode supérieure en tant que gestionnaire qui traite de la réponse à API Gateway. Cette méthode retourne la charge utile requise ou lève une exception avec une exception d'origine codée en tant que chaîne JSON en tant que message d'exception.
def my_handler_core(event, context):
try:
s3conn.head_object(Bucket='my_bucket', Key='my_filename')
...
return something
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == "404":
raise ClientException("Key '{}' not found".format(filename))
def my_handler(event=None, context=None):
try:
token = my_handler_core(event, context)
response = {
"response": token
}
# This is the happy path
return response
except Exception as e:
exception_type = e.__class__.__name__
exception_message = str(e)
api_exception_obj = {
"isError": True,
"type": exception_type,
"message": exception_message
}
# Create a JSON string
api_exception_json = json.dumps(api_exception_obj)
raise LambdaException(api_exception_json)
# Simple exception wrappers
class ClientException(Exception):
pass
class LambdaException(Exception):
pass
Exceptionnellement, Lambda renverra désormais:
{
"stackTrace": [
[
"/var/task/mycode.py",
42,
"my_handler",
"raise LambdaException(api_exception_json)"
]
],
"errorType": "LambdaException",
"errorMessage": "{\"message\": \"Key 'my_filename' not found\", \"type\": \"ClientException\", \"isError\": true}"
}
Maintenant que vous avez tous les détails dans le message d'erreur , vous pouvez commencer à mapper les codes d'état et créer des charges utiles d'erreur bien formées. API Gateway analyse et échappe le champ errorMessage, de sorte que l'expression régulière utilisée n'a pas besoin de gérer l'échappement.
Pour intercepter cette erreur ClientException en tant que 400 et mapper la charge utile sur un modèle d'erreur propre, vous pouvez procéder comme suit:
Créer un nouveau modèle d'erreur:
{
"type": "object",
"title": "MyErrorModel",
"properties": {
"isError": {
"type": "boolean"
},
"message": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"token",
"isError",
"type"
]
}
400
400
.*"type"\s*:\s*"ClientException".*
Ajoutez un modèle de mappage de corps pour application/json
Pour mapper le contenu de errorMessage
à votre modèle:
#set($inputRoot = $input.path('$'))
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"isError" : true,
"message" : "$errorMessageObj.message",
"type": "$errorMessageObj.type"
}