Condividi tramite


Gestione degli errori globali nell'API Web ASP.NET 2

di David Matson, Rick Anderson

Questo argomento offre una panoramica della gestione degli errori globali in ASP.NET API Web 2 per ASP.NET 4.x. Oggi non esiste un modo semplice nell'API Web per registrare o gestire gli errori a livello globale. Alcune eccezioni non gestite possono essere elaborate tramite filtri eccezioni, ma esistono diversi casi che i filtri eccezioni non possono gestire. Per esempio:

  1. Eccezioni generate dai costruttori dei controller.
  2. Eccezioni generate dai gestori di messaggi.
  3. Eccezioni generate durante il routing.
  4. Eccezioni generate durante la serializzazione del contenuto della risposta.

Si vuole fornire un modo semplice e coerente per registrare e gestire (ove possibile) queste eccezioni.

Esistono due casi principali per la gestione delle eccezioni, il caso in cui è possibile inviare una risposta di errore e il caso in cui è possibile registrare l'eccezione. Un esempio per il secondo caso è quando viene generata un'eccezione al centro del contenuto della risposta in streaming; in questo caso è troppo tardi per inviare un nuovo messaggio di risposta, dal momento che il codice di stato, le intestazioni e il contenuto parziale sono già passati attraverso la rete, quindi è sufficiente interrompere la connessione. Anche se l'eccezione non può essere gestita per generare un nuovo messaggio di risposta, è comunque supportata la registrazione dell'eccezione. Nei casi in cui è possibile rilevare un errore, è possibile restituire una risposta di errore appropriata, come illustrato di seguito:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

Opzioni esistenti

Oltre ai filtri delle eccezioni, i gestori di messaggi possono essere usati oggi per osservare tutte le risposte a 500 livelli, ma agire su tali risposte è difficile, in quanto non hanno contesto sull'errore originale. I gestori di messaggi presentano anche alcune delle stesse limitazioni dei filtri delle eccezioni relativi ai casi che possono gestire. Anche se l'API Web dispone di un'infrastruttura di traccia che acquisisce le condizioni di errore, l'infrastruttura di traccia è destinata alla diagnostica e non è progettata o adatta per l'esecuzione in ambienti di produzione. La gestione e la registrazione globali delle eccezioni devono essere servizi che possono essere eseguiti durante la produzione e collegati a soluzioni di monitoraggio esistenti, ad esempio ELMAH.

Panoramica della soluzione

Sono disponibili due nuovi servizi sostituibili dall'utente, IExceptionLogger e IExceptionHandler , per registrare e gestire eccezioni non gestite. I servizi sono molto simili, con due differenze principali:

  1. È supportata la registrazione di più logger di eccezioni, ma solo un singolo gestore di eccezioni.
  2. I logger di eccezioni vengono sempre chiamati, anche se stiamo per interrompere la connessione. I gestori di eccezioni vengono chiamati solo quando siamo ancora in grado di scegliere il messaggio di risposta da inviare.

Entrambi i servizi forniscono l'accesso a un contesto di eccezione contenente informazioni rilevanti dal punto in cui è stata rilevata l'eccezione, in particolare HttpRequestMessage, HttpRequestContext, l'eccezione generata e l'origine dell'eccezione (dettagli di seguito).

Principi di progettazione

  1. Nessun cambiamento critico Poiché questa funzionalità viene aggiunta in una versione secondaria, un vincolo importante che influisce sulla soluzione è che non ci siano modifiche critiche, né ai contratti di tipo né al comportamento. Questo vincolo ha escluso alcune operazioni di pulizia che avremmo voluto eseguire nei confronti dei blocchi catch esistenti che trasformano le eccezioni in risposte 500. Questa pulizia aggiuntiva potrebbe essere presa in considerazione per una nuova versione principale successiva.
  2. Mantenimento della coerenza con i costrutti dell'API Web La pipeline di filtro dell'API Web è un ottimo modo per gestire le problematiche trasversali con la flessibilità di applicare la logica a un ambito specifico dell'azione, specifico del controller o globale. I filtri, compresi i filtri di eccezione, hanno sempre contesti di azione e di controller, anche quando sono registrati a livello globale. Questo contratto ha senso per i filtri, ma significa che i filtri delle eccezioni, anche quelli con ambito globale, non sono adatti per alcuni casi di gestione delle eccezioni, ad esempio le eccezioni dei gestori di messaggi, in cui non esiste alcun contesto di azione o controller. Se si vuole usare l'ambito flessibile offerto dai filtri per la gestione delle eccezioni, comunque sono necessari i filtri per eccezioni. Tuttavia, se è necessario gestire l'eccezione all'esterno di un contesto del controller, è necessario anche un costrutto separato per la gestione completa degli errori globali (senza vincoli di contesto del controller e contesto di azione).

Quando usare

  • I logger di eccezioni sono la soluzione per visualizzare tutte le eccezioni non gestite rilevate dall'API Web.
  • I gestori di eccezioni sono la soluzione per personalizzare tutte le possibili risposte alle eccezioni non gestite rilevate dall'API Web.
  • I filtri eccezioni sono la soluzione più semplice per l'elaborazione delle eccezioni non gestite del subset correlate a un'azione o a un controller specifico.

Dettagli servizio

Le interfacce di servizio del logger e del gestore delle eccezioni sono semplici metodi asincroni che ricevono i rispettivi contesti.

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

Sono inoltre disponibili classi di base per entrambe queste interfacce. L'override dei metodi di base (sincronizzazione o asincrona) è tutto ciò che è necessario per registrare o gestire nei momenti consigliati. Per la registrazione, la ExceptionLogger classe base garantisce che il metodo di registrazione principale venga chiamato una sola volta per ogni eccezione (anche se successivamente propaga ulteriormente lo stack di chiamate e viene intercettato di nuovo). La ExceptionHandler classe base chiamerà il metodo di gestione principale solo per le eccezioni all'inizio dello stack di chiamate, ignorando i blocchi catch annidati legacy. Le versioni semplificate di queste classi di base sono riportate nell'appendice seguente. Sia IExceptionLogger che IExceptionHandler ricevono informazioni sull'eccezione tramite un oggetto ExceptionContext.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

Quando il framework chiama un logger delle eccezioni o un gestore di eccezioni, fornirà sempre un Exception e un Request. Ad eccezione degli unit test, fornirà sempre un oggetto RequestContext. Raramente fornirà un ControllerContext e un ActionContext (solo quando si chiama dal blocco catch per i filtri delle eccezioni). Raramente fornirà un oggetto Response (solo in alcuni casi IIS, quando si è nel mezzo di tentare di scrivere la risposta). Si noti che, poiché alcune di queste proprietà potrebbero essere null, spetta all'utente verificare null prima di accedere ai membri della classe di eccezione. CatchBlock è una stringa che indica quale blocco catch ha visto l'eccezione. Le stringhe di blocco catch sono le seguenti:

  • HttpServer (metodo SendAsync)

  • HttpControllerDispatcher (metodo SendAsync)

  • HttpBatchHandler (metodo SendAsync)

  • IExceptionFilter (elaborazione da parte di ApiController della pipeline di filtro eccezioni in ExecuteAsync)

  • Host OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (per bufferizzare l'output)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (per l'output di streaming)
  • Fornitore di hosting

    • HttpControllerHandler.WriteBufferedResponseContentAsync (per la bufferizzazione dell'output)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (per lo streaming dell'output)
    • HttpControllerHandler.WriteErrorResponseContentAsync (per i fallimenti nel recupero degli errori in modalità buffer)

L'elenco delle stringhe di blocco catch è disponibile anche tramite proprietà statiche di sola lettura. (Le stringhe del blocco catch principale si trovano nei ExceptionCatchBlocks statici; il resto è presente in una classe statica ciascuna per host OWIN e web host). IsTopLevelCatchBlock è utile per seguire il modello consigliato di gestione delle eccezioni solo all'inizio dello stack di chiamate. Anziché trasformare le eccezioni in 500 risposte in qualsiasi punto in cui si verifica un blocco catch annidato, un gestore eccezioni può consentire la propagazione delle eccezioni fino a quando non stanno per essere visualizzate dall'host.

Oltre a ExceptionContext, un logger ottiene un'altra informazione tramite l'intero ExceptionLoggerContext:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

La seconda proprietà, CanBeHandled, consente a un logger di identificare un'eccezione che non può essere gestita. Quando la connessione sta per essere interrotta e non è possibile inviare alcun nuovo messaggio di risposta, i logger verranno chiamati, ma il gestore non verrà chiamato e i logger possono identificare questo scenario da questa proprietà.

In aggiunta a ExceptionContext, un gestore ottiene un'altra proprietà che può essere impostata sull'intero ExceptionHandlerContext per gestire l'eccezione:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Un gestore eccezioni indica che ha gestito un'eccezione impostando la Result proprietà su un risultato dell'azione( ad esempio, exceptionResult, InternalServerErrorResult, StatusCodeResult o un risultato personalizzato). Se la Result proprietà è Null, l'eccezione non viene gestita e l'eccezione originale verrà generata nuovamente.

Per le eccezioni all'inizio dello stack di chiamate, è stato eseguito un passaggio aggiuntivo per assicurarsi che la risposta sia appropriata per i chiamanti API. Se l'eccezione si propaga fino all'host, il chiamante visualizzerà la schermata gialla di errore o un'altra risposta fornita dall'host, che di solito è in HTML e di solito non è una risposta di errore dell'API appropriata. In questi casi, Result viene avviato come non-null, e solo se un gestore di eccezioni personalizzato lo imposta esplicitamente su null (non gestito), l'eccezione verrà propagata all'host. L'impostazione di Result su null in questi casi può essere utile per due scenari:

  1. API Web ospitata da OWIN con middleware di gestione delle eccezioni personalizzato registrato prima/all'esterno dell'API Web.
  2. Il debug locale tramite browser, dove la schermata gialla della morte rappresenta in realtà una risposta utile per un'eccezione non gestita.

Per i logger di eccezioni e i gestori di eccezioni, non viene eseguita alcuna operazione di ripristino se il logger o il gestore stesso genera un'eccezione. Se si ha un approccio migliore, oltre a consentire la propagazione dell'eccezione, lasciare commenti e suggerimenti nella parte inferiore di questa pagina. Il contratto per i logger di eccezioni e i gestori è che non devono consentire la propagazione delle eccezioni ai chiamanti; in caso contrario, l'eccezione verrà propagata, spesso fino all'host, risultando in un errore HTML, ad esempio la schermata gialla di ASP.NET, che viene inviata al client (cosa che di solito non è l'opzione preferita per i chiamanti API che si aspettano JSON o XML).

Examples

Logger di tracciamento delle eccezioni

Il logger di eccezione seguente invia i dati di eccezione alle fonti di traccia configurate (inclusa la finestra di output Debug in Visual Studio).

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

Gestore di eccezioni per messaggi di errore personalizzati

Il gestore eccezioni seguente genera una risposta di errore personalizzata ai client, incluso un indirizzo di posta elettronica per contattare il supporto tecnico.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

Registrare i filtri delle eccezioni

Se utilizzi il modello di progetto "ASP.NET applicazione Web MVC 4" per creare il progetto, inserisci il codice di configurazione dell'API Web all'interno della WebApiConfig classe nella cartella App_Start.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

Appendice: Dettagli classe base

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}