Manipulação de exceção da API da Web do ASP.NET Core

280

Estou usando o ASP.NET Core para meu novo projeto de API REST depois de usar a API da Web ASP.NET regular por muitos anos. Não vejo uma boa maneira de lidar com exceções na API da Web do ASP.NET Core. Tentei implementar o filtro / atributo de tratamento de exceções:

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

E aqui está o meu registro de filtro de inicialização:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

O problema que eu estava tendo é que, quando ocorre uma exceção no meu, AuthorizationFilterele não está sendo tratado ErrorHandlingFilter. Eu esperava que fosse pego lá, exatamente como funcionava com a antiga API da Web do ASP.NET.

Então, como posso capturar todas as exceções de aplicativos e de filtros de ação?

Andrei
fonte
3
Você já experimentou o UseExceptionHandlermiddleware?
Pawel
Eu tenho um exemplo aqui sobre como usar o UseExceptionHandlermiddleware
Ilya Chernomordik

Respostas:

539

Tratamento de exceções Middleware

Depois de muitas experiências com diferentes abordagens de manipulação de exceções, acabei usando o middleware. Funcionou melhor para o meu aplicativo ASP.NET Core Web API. Ele lida com exceções de aplicativos, além de filtros de ação, e eu tenho controle total sobre o tratamento de exceções e a resposta HTTP. Aqui está minha exceção ao lidar com o 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);
    }
}

Registre-o antes do MVC na Startupclasse:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Você pode adicionar rastreamento de pilha, nome do tipo de exceção, códigos de erro ou qualquer coisa que desejar. Muito flexível. Aqui está um exemplo de resposta de exceção:

{ "error": "Authentication token is not valid." }

Considere injetar IOptions<MvcJsonOptions>o Invokemétodo para usá-lo quando serializar o objeto de resposta para utilizar as configurações de serialização do ASP.NET MVC JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)para obter melhor consistência de serialização em todos os pontos de extremidade.

Abordagem 2

Existe outra API não óbvia chamada UseExceptionHandlerque funciona "ok" para cenários 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);
}));

Essa não é uma maneira muito óbvia, mas fácil de configurar o tratamento de exceções. No entanto, ainda prefiro a abordagem de middleware em vez de obter mais controle com capacidade de injetar as dependências necessárias.

Andrei
fonte
4
Eu tenho batido minha cabeça contra a mesa tentando fazer com que um middleware personalizado funcionasse hoje, e funciona basicamente da mesma maneira (estou usando-o para gerenciar a unidade de trabalho / transação de uma solicitação). O problema que estou enfrentando é que exceções levantadas no 'próximo' não são capturadas no middleware. Como você pode imaginar, isso é problemático. O que estou fazendo de errado / faltando? Alguma sugestão ou sugestão?
brappleye3
5
@ brappleye3 - Eu descobri qual era o problema. Eu estava apenas registrando o middleware no lugar errado na classe Startup.cs. Eu me mudeiapp.UseMiddleware<ErrorHandlingMiddleware>(); para pouco antes app.UseStaticFiles();. A exceção parece ter sido detectada corretamente agora. Isso me leva a acreditar em app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();fazer alguma invasão interna de middleware mágico para obter a ordem correta do middleware.
Jamadan
4
Concordo que o middleware personalizado pode ser muito útil, mas questionaria o uso de exceções para situações NotFound, Unauthorized e BadRequest. Por que não simplesmente definir o código de status (usando NotFound () etc.) e manipulá-lo em seu middleware personalizado ou via UseStatusCodePagesWithReExecute? Consulte devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api para mais informações
Paul Hiles
4
É ruim porque está sempre serializando para JSON, ignorando completamente a negociação de conteúdo.
Konrad
5
Ponto válido @Konrad. Por isso eu disse que este exemplo é onde você pode começar, e não o resultado final. Para 99% das APIs, o JSON é mais que suficiente. Se você acha que essa resposta não é boa o suficiente, sinta-se à vontade para contribuir.
Andrei
60

O mais recente Asp.Net Core(pelo menos da versão 2.2, provavelmente anterior) possui um middleware integrado que facilita um pouco a comparação com a implementação na resposta aceita:

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

Deve fazer o mesmo, apenas um pouco menos de código para escrever.

Importante: Lembre-se de adicioná-lo antes UseMvc(ou UseRoutingno .Net Core 3), pois a ordem é importante.

Ilya Chernomordik
fonte
Ele suporta DI como um argumento para o manipulador ou seria necessário usar um padrão de localizador de serviço dentro do manipulador?
lp
33

Sua melhor aposta é usar o middleware para obter o log que você está procurando. Você deseja colocar seu log de exceção em um middleware e, em seguida, manipular suas páginas de erro exibidas para o usuário em um middleware diferente. Isso permite a separação da lógica e segue o design que a Microsoft apresentou com os dois componentes de middleware. Aqui está um bom link para a documentação da Microsoft: Tratamento de erros no ASP.Net Core

Para seu exemplo específico, convém usar uma das extensões no middleware StatusCodePage ou criar o seu próprio dessa maneira .

Você pode encontrar um exemplo aqui para registrar exceções: 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();
}

Se você não gosta dessa implementação específica, também pode usar o ELM Middleware , e aqui estão alguns exemplos: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Se isso não funcionar para as suas necessidades, você sempre pode rolar o seu próprio componente Middleware observando as implementações do ExceptionHandlerMiddleware e ElmMiddleware para entender os conceitos de criação do seu.

É importante adicionar a exceção que trata do middleware abaixo do statusCodePages, mas acima de todos os outros componentes do middleware. Dessa forma, o middleware Exception capturará a exceção, registre-a e permita que a solicitação continue no middleware StatusCodePage, que exibirá a página de erro amigável para o usuário.

Ashley Lee
fonte
De nada. Também forneci um link para um exemplo para substituir o UseStatusPages padrão em casos extremos que podem atender melhor à sua solicitação.
21816 Ashley
1
Observe que o Elm não persiste nos logs e é recomendável usar o Serilog ou o NLog para fornecer a serialização. Veja os logs do ELM desaparecem. Podemos persistir em um arquivo ou banco de dados?
Michael Freidgeim
2
O link está quebrado agora.
Mathias Lykkegaard Lorenzen
@AshleyLee, duvido que UseStatusCodePagesseja útil nas implementações de serviços de API da Web. Não há pontos de vista ou HTML em tudo, apenas respostas JSON ...
Paul Michalik
23

A resposta bem aceita me ajudou muito, mas eu queria passar o HttpStatusCode no meu middleware para gerenciar o código de status do erro em tempo de execução.

De acordo com este link, tive uma idéia para fazer o mesmo. Então, mesclei a resposta Andrei com isso. Então, meu código final está abaixo:
1. Classe base

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Tipo de classe de exceção 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";
    }

}


3. Middleware de exceção personalizada

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


4. Método de Extensão

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Configure o método em startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Agora meu método de login no controlador de conta:

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

Acima, você pode ver se não encontrei o usuário e, em seguida, criei o HttpStatusCodeException no qual passei o status HttpStatusCode.NotFound e uma mensagem personalizada
no middleware

catch (HttpStatusCodeException ex)

será bloqueado e passará o controle para

Método privado Task HandleExceptionAsync (contexto HttpContext, exceção HttpStatusCodeException)

.


Mas e se eu receber um erro de execução antes? Para isso, usei o bloco try catch, que lança a exceção e será capturado no bloco catch (Exception exceptionObj) e passará o controle para

Tarefa HandleExceptionAsync (contexto HttpContext, exceção de exceção)

método.

Eu usei uma única classe ErrorDetails para uniformidade.

Arjun
fonte
Onde colocar o método de extensão? Infelizmente no startup.csno void Configure(IapplicationBuilder app)eu recebo um erro IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. E eu adicionei a referência, onde CustomExceptionMiddleware.csestá.
Spedo De La Rossa
você não deseja usar exceções, pois elas diminuem a velocidade das suas APIs. exceções são muito caras.
lnaie
@Inaie, não posso dizer sobre isso ... mas parece que você nunca tem qualquer excepção a alça para .. Grande trabalho
Arjun
19

Para configurar o comportamento de manipulação de exceções por tipo de exceção, você pode usar o Middleware dos pacotes NuGet:

Exemplo de código:

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();
}
Ihar Yakimush
fonte
16

Em primeiro lugar, graças a Andrei, pois baseiei minha solução no exemplo dele.

Estou incluindo o meu, pois é uma amostra mais completa e pode economizar tempo para os leitores.

A limitação da abordagem de Andrei é que não lida com o log, capturando variáveis ​​de solicitação potencialmente úteis e negociação de conteúdo (ele sempre retornará JSON, independentemente do que o cliente tenha solicitado - XML ​​/ texto sem formatação, etc.).

Minha abordagem é usar um ObjectResult que nos permita usar a funcionalidade inserida no MVC.

Esse código também impede o armazenamento em cache da resposta.

A resposta ao erro foi decorada de forma que possa ser serializada pelo serializador XML.

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;
        }
    }
}
CountZero
fonte
9

Primeiro, configure o ASP.NET Core 2 Startuppara executar novamente em uma página de erro os erros do servidor da Web e as exceções não tratadas.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Em seguida, defina um tipo de exceção que permita gerar erros nos códigos de status HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Por fim, em sua controladora para a página de erro, personalize a resposta com base no motivo do erro e se a resposta será vista diretamente pelo usuário final. Este código assume que todos os URLs da API começam com /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 });
}

O ASP.NET Core registrará os detalhes do erro para você depurar, portanto, um código de status pode ser tudo o que você deseja fornecer a um solicitante (potencialmente não confiável). Se você quiser mostrar mais informações, pode aprimorá HttpException-las para fornecê-las. Para erros de API, é possível colocar informações de erro codificadas em JSON no corpo da mensagem substituindo return StatusCode...por return Json....

Edward Brey
fonte
0

use middleware ou IExceptionHandlerPathFeature está bom. existe outra maneira de eshop

crie um filtro de exceção e registre-o

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
fonte