DEV Community

Cover image for Serverless Framework: Tratamento de erros no AWS Lambda e Amazon API Gateway
Eduardo Rabelo
Eduardo Rabelo

Posted on

Serverless Framework: Tratamento de erros no AWS Lambda e Amazon API Gateway

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 está uma bagunça e passa para o Serverless Framework, 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:

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:

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:

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; 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:

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

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. 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. 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:

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

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

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 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:

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

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:

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

E em 500-response-template.yml:

'{
  "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). 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

Top comments (2)

Collapse
 
caiquecastro profile image
Caíque de Castro Soares da Silva

What does integration: lambda does on serverless.yml?

Collapse
 
oieduardorabelo profile image
Eduardo Rabelo

olá Caíque, obrigado pela leitura,

a diferença principal é o payload passado pelo API Gateway para sua Lambda

usando integration: lambda temos o seguinte cenário:

ao usar integration: lambda-proxy (que é o default ao usar Serverless Framework), temos o seguinte cenário:

eu explico mais sobre isso nesse outro artigo:

dev.to/oieduardorabelo/aws-serverl...

e você pode verificar a documentação do framework também:

serverless.com/framework/docs/prov...