web-dev-qa-db-fra.com

API Gateway + Lambda + Python: gestion des exceptions

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.

18

tl; dr

  1. Votre gestionnaire Lambda doit lever une exception si vous souhaitez une réponse autre que 200.
  2. Attrapez toutes les exceptions dans votre méthode de gestionnaire. Formatez le message d'exception intercepté en JSON et lancez-le en tant que type d'exception personnalisé.
  3. Utilisez la réponse d'intégration pour regex votre exception personnalisée trouvée dans le champ errorMessage de la réponse Lambda.

API Gateway + Gestion des exceptions AWS Lambda

Il y a un certain nombre de choses que vous devez savoir sur Lambda, API Gateway et comment ils fonctionnent ensemble.

Exceptions Lambda

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

Réponse d'intégration d'API Gateway

Aperçu

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.

Détecter les exceptions

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!)

Exceptions avec sens

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

Mapper les exceptions

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:

  1. 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"
      ]
    }
    
  2. Modifiez "Réponse de la méthode" et mappez le nouveau modèle à 400
  3. Ajouter une nouvelle réponse d'intégration
  4. Définissez le code sur 400
  5. Définissez l'expression régulière pour qu'elle corresponde aux types "ClientException" avec une tolérance pour les espaces: .*"type"\s*:\s*"ClientException".*
  6. 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"
    }
    
33