c# - tag - try catch web api
Manejo de excepciones de API web ASP.NET Core (7)
Comencé a usar ASP.NET Core para mi nuevo proyecto API REST después de usar ASP.NET Web API durante muchos años. No veo una buena manera de manejar las excepciones en ASP.NET Core Web API. Traté de implementar el filtro / atributo de manejo de excepciones:
public class ErrorHandlingFilter : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
HandleExceptionAsync(context);
context.ExceptionHandled = true;
}
private static void HandleExceptionAsync(ExceptionContext context)
{
var exception = context.Exception;
if (exception is MyNotFoundException)
SetExceptionResult(context, exception, HttpStatusCode.NotFound);
else if (exception is MyUnauthorizedException)
SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
else if (exception is MyException)
SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
else
SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
}
private static void SetExceptionResult(
ExceptionContext context,
Exception exception,
HttpStatusCode code)
{
context.Result = new JsonResult(new ApiResponse(exception))
{
StatusCode = (int)code
};
}
}
Y aquí está mi registro de filtro de inicio:
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizationFilter());
options.Filters.Add(new ErrorHandlingFilter());
});
El problema que tenía es que, cuando se produce una excepción en mi
AuthorizationFilter
ErrorHandlingFilter no lo está manejando.
Esperaba que fuera atrapado allí al igual que funcionaba con la antigua API web ASP.NET.
Entonces, ¿cómo puedo detectar todas las excepciones de la aplicación, así como cualquier excepción de los filtros de acción?
Middleware de manejo de excepciones
Después de muchos experimentos con diferentes enfoques de manejo de excepciones, terminé usando middleware. Funcionó mejor para mi aplicación ASP.NET Core Web API. Maneja las excepciones de la aplicación, así como las excepciones de los filtros de acción y tengo control total sobre el manejo de excepciones y la respuesta HTTP. Aquí está mi excepción en el manejo de middleware:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
if (ex is MyNotFoundException) code = HttpStatusCode.NotFound;
else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
else if (ex is MyException) code = HttpStatusCode.BadRequest;
var result = JsonConvert.SerializeObject(new { error = ex.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
Regístrese
antes de MVC
en la clase de
Startup
:
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();
Puede agregar seguimiento de pila, nombre de tipo de excepción, códigos de error o cualquier cosa que desee. Muy flexible. Aquí hay un ejemplo de respuesta de excepción:
{ "error": "Authentication token is not valid." }
Considere inyectar
IOptions<MvcJsonOptions>
al método
Invoke
para luego usarlo cuando serialice el objeto de respuesta para utilizar la configuración de serialización de ASP.NET MVC en
JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)
para una mejor consistencia de serialización en todos los puntos finales.
Enfoque 2
Hay otra API no obvia llamada
UseExceptionHandler
que funciona "bien" para situaciones simples:
app.UseExceptionHandler(a => a.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = feature.Error;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}));
Esta no es una manera muy obvia pero fácil de configurar el manejo de excepciones. Sin embargo, todavía prefiero el enfoque de middleware sobre él, ya que obtengo más control con la capacidad de inyectar las dependencias necesarias.
El último
Asp.Net Core
(al menos desde 2.2, probablemente anterior) tiene un middleware incorporado que lo hace un poco más fácil en comparación con la implementación en la respuesta aceptada:
app.UseExceptionHandler(a => a.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature.Error;
var result = JsonConvert.SerializeObject(new { error = exception.Message });
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}));
Debería hacer más o menos lo mismo, solo un poco menos de código para escribir.
Recuerde agregarlo antes de
UseMvc
ya que el orden es importante.
En primer lugar, gracias a Andrei porque he basado mi solución en su ejemplo.
Incluyo el mío ya que es una muestra más completa y podría ahorrarle algo de tiempo a los lectores.
La limitación del enfoque de Andrei es que no maneja el registro, capturando variables de solicitud potencialmente útiles y negociación de contenido (siempre devolverá JSON sin importar lo que el cliente haya solicitado: XML / texto plano, etc.).
Mi enfoque es usar un ObjectResult que nos permite usar la funcionalidad integrada en MVC.
Este código también evita el almacenamiento en caché de la respuesta.
La respuesta de error se ha decorado de tal manera que el serializador XML puede serializarla.
try
{
IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
if (Result != null)//User Found
return Result;
else// Not Found
throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
}
catch (Exception ex)
{
throw ex;
}
La respuesta bien aceptada me ayudó mucho, pero quería pasar HttpStatusCode en mi middleware para administrar el código de estado de error en tiempo de ejecución.
Según
este enlace,
tengo una idea para hacer lo mismo.
Así que fusioné la respuesta Andrei con esto.
Entonces mi código final está a continuación:
1. Clase base
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next;
private readonly IActionResultExecutor<ObjectResult> executor;
private readonly ILogger logger;
private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();
public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
{
this.next = next;
this.executor = executor;
logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));
if (context.Response.HasStarted)
{
throw;
}
var routeData = context.GetRouteData() ?? new RouteData();
ClearCacheHeaders(context.Response);
var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);
var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
{
StatusCode = (int) HttpStatusCode.InternalServerError,
};
await executor.ExecuteAsync(actionContext, result);
}
}
private static string GetRequestData(HttpContext context)
{
var sb = new StringBuilder();
if (context.Request.HasFormContentType && context.Request.Form.Any())
{
sb.Append("Form variables:");
foreach (var x in context.Request.Form)
{
sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
}
}
sb.AppendLine("Method: " + context.Request.Method);
return sb.ToString();
}
private static void ClearCacheHeaders(HttpResponse response)
{
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
}
[DataContract(Name= "ErrorResponse")]
public class ErrorResponse
{
[DataMember(Name = "Message")]
public string Message { get; set; }
public ErrorResponse(string message)
{
Message = message;
}
}
}
2. Tipo de clase de excepción personalizada
public class ErrorDetails
{
public int StatusCode { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
3. Middleware de excepción personalizada
public class HttpStatusCodeException : Exception
{
public HttpStatusCode StatusCode { get; set; }
public string ContentType { get; set; } = @"text/plain";
public HttpStatusCodeException(HttpStatusCode statusCode)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }
public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
{
this.ContentType = @"application/json";
}
}
4. Método de extensión
public class CustomExceptionMiddleware
{
private readonly RequestDelegate next;
public CustomExceptionMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (HttpStatusCodeException ex)
{
await HandleExceptionAsync(context, ex);
}
catch (Exception exceptionObj)
{
await HandleExceptionAsync(context, exceptionObj);
}
}
private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
{
string result = null;
context.Response.ContentType = "application/json";
if (exception is HttpStatusCodeException)
{
result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
context.Response.StatusCode = (int)exception.StatusCode;
}
else
{
result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
return context.Response.WriteAsync(result);
}
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return context.Response.WriteAsync(result);
}
}
5. Configure Method en startup.cs
public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<CustomExceptionMiddleware>();
}
Ahora mi método de inicio de sesión en el controlador de cuenta:
app.ConfigureCustomExceptionMiddleware();
app.UseMvc();
Arriba puede ver si no he encontrado al usuario y luego levantar la HttpStatusCodeException en la que he pasado el estado HttpStatusCode.NotFound y un mensaje personalizado
En middleware
catch (HttpStatusCodeException ex)
bloqueado se llamará que pasará el control a
Método privado Task HandleExceptionAsync (contexto HttpContext, excepción HttpStatusCodeException)
.
Pero, ¿qué pasa si recibí un error de tiempo de ejecución antes?
Para eso he usado el bloque try catch que lanza una excepción y quedaré atrapado en el bloque catch (Exception exceptionObj) y pasaré el control a
Task HandleExceptionAsync (contexto HttpContext, excepción de excepción)
método.
He usado una sola clase ErrorDetails para uniformidad.
Para configurar el comportamiento de manejo de excepciones por tipo de excepción, puede usar Middleware de paquetes NuGet:
-
Community.AspNetCore.ExceptionHandling.NewtonsoftJson
para
ASP.NET Core 2.0
-
Community.AspNetCore.ExceptionHandling.Mvc
para
ASP.NET Core 2.1+
.
Código de muestra:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddExceptionHandlingPolicies(options =>
{
options.For<InitializationException>().Rethrow();
options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();
options.For<SomeBadRequestException>()
.Response(e => 400)
.Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
.WithBody((req,sw, exception) =>
{
byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
return sw.WriteAsync(array, 0, array.Length);
})
.NextPolicy();
// Ensure that all exception types are handled by adding handler for generic exception at the end.
options.For<Exception>()
.Log(lo =>
{
lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
lo.Category = (context, exception) => "MyCategory";
})
.Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
.ClearCacheHeaders()
.WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
.Handled();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandlingPolicies();
app.UseMvc();
}
Primero, configure el
Startup
ASP.NET Core 2 para volver a ejecutar en una página de error cualquier error del servidor web y cualquier excepción no controlada.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment()) {
// Debug config here...
} else {
app.UseStatusCodePagesWithReExecute("/Error");
app.UseExceptionHandler("/Error");
}
// More config...
}
A continuación, defina un tipo de excepción que le permitirá generar errores con códigos de estado HTTP.
public class HttpException : Exception
{
public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
public HttpStatusCode StatusCode { get; private set; }
}
Finalmente, en su controlador para la página de error, personalice la respuesta según el motivo del error y si la respuesta será vista directamente por un usuario final.
Este código asume que todas las URL de API comienzan con
/api/
.
[AllowAnonymous]
public IActionResult Error()
{
// Gets the status code from the exception or web server.
var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;
// For API errors, responds with just the status code (no page).
if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
return StatusCode((int)statusCode);
// Creates a view model for a user-friendly error page.
string text = null;
switch (statusCode) {
case HttpStatusCode.NotFound: text = "Page not found."; break;
// Add more as desired.
}
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}
ASP.NET Core registrará los detalles del error para que pueda depurarlos, por lo que un código de estado puede ser todo lo que desea proporcionar a un solicitante (potencialmente no confiable).
Si desea mostrar más información, puede mejorar
HttpException
para proporcionarla.
Para errores de API, puede poner información de error codificada con JSON en el cuerpo del mensaje reemplazando
return StatusCode...
con
return Json...
Su mejor opción es utilizar Middleware para lograr el registro que está buscando. Desea colocar su registro de excepciones en un middleware y luego manejar sus páginas de error que se muestran al usuario en un middleware diferente. Eso permite la separación de la lógica y sigue el diseño que Microsoft ha presentado con los 2 componentes de middleware. Aquí hay un buen enlace a la documentación de Microsoft: Manejo de errores en ASP.Net Core
Para su ejemplo específico, es posible que desee utilizar una de las extensiones en el middleware StatusCodePage o enrollar la suya de this .
Puede encontrar un ejemplo aquí para registrar excepciones: ExceptionHandlerMiddleware.cs
public void Configure(IApplicationBuilder app)
{
// app.UseErrorPage(ErrorPageOptions.ShowAll);
// app.UseStatusCodePages();
// app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
// app.UseStatusCodePages("text/plain", "Response, status code: {0}");
// app.UseStatusCodePagesWithRedirects("~/errors/{0}");
// app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
// app.UseStatusCodePages(builder => builder.UseWelcomePage());
app.UseStatusCodePagesWithReExecute("/Errors/{0}"); // I use this version
// Exception handling logging below
app.UseExceptionHandler();
}
Si no le gusta esa implementación específica, también puede usar ELM Middleware , y aquí hay algunos ejemplos: Elm Exception Middleware
public void Configure(IApplicationBuilder app)
{
app.UseStatusCodePagesWithReExecute("/Errors/{0}");
// Exception handling logging below
app.UseElmCapture();
app.UseElmPage();
}
Si eso no funciona para sus necesidades, siempre puede rodar su propio componente de Middleware mirando sus implementaciones de ExceptionHandlerMiddleware y ElmMiddleware para comprender los conceptos para construir el suyo propio.
Es importante agregar el middleware de manejo de excepciones debajo del middleware StatusCodePages pero sobre todos sus otros componentes de middleware. De esa forma, su middleware Exception capturará la excepción, la registrará y luego permitirá que la solicitud continúe con el middleware StatusCodePage, que mostrará la página de error amigable al usuario.