DEV Community

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

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

oieduardorabelo profile image Eduardo Rabelo ・7 min read

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

Discussion (2)

pic
Editor guide
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 Author

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