Serverless Framework: Tratamento de erros no AWS Lambda e Amazon API Gateway
Tags: ptbr, serverless, aws, webdev
Published: 2020-05-15 11:03:45

As ofertas da AWS Lambda e Amazon API Gateway forneceram um novo mecanismo poderoso para o desenvolvimento rápido de APIs REST sem a sobrecarga de criação de infraestrutura e código *boilerplate* para ativar servidores web. Quando você percebe que o [SAM](https://aws.amazon.com/serverless/sam/) está uma bagunça e passa para o [Serverless Framework](https://serverless.com/), as coisas realmente começam a voar. Inevitavelmente, você chegará ao ponto em que essa prova de conceito precisa começar a se tornar mais real. Isso significa adicionar funcionalidades como autenticação e tratamento/relatório de erros. Depois de vasculhar a web e achar os resultados insatisfatórios, decidi publicar minha experiência aqui na esperança de que outros possam se beneficiar.

**Objetivo: fornecer códigos de status e mensagens apropriadas para os consumidores da API REST, como a interface do usuário em Angular, ao mesmo tempo em que fornecemos uma intrusão mínima dos códigos de status HTTP no próprio código Lambda.**

## Etapa 1: Integração Lambda Padrão

O Serverless Framework faz um ótimo trabalho na criação de recursos com padrões razoáveis, removendo a necessidade de código boilerplate, para que o desenvolvedor (ou engenheiro DevOps) possa se concentrar em "coisas mais importantes". Para fazer sentido, a primeira coisa que precisamos fazer é tentar entender o que o Serverless Framework oferece por padrão.

Usando a Integração Lambda, criamos um *endpoint* simples em nosso `serverless.yml`:

```yml
createDeal:
  handler: create-deal/index.handler
  events:
    - http:
        path: create-deal
        integration: lambda
        method: post
        cors: true
```

Quando fazemos o deploy na AWS, vemos o seguinte no Amazon API Gateway:

![](https://miro.medium.com/max/1400/1*-Neuq8XB1cMGgzbd0fcONw.png)

Serverless Framework, por padrão, criou várias expressões regulares que são mapeadas para códigos de erro. Com isso, precisamos garantir que os erros lançados em nosso código incluam o código de status apropriado com a string de erro esperada. Vejamos um exemplo:

```js
exports.handler = async (event) => {
    if (!event.dealName) {
        throw new Error('[400] Missing required property');
    }

    return createDeal(event);
};
```

Fácil né?

Talvez não. O que acontece se recebermos um erro em `createDeal()` que não esteja em conformidade com este padrão? Não podemos confiar em todos os métodos e bibliotecas chamados para saber se as *strings* de erro precisam conter um código de status HTTP. Certamente, podemos executar um `try..catch`, mas isso ainda coloca a lógica de determinar o código de status HTTP em nosso código dentro do *endpoint*. Além disso, qualquer consumidor da resposta precisará usar ou analisar o código de status da mensagem de erro. Por fim, se um erro for gerado sem um dos códigos de status mapeados, o API Gateway retornará uma resposta `504 Bad Gateway`, que é de pouco valor para o consumidor. Nós podemos fazer melhor.

## Etapa 2: Integração Proxy Lambda

Uma recomendação comum ao criar Lambdas é usar a "integração de proxy". O `serverless-stack` [tem uma excelente descrição de como fazer isso](https://serverless-stack.com/chapters/add-a-create-note-api.html); portanto, tentarei não ser repetitivo. O desenvolvedor é responsável por definir o código de status HTTP ao criar uma resposta e é responsável por capturar quaisquer erros no código, para que uma resposta apropriada possa ser criada. Vamos dar uma olhada nos exemplos abaixos:

```yml
createDeal:
  handler: create-deal/index.handler
  events:
    - http:
        path: create-deal
        method: post
        cors: true
```

Observe que, em vez de usar a integração lambda, estamos usando a integração padrão (proxy).

```js
exports.handler = async (event) => {
    const dealInfo = JSON.parse(event.body);
    if (!dealInfo.dealName) {
        return {
            statusCode: 400,
            headers: {
                'Content-Type': 'application/json', 
                'Access-Control-Allow-Origin': '*'
            },
            body: {
                message: 'Missing required property'
            }
        }
    }
    const newDeal = await createDeal(dealInfo);
    return {
        statusCode: 200,
        headers: {
           'Content-Type': 'application/json', 
           'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify(newDeal)
    }
};
```

Essa abordagem tem seus prós e contras. Como a solução anterior, o desenvolvedor é responsável por capturar todos os erros e retornar o código e a mensagem de status apropriados. Também como a solução anterior, obteremos um `504 Bad Gateway` se a resposta não estiver no formato correto, como quando um erro não detectado é gerado.

A solução no `serverless-stack` inclui uma função utilitária para simplificar a resposta `return buildResponse(statusCode, body)`. Isso ajuda parte do código que é comum em torno da resposta, como informações do cabeçalho, mas não aborda as preocupações de erros não capturados e mapeamentos de código de status na função lambda.

## Etapa 3: Middleware com Middy

Uma abordagem popular para abstrair muito do clichê de códigos Lambda é usar uma solução de middleware como o [middy](https://github.com/middyjs/middy). Middy envolve a chamada lambda e permite ao desenvolvedor anexar qualquer número de código em pré e pós-execução, que possa ser usado para modificar a solicitação ou resposta. Isso não soluciona o problema de declarar códigos de status HTTP na função lambda, mas fornece alguns recursos poderosos para integração com outras bibliotecas comuns, como o [http-errors](https://github.com/jshttp/http-errors). Infelizmente, o ganho líquido para este caso de uso não é melhor do que as soluções mais leves acima. Acabamos com um resultado semelhante com mais dependências e código adicional:

```yaml
createDeal:
  handler: create-deal/index.handler
  events:
    - http:
        path: create-deal
        method: post
        cors: true
```

Novamente estamos usando a integração padrão (proxy).

```js
const middy = require('middy');
const {httpErrorHandler, cors} = require('middy/middlewares');
const createError = require('http-errors');
const {autoProxyResponse} = require('middy-autoproxyresponse');

// envolve o manipulador da lambda no middleware
const handler = middy(async (event) => {
    const dealInfo = JSON.parse(event.body);
    if (!dealInfo.dealName) {
        throw new createError.BadRequest({message: 'Missing required property'});
    }
    return createDeal(dealInfo);
});

// declaramos quais middlewares usar
handler
    .use(autoProxyResponse())
    .use(httpErrorHandler())
    .use(cors());

module.exports = {handler};
```

O middleware CORS é uma adição bem-vinda, pois permite ignorar a entrada manual do cabeçalho `'Access-Control-Allow-Origin'`, mas precisamos adicionar o middleware de terceiros [auto-proxy-response](https://www.npmjs.com/package/middy-autoproxyresponse) para agrupar o valor retornado em um formato de resposta lambda válido (com código de status 200) ou criar manualmente o objeto de resposta.

## Etapa 4 - Final: Mapeando códigos de erro no API Gateway

Após muita discussão sobre usar a resposta do proxy (como recomendado pelo Serverless Framework e pela AWS), cheguei a uma conclusão que já vi repetidamente na web: tornar o desenvolvimento de curto prazo mais rápido, mas não fornece a separação que eu esperaria da resposta HTTP do código lambda. Essa separação pode ser usada para simplificar a vida dos desenvolvedores, dificultando o tiro no pé por não conseguir detectar um erro e ao mesmo tempo reduzir *boilerplate* para tornar o código mais legível como uma unidade de trabalho. O que isso adiciona é uma configuração extra no Serverless Framework na forma de **modelos de resposta** e **regex de códigos de erros**.

Código lambda:

```js
exports.handler = async (event) => {
    if (!event.dealName) {
        throw new Error('Missing required property');
    }

    return createDeal(event);
};
```

Retornamos a um código lambda simples em um mundo onde os códigos de status HTTP são alegremente desconhecidos. Se `createDeal()` retornar um erro que não seja capturado e reconvertido, não interromperemos o tratamento pelo API Gateway.

No `serverless.yml`

```yaml
createDeal:
  handler: create-deal/index.handler
  events:
    - http:
        path: create-deal
        method: post
        cors: true
  response:
    headers:
      Content-Type: "'application/json'"
    statusCodes:
      200:
        pattern: ''
      400:
        pattern: '^Missing.*'
        template: ${file(resources/error-response-template.yml)}
      500:
        pattern: '^(?!Missing).*'
        template: ${file(resources/500-response-template.yml)}
```

Nosso `error-response-template.yml`:

```yaml
'{
  "message": $input.json("$.errorMessage")
}'
```

E em `500-response-template.yml`:

```yaml
'{
  "message": "Internal Server Error"
}'
```

Toda a manipulação do código de status HTTP foi abstraída na configuração do API Gateway. O API Gateway aplicará o regex pattern no campo `errorMessage` da resposta para determinar qual código de status e modelo de resposta usar ([consulte a documentação para mais detalhes](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-method-settings-execution-console.html)). Nesse caso, configurei 3 códigos de status possíveis:

- 200 - se não houver mensagem de erro
- 400 - se a mensagem de erro começar com a sequência `Missing`
- 500 - se a mensagem de erro iniciar com qualquer outra sequência (*catch-all*)

Eles funcionarão além dos códigos de status 401 e 403 existentes que o API Gateway retornará quando a autenticação ou autorização falhar usando um autorizador personalizado (tópico para outro artigo).

Além disso, criei modelos de resposta padrão que podem ser reutilizados, reduzindo o copiar e colar, padronizando o formato de retorno.

No mais, acredito que isso fornece a experiência mais simples para o desenvolvedor, maximizando o comportamento consistente e o formato de resposta. O desenvolvedor não precisa se preocupar com códigos de status HTTP ao escrever funções lambda, e os possíveis códigos de status que podem ser retornados são documentados em um único local, e não em todo o código. Há algum custo e complexidade em executar a regex na mensagem de erro, mas para esse caso de uso é uma troca aceitável.

Eu espero que você tenha achado útil essa explicação!

# Créditos

- [Error Handling in AWS Lambda and API Gateway](https://medium.com/@benarena/error-handling-in-aws-lambda-and-api-gateway-35bf38fa6e83), escrito originalmente por [Ben Arena](https://www.linkedin.com/in/benjamen-arena/).