Gestione delle eccezioni dell'API Web ASP.NET Core


280

Sto usando ASP.NET Core per il mio nuovo progetto API REST dopo aver usato l'API Web ASP.NET normale per molti anni. Non vedo alcun buon modo per gestire le eccezioni nell'API Web ASP.NET Core. Ho provato a implementare il filtro / attributo di gestione delle eccezioni:

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

Ed ecco la mia registrazione del filtro di avvio:

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

Il problema che stavo riscontrando è che quando si verifica un'eccezione nel mio AuthorizationFilternon viene gestita da ErrorHandlingFilter. Mi aspettavo che venisse catturato lì proprio come funzionava con la vecchia API Web ASP.NET.

Quindi, come posso rilevare tutte le eccezioni dell'applicazione e anche eventuali eccezioni dai filtri di azione?


3
Hai provato il UseExceptionHandlermiddleware?
Pawel,

Ho un esempio qui su come utilizzare il UseExceptionHandlermiddleware
Ilya Chernomordik,

Risposte:


539

Middleware di gestione delle eccezioni

Dopo molti esperimenti con diversi approcci alla gestione delle eccezioni, ho finito per usare il middleware. Ha funzionato al meglio per la mia applicazione API Web ASP.NET Core. Gestisce le eccezioni dell'applicazione e le eccezioni dai filtri di azione e ho il pieno controllo sulla gestione delle eccezioni e sulla risposta HTTP. Ecco il mio middleware per la gestione delle eccezioni:

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

Registralo prima di MVC in Startupclasse:

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

È possibile aggiungere la traccia dello stack, il nome del tipo di eccezione, i codici di errore o tutto ciò che si desidera. Molto flessibile. Ecco un esempio di risposta alle eccezioni:

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

Prendere in considerazione l'iniezione IOptions<MvcJsonOptions>al Invokemetodo per utilizzarlo quando si serializza l'oggetto response per utilizzare le impostazioni di serializzazione di ASP.NET MVC JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)per una migliore coerenza della serializzazione su tutti gli endpoint.

Approccio 2

Esiste un'altra API non ovvia chiamata UseExceptionHandlerche funziona "ok" per scenari semplici:

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

Questo non è un modo molto ovvio ma semplice per impostare la gestione delle eccezioni. Tuttavia preferisco ancora l'approccio middleware rispetto ad esso poiché ottengo un maggiore controllo con la capacità di iniettare le dipendenze necessarie.


4
Ho battuto la testa contro la scrivania cercando di far funzionare un middleware personalizzato oggi, e funziona sostanzialmente allo stesso modo (lo sto usando per gestire l'unità di lavoro / transazione per una richiesta). Il problema che sto affrontando è che le eccezioni sollevate in "next" non vengono rilevate nel middleware. Come puoi immaginare, questo è problematico. Cosa sto facendo di sbagliato / mancante? Qualche puntatore o suggerimento?
brappleye3,

5
@ brappleye3 - Ho capito quale fosse il problema. Stavo solo registrando il middleware nel posto sbagliato nella classe Startup.cs. Mi sono trasferito app.UseMiddleware<ErrorHandlingMiddleware>();poco prima app.UseStaticFiles();. L'eccezione sembra essere stata rilevata correttamente ora. Questo mi porta a credere di app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();fare qualche hackware di middleware magico interno per ottenere l'ordinamento corretto del middleware.
Jamadan,

4
Concordo sul fatto che il middleware personalizzato può essere molto utile ma metterebbe in dubbio l'utilizzo delle eccezioni per le situazioni NotFound, Non autorizzato e BadRequest. Perché non semplicemente impostare il codice di stato (utilizzando NotFound () ecc.) E quindi gestirlo nel middleware personalizzato o tramite UseStatusCodePagesWithReExecute? Vedi devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api per maggiori informazioni
Paul Hiles,

4
È negativo perché si serializza sempre su JSON, ignorando completamente la negoziazione dei contenuti.
Konrad,

5
@Konrad punto valido. Ecco perché ho detto che questo esempio è dove puoi iniziare e non il risultato finale. Per il 99% delle API JSON è più che sufficiente. Se ritieni che questa risposta non sia abbastanza valida, sentiti libero di contribuire.
Andrei,

61

La versione più recente Asp.Net Core(almeno dalla 2.2, probabilmente precedente) ha un middleware integrato che lo rende un po 'più semplice rispetto all'implementazione nella risposta accettata:

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

Dovrebbe fare praticamente lo stesso, solo un po 'meno di codice da scrivere.

Importante: ricordati di aggiungerlo prima UseMvc(o UseRoutingin .Net Core 3) poiché l'ordine è importante.


Supporta DI come arg per il gestore o si dovrebbe usare un modello di localizzazione di servizio all'interno del gestore?
lp

33

La soluzione migliore è utilizzare il middleware per ottenere la registrazione che stai cercando. Si desidera inserire la registrazione delle eccezioni in un middleware e quindi gestire le pagine di errore visualizzate all'utente in un middleware diverso. Ciò consente la separazione della logica e segue il design che Microsoft ha predisposto con i 2 componenti del middleware. Ecco un buon collegamento alla documentazione di Microsoft: Gestione degli errori in ASP.Net Core

Per il tuo esempio specifico, potresti voler utilizzare una delle estensioni nel middleware StatusCodePage o creare il tuo like questa .

È possibile trovare un esempio qui per la registrazione delle eccezioni: 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 non ti piace quella specifica implementazione, puoi anche usare ELM Middleware , e qui ci sono alcuni esempi: Elm Exception Middleware

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

Se ciò non funziona in base alle tue esigenze, puoi sempre creare il tuo componente Middleware osservando le loro implementazioni di ExceptionHandlerMiddleware e ElmMiddleware per comprendere i concetti per crearne uno tuo.

È importante aggiungere l'eccezione che gestisce il middleware sotto il middleware StatusCodePages ma soprattutto gli altri componenti del middleware. In questo modo il middleware di eccezione acquisirà l'eccezione, la registrerà, quindi consentirà alla richiesta di passare al middleware StatusCodePage che visualizzerà la pagina di errore descrittiva per l'utente.


Prego. Ho anche fornito un collegamento a un esempio per l'override delle UseStatusPages predefinite sui casi limite che potrebbero soddisfare meglio la tua richiesta.
Ashley Lee,

1
Si noti che Elm non persiste nei registri ed è consigliabile utilizzare Serilog o NLog per fornire la serializzazione. Vedi i log ELM scompare. Possiamo persistere in un file o DB?
Michael Freidgeim,

2
Il collegamento ora è interrotto.
Mathias Lykkegaard Lorenzen

@AshleyLee, dubito che UseStatusCodePagessia utile nelle implementazioni del servizio API Web. Nessuna visualizzazione o HTML, solo risposte JSON ...
Paul Michalik,

23

La risposta ben accettata mi ha aiutato molto, ma volevo passare HttpStatusCode nel mio middleware per gestire il codice di stato dell'errore in fase di esecuzione.

Secondo questo link ho avuto l'idea di fare lo stesso. Quindi ho unito la risposta Andrei a questo. Quindi il mio codice finale è di seguito:
1. Classe di base

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

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

2. Tipo di classe di eccezione personalizzata

 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 di eccezione personalizzato

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. Metodo di estensione

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

5. Configurare il metodo in startup.cs

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

Ora il mio metodo di accesso in Controller account:

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

Sopra puoi vedere se non ho trovato l'utente quindi sollevare lo stato HttpStatusCodeException in cui ho passato lo stato HttpStatusCode.NotFound e un messaggio personalizzato
nel middleware

catch (HttpStatusCodeException ex)

verrà chiamato il blocco che passerà il controllo a

Attività privata HandleExceptionAsync (contesto HttpContext, eccezione HttpStatusCodeException)

.


E se avessi ricevuto un errore di runtime prima? Per questo ho usato provare a catturare il blocco che genera un'eccezione e verrà catturato nel blocco catch (Exception exceptionObj) e passerà il controllo a

Attività HandleExceptionAsync (contesto HttpContext, eccezione eccezione)

metodo.

Ho usato una singola classe ErrorDetails per l'uniformità.


Dove mettere il metodo di estensione? Sfortunatamente nel startup.csin void Configure(IapplicationBuilder app)ottengo un errore IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. E ho aggiunto il riferimento, dov'è CustomExceptionMiddleware.cs.
Spedo De La Rossa,

non vuoi usare eccezioni perché rallentano le tue API. le eccezioni sono molto costose.
lnaie,

@Inaie, Non posso dirlo ... ma sembra che tu non abbia mai avuto eccezioni da gestire. Ottimo lavoro
Arjun,

19

Per configurare il comportamento di gestione delle eccezioni per tipo di eccezione è possibile utilizzare Middleware dai pacchetti NuGet:

Esempio di codice:

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

16

Innanzitutto, grazie ad Andrei, poiché ho basato la mia soluzione sul suo esempio.

Includo il mio in quanto è un campione più completo e potrebbe far risparmiare tempo ai lettori.

La limitazione dell'approccio di Andrei è che non gestisce la registrazione, l'acquisizione di variabili di richiesta potenzialmente utili e la negoziazione del contenuto (restituirà sempre JSON indipendentemente da ciò che il client ha richiesto - XML ​​/ testo normale ecc.).

Il mio approccio è utilizzare un ObjectResult che ci consente di utilizzare la funzionalità inserita in MVC.

Questo codice impedisce anche la memorizzazione nella cache della risposta.

La risposta all'errore è stata decorata in modo tale da poter essere serializzata dal serializzatore 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;
        }
    }
}

9

Innanzitutto, configurare ASP.NET Core 2 Startupper eseguire nuovamente una pagina di errore per eventuali errori dal server Web ed eventuali eccezioni non gestite.

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

Successivamente, definisci un tipo di eccezione che ti consentirà di generare errori con i codici di stato HTTP.

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

Infine, nel controller per la pagina di errore, personalizzare la risposta in base al motivo dell'errore e se la risposta verrà visualizzata direttamente da un utente finale. Questo codice presuppone che tutti gli URL API inizino 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 registrerà i dettagli dell'errore con cui eseguire il debug, quindi un codice di stato potrebbe essere tutto ciò che si desidera fornire a un richiedente (potenzialmente non attendibile). Se vuoi mostrare più informazioni, puoi migliorare HttpExceptionper fornirle. Per gli errori API, è possibile inserire informazioni sull'errore con codifica JSON nel corpo del messaggio sostituendolo return StatusCode...con return Json....


0

usare middleware o IExceptionHandlerPathFeature va bene. c'è un altro modo in eshop

creare un filtro eccezioni e registrarlo

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.