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");
}
}