Atualmente, estou aprendendo sobre gerenciamento de erros em projetos de API Web do ASP.NET Core para tentar encontrar uma solução elegante que garanta que um ProblemDetails
seja sempre retornado quando algo der errado.
Estou usando o WeatherForecast
modelo de API padrão que é criado ao criar um novo projeto de API Web em C#/ASP.NET Core 8 com controladores.
Implementei um manipulador de exceções personalizado; no entanto, estou descobrindo que ele nem sempre funciona.
Quando chamo a API usando Postman
para causar o erro, estou obtendo o ProblemDetails
código de status e as informações de cabeçalho corretos; no entanto, quando chamo a API a partir do recurso "tente" na Swagger
interface, estou obtendo o código de status errado (um erro interno 500) com detalhes de exceção em vez de um ProblemDetails
e as informações de cabeçalho erradas.
Analisando isso, descobri que na minha GlobalExceptionHandler
classe (que implementa IExceptionHandle
a interface), o ProblemDetailsService.TryWriteAsync(...)
método retorna false
se eu estiver tentando o código do Swagger; no entanto, quando executo exatamente o mesmo código chamando-o do Postman, o ProblemDetailsService.TryWriteAsync(...)
método retorna true
.
Coloquei um caso para quando o 'ProblemDetailsService.TryWriteAsync(...)' retornar falha para que eu pudesse tentar escrever diretamente ProblemDetailsContext
no httpContext.Response
.
Quando fiz isso, consegui capturar a seguinte exceção (novamente, isso só ocorre ao usar o recurso Try Code do Swagger... não ao chamar do Postman):
A serialização e desserialização de instâncias 'System.Type' não são suportadas. Caminho: $.HttpContext.Features.Key."
Não sei por que isso está falhando.
Estou procurando ajuda sobre como garantir que meu gerenciamento de erros se comporte da mesma maneira, independentemente de como a API é chamada.
Esta é a implementação para minha GlobalExceptionHandler
classe:
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly IProblemDetailsService _problemDetailsService;
public GlobalExceptionHandler(IProblemDetailsService problemDetailsService)
{
if (problemDetailsService == null) throw new ArgumentException(nameof(problemDetailsService));
_problemDetailsService = problemDetailsService;
}
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
var message = exception switch
{
ArgumentException => ((ArgumentException)exception).Message,
ValidationException => ((ValidationException)exception).Message,
_ => $"Internal Server Error ({exception.GetType().Name})"
};
int status = exception switch
{
ArgumentException => (int)HttpStatusCode.BadRequest,
ValidationException => (int)HttpStatusCode.BadRequest,
_ => (int)HttpStatusCode.InternalServerError
};
ProblemDetails problemDetails = new()
{
Title = $"Bad Request: {exception.GetType().Name}", // human-readable summary of the problem type
Detail = message, //detailed explanation specific to the problem
Status = status,
Instance = httpContext.Request.GetEncodedUrl(),
Type = exception.HelpLink
};
var errorcontext = new ProblemDetailsContext()
{
HttpContext = httpContext,
ProblemDetails = problemDetails,
//Exception = exception,
AdditionalMetadata = null
};
httpContext.Response.Clear();
httpContext.Response.StatusCode = status;
var written = await _problemDetailsService.TryWriteAsync(errorcontext);
if (!written)
{
try
{
await httpContext.Response.WriteAsJsonAsync<ProblemDetailsContext>(errorcontext);
written = true;
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
written = false;
}
}
return written;
}
}
Este é meu Program.cs
:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Net.Http;
using Microsoft.AspNetCore.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Add services to the container.
builder.Services.AddExceptionHandler<ProblemDetailsScratchPad.GlobalExceptionHandler>(); // Using a custom exception handler that converts the exception into a Problem Details
builder.Services.AddProblemDetails(); // required for using the exception handler with the custom problem details exception handler code because it adds the services required for Problem Details
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseExceptionHandler(opt => { }); //adding the custom GlobalExceptionHandler to the app's pipeline
app.UseAuthorization();
app.MapControllers();
app.Run();
E esta é a implementação para meu WeatherForecastController
(não que chamar o post ou get irá lançar a exceção a ser tratada pelo manipulador de exceção personalizado)
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet("GetWeatherForecast")]
public ActionResult<IEnumerable<object>> GetWeatherForecast()
{
throw new ArgumentException("Please provide a valid value for a week day: between 0 and 6");
}
[HttpPost("PostWeatherForecastDTO")]
public ActionResult<IEnumerable<object>> PostWeatherForecastDTO()
{
throw new Exception("Please provide a valid value for a week day: between 0 and 6");
}
}
Consegui reproduzir seu problema com o "Try it out/Execute" do Swagger. Ele usou o cabeçalho accept por padrão
plain/text
. O Postman usa o padrãoapplication/json
, então funciona lá. Você pode tentar definir o "media type"application/json
na interface do usuário do Swagger antes de enviar a solicitação. Ele produziu a resposta esperada para mim.A incompatibilidade do cabeçalho de aceitação também explicaria o motivo
ProblemDetailsService.TryWriteAsync
da falha: espera-se que ele produza JSON, não texto simples.Você pode dar uma olhada neste problema aberto no GitHub detalhando seu problema para soluções alternativas (se você não quiser apenas especificar application/json no Swagger)
O problema que você está enfrentando está relacionado a como o recurso "Try it" do Swagger interage com seu manipulador de exceção personalizado e o
ProblemDetailsService
. Especificamente, oProblemDetailsService.TryWriteAsync
método falha quando chamado do Swagger porque o middleware do Swagger pode estar interferindo no processo de serialização, particularmente ao lidar com certas propriedades comoHttpContext
.Aqui está uma abordagem passo a passo para garantir um tratamento de erros consistente em todos os clientes (Postman, Swagger, etc.):
1. Simplifique o manipulador de exceções
Em vez de depender de
ProblemDetailsService.TryWriteAsync
, você pode escrever aProblemDetails
resposta diretamente para oHttpContext
. Isso garante que a resposta seja sempre consistente, independentemente do cliente.Atualize seu
GlobalExceptionHandler
da seguinte forma:2. Certifique-se de que seu
Program.cs
está configurado corretamente para usar o manipulador de exceção personalizado eProblemDetails