Ir para o conteúdo do rodapé
Iron Academy Logo
Aprenda C#
Aprenda C#

Outras categorias

Tratamento Global de Erros em APIs Mínimas em C#

[[academy-video-youtube({"vid": "B5NsgtdwOlg", "start_time": "0", "title": "Global Error Handling in C# Minimal APIs", "creator": "Tim Corey", "length": "13m 30s"})]]

Uma API web que lança uma exceção não tratada, por padrão, retornará o tipo de página de erro que ajuda um desenvolvedor a depurar localmente e ajuda um estranho a mapear sua pilha de chamadas. Números de linha, nomes de tipos, e o caminho para o arquivo fonte fluem de volta para quem fez a solicitação. Capturar cada erro no ponto onde ele pode acontecer é a abordagem correta, mas isso só funciona até o próximo try/catch esquecido. Um manipulador global é a rede de segurança que captura o que o ponto de extremidade perdeu.

Em seu vídeo "Tratamento Global de Erros em APIs Mínimas C#", Tim Corey constrói uma pequena API mínima com um ponto de extremidade deliberadamente quebrado, demonstra a página de erro do desenvolvedor que é retornada sem proteção e, em seguida, conecta app.UseExceptionHandler para interceptar qualquer exceção não capturada e responder com um genérico 500. Ele também reforça por que o tratamento no nível do ponto de extremidade continua sendo o caminho preferido: o manipulador global é o recurso, não a estratégia. Qualquer pessoa que esteja enviando uma API mínima e queira ter certeza de que nenhum rastreamento de pilha jamais saia do servidor, encontrará abaixo a configuração do middleware e a lógica de design em torno dela.

Construindo uma API Mínima com um Endpoint Quebrado

[1:08 - 3:01] Tim começa com um novo projeto .NET 8 ASP.NET Core Web API chamado ErrorDemoApp. As opções do modelo de projeto permanecem próximas aos padrões: HTTPS ativado, OpenAPI ativado, sem autenticação, instruções de nível superior deixadas habilitadas, e a caixa de seleção de controladores desmarcada, já que esta é uma API mínima. O Program.cs gerado mantém o Swagger, mas o endpoint de exemplo de previsão do tempo e seu registro são excluídos para que o arquivo mostre apenas o básico.

No lugar do exemplo, ele adiciona um único endpoint em /demo cujo único propósito é falhar:

app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});
app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});

Executar o projeto com Ctrl+F5 (iniciar sem depuração) impede que o depurador do Visual Studio intercepte o lançamento, então a falha se manifesta da maneira que ocorreria para um chamador HTTP real. O Swagger é aberto, o endpoint /demo é o único disponível, e executá-lo retorna uma resposta 500. O corpo da resposta contém o tipo de exceção, a mensagem e uma referência à linha 18 do Program.cs.

Por que a Página de Erro Padrão Vaza Detalhes de Implementação

[3:01 - 5:00] Acessar /demo diretamente no navegador (sem o wrapper ?message= do Swagger) mostra a página de exceção do desenvolvedor em vez da resposta JSON. A página renderiza o nome da exceção, a mensagem, o caminho do arquivo e o número da linha onde o lançamento ocorreu, os detalhes brutos da exceção e os quadros de pilha acima do lançamento. Para um desenvolvedor trabalhando localmente, isso é ouro. Para qualquer outra pessoa, é um mapa gratuito da base de código.

O ponto de Tim é apresentado sem rodeios: essa página existe para ajudar os desenvolvedores e nunca deve chegar aos usuários finais. O fato de que às vezes chega é o motivo pelo qual um manipulador global é importante. Mesmo as equipes que diligentemente envolvem cada endpoint em tratamento de exceções eventualmente esquecem um, e o custo de esquecer um é o rastreamento completo da pilha indo para quem perguntou.

Capturando Erros Primeiramente no Endpoint

[5:00 - 6:30] Antes de instalar o manipulador global, Tim envolve o endpoint de demonstração em um try/catch para ilustrar o caminho preferido. O manipulador retorna Results.BadRequest(ex.Message) para qualquer exceção lançada:

app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});
app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});

O resultado é um 400 carregando apenas a string de mensagem. Nenhum rastreamento de pilha, nenhum caminho de arquivo, nenhum número de linha. Se a própria mensagem deve ser exposta depende da aplicação; para uma API pública, mesmo a mensagem pode vazar mais do que a equipe deseja, caso em que o manipulador substitui por uma string genérica. O catch local dá ao endpoint controle total sobre o que o chamador vê, incluindo a escolha de retornar um código de status mais específico do que 500 quando o modo de falha é realmente conhecido.

O que este padrão não pode fazer é capturar o que o ponto de extremidade se esqueceu de envolver. Qualquer novo caminho de código, qualquer relançamento de uma camada mais profunda, qualquer Task que lance em um thread separado, todos ignoram o try/catch do ponto de extremidade. Essa é a lacuna que o middleware preenche.

Conectando o Middleware UseExceptionHandler

[6:30 - 10:00] Logo abaixo da linha app.UseHttpsRedirection(), o manipulador é registrado com app.UseExceptionHandler. A sobrecarga que aceita uma ação de construtor expõe o pipeline subjacente, o que permite que o manipulador defina explicitamente a forma da resposta:

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});
app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});

Algumas escolhas nesse bloco importam. Forçar o código de status para 500 significa que o chamador não pode inferir nada do número de resposta; seja qual for o tipo de exceção interna, a superfície parece a mesma. Forçar o tipo de conteúdo para application/json combina com o resto das respostas da API, o que mantém os clientes em um único analisador. O IExceptionHandlerFeature expõe a exceção original para que um verdadeiro manipulador possa registrá-la; Tim usa Console.WriteLine aqui como um substituto para qualquer logger que o projeto realmente traria.

A chamada final WriteAsJsonAsync retorna um objeto anônimo com o código de status e a mensagem genérica. O corpo não diz nada sobre o que falhou além do fato de que algo falhou, que é o ponto. Diagnósticos internos pertencem ao log, não à resposta.

Testando os Caminhos Tratados e Não Tratados

[10:00 - 13:14] Com o try/catch ainda no lugar, o endpoint executa o caminho local: Swagger mostra um 400 carregando "Esta é uma exceção de demonstração". O middleware nunca vê o lançamento porque o bloco de catch o resolve primeiro. Este é o design que Tim quer por padrão: manipuladores locais fazem seu trabalho, e o manipulador global está dormente.

Removendo o try/catch e executando novamente exercita o fallback. A mesma solicitação agora retorna um 500 com o corpo JSON { "statusCode": 500, "message": "Internal Server Error" }. Nada na resposta revela onde a exceção foi lançada ou qual tipo era. A janela do console do Visual Studio, no entanto, mostra o texto da exceção original registrado através do placeholder Console.WriteLine, incluindo o caminho do arquivo e número da linha. O diagnóstico completo fica onde os desenvolvedores podem lê-lo; a resposta fica onde não pode vazar.

Este padrão se estende para uma API mínima com middleware de validação, autenticação personalizada ou qualquer outro componente de pipeline. O manipulador de exceções se senta cedo no pipeline e captura o que quer que se propague a partir de um estágio posterior.

Concluindo: Defesa em Profundidade

[13:14 - 13:30] O tratamento local e o tratamento global não são alternativas; são camadas. O manipulador local dá ao endpoint a chance de responder de maneira significativa quando o modo de falha é conhecido. O manipulador global garante que qualquer coisa que a camada local perdeu produza uma resposta que seja consistente, genérica e segura. APIs baseadas em controladores usam a mesma ideia com pequenos ajustes de sintaxe, mas a forma de API mínima é a que vale a pena se acostumar primeiro porque a área de superfície é pequena o suficiente para ver o quadro completo em um único arquivo.

Conclusão

[13:14 - 13:30] Configurar um manipulador de erro global em uma API mínima é feito em três etapas: registrar UseExceptionHandler cedo no pipeline, definir o status da resposta e o tipo de conteúdo dentro do manipulador, e escrever um corpo deliberadamente genérico para que nenhum detalhe de implementação escape. Combine isso com blocos try/catch locais em torno dos caminhos de código mais propensos a falhar, e você tem um modelo de defesa em profundidade onde o manipulador global é a rede de segurança em vez da estratégia.

Dica de Exemplo: Quando o manipulador chama um logger real, passe todo o objeto de exceção (não apenas a mensagem) para que o pipeline de registro estruturado capture o tipo, a pilha e quaisquer exceções internas. Um logger como o Serilog preservará tudo isso como propriedades consultáveis, o que significa que o alerta que dispara para um 500 em produção carrega contexto suficiente para reproduzir localmente sem ninguém executar novamente a solicitação.

Assista ao vídeo completo no canal do YouTube dele e obtenha mais insights sobre como construir APIs mínimas prontas para produção na série 10-Minute Training.

Hero Worlddot related to Tratamento Global de Erros em APIs Mínimas em C#
Hero Affiliate related to Tratamento Global de Erros em APIs Mínimas em C#

Ganhe mais compartilhando o que você ama.

Você cria conteúdo para desenvolvedores que trabalham com .NET, C#, Java, Python ou Node.js? Transforme sua expertise em renda extra!

Equipe de Suporte Iron

Estamos online 24 horas por dia, 5 dias por semana.
Bater papo
E-mail
Liga para mim