Gestione degli errori globali in API Web ASP.NET 2

di David Matson, Rick Anderson

In questo argomento viene fornita una panoramica della gestione degli errori globali in API Web ASP.NET 2 per ASP.NET 4.x. Oggi non esiste un modo semplice nell'API Web per registrare o gestire errori a livello globale. Alcune eccezioni non gestite possono essere elaborate tramite filtri di eccezione, ma esistono diversi casi che i filtri delle eccezioni non possono gestire. Ad 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 (se 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 tutto ciò che è possibile eseguire è registrare l'eccezione. Un esempio per quest'ultimo caso è quando viene generata un'eccezione nel mezzo del contenuto della risposta di streaming; in tal caso è troppo tardi inviare un nuovo messaggio di risposta poiché il codice di stato, le intestazioni e il contenuto parziale sono già andati attraverso la rete, quindi è sufficiente interrompere la connessione. Anche se l'eccezione non può essere gestita per produrre 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 nell'esempio seguente:

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 livello di 500 livelli, ma agire su tali risposte è difficile, in quanto non hanno contesto sull'errore originale. I gestori di messaggi hanno anche alcune delle stesse limitazioni dei filtri delle eccezioni relativi ai casi che possono gestire. Sebbene l'API Web abbia l'infrastruttura di traccia che acquisisce le condizioni di errore, l'infrastruttura di traccia è destinata a scopi di diagnostica e non è progettata o adatta per l'esecuzione in ambienti di produzione. La gestione e la registrazione di eccezioni globali 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ù loggger di eccezioni, ma solo un singolo gestore di eccezioni.
  2. I loggger di eccezione vengono sempre chiamati, anche se stiamo per interrompere la connessione. I gestori di eccezioni vengono chiamati solo quando siamo ancora in grado di scegliere quale messaggio di risposta inviare.

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

Principi di progettazione

  1. Nessuna modifica di rilievo Poiché questa funzionalità viene aggiunta in una versione secondaria, un vincolo importante che influisce sulla soluzione è che non ci sono modifiche di rilievo, per digitare contratti o comportamento. Questo vincolo ha escluso alcune operazioni di pulizia da eseguire in termini di blocchi catch esistenti che trasformano le eccezioni in 500 risposte. Questa pulizia aggiuntiva è un elemento che potrebbe essere considerato per una versione principale successiva.
  2. Gestione della coerenza con i costrutti dell'API Web La pipeline di filtro dell'API Web è un ottimo modo per gestire i problemi di taglio incrociato con la flessibilità di applicare la logica in un ambito specifico dell'azione, specifico del controller o globale. I filtri, inclusi i filtri di eccezione, hanno sempre contesti di azione e controller, anche se registrati nell'ambito globale. Questo contratto ha senso per i filtri, ma significa che i filtri delle eccezioni, anche quelli con ambito globale, non sono adatti a alcuni casi di gestione delle eccezioni, ad esempio eccezioni dai gestori di messaggi, dove non esiste alcun contesto di azione o controller. Se si vuole usare l'ambito flessibile offerto dai filtri per la gestione delle eccezioni, sono comunque necessari filtri di eccezione. 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 (qualcosa senza vincoli di contesto del controller e contesto di azione).

Utilizzo

  • I loggger 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 di eccezione sono la soluzione più semplice per l'elaborazione delle eccezioni non gestite del subset correlate a un'azione o a un controller specifici.

Dettagli del servizio

Le interfacce del servizio del logger e del gestore delle eccezioni sono metodi asincroni che accettano i rispettivi contesti:

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

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

Sono disponibili anche classi di base per entrambe queste interfacce. L'override dei metodi core (sincronizzazione o asincrona) è tutto ciò che è necessario per registrare o gestire al momento consigliato. Per la registrazione, la ExceptionLogger classe base garantisce che il metodo di registrazione principale venga chiamato una sola volta per ogni eccezione (anche se in seguito si propaga ulteriormente lo stack di chiamate e viene catturato di nuovo). La ExceptionHandler classe di base chiamerà il metodo di gestione principale solo per le eccezioni nella parte superiore dello stack di chiamate, ignorando i blocchi catch annidati legacy. Le versioni semplificate di queste classi di base si trovano nell'appendice seguente. Sia IExceptionLogger che IExceptionHandler ricevere informazioni sull'eccezione tramite un ExceptionContextoggetto .

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 di eccezioni o un gestore di eccezioni, fornirà sempre un Exception oggetto e un Request. Ad eccezione di unit test, fornisce sempre un RequestContextoggetto . Raramente fornisce un ControllerContext oggetto e ActionContext (solo quando si chiama dal blocco catch per i filtri di eccezione). Viene fornito molto raramente un oggetto Response(solo in alcuni casi IIS quando al centro del tentativo di scrivere la risposta). Si noti che poiché alcune di queste proprietà possono essere null destinate al consumer per 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 di ApiController della pipeline di filtro eccezioni in ExecuteAsync)

  • Host OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (per l'output del buffering)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (per l'output di streaming)
  • Host Web:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (per l'output del buffering)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (per l'output di streaming)
    • HttpControllerHandler.WriteErrorResponseContentAsync (per errori nel ripristino degli errori in modalità di output con buffer)

L'elenco delle stringhe di blocco catch è disponibile anche tramite proprietà statiche di lettura. La stringa del blocco catch principale si trovano nella stringa statica ExceptionCatchBlocks; il resto viene visualizzato in una classe statica ogni per OWIN e l'host Web.IsTopLevelCatchBlock è utile per seguire il modello consigliato di gestione delle eccezioni solo nella parte superiore dello stack di chiamate. Anziché trasformare le eccezioni in 500 risposte ovunque si verifichi un blocco catch annidato, un gestore di eccezioni può consentire la propagazione delle eccezioni fino a quando non stanno per essere visualizzati dall'host.

Oltre a , un logger ottiene un'altra parte di informazioni tramite l'intero ExceptionContextExceptionLoggerContext:

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

La seconda proprietà, , CanBeHandledconsente 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 loggger verranno chiamati, ma il gestore non verrà chiamato e i loggger possono identificare questo scenario da questa proprietà.

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

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

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

Per le eccezioni all'inizio dello stack di chiamate, è stato eseguito un passaggio aggiuntivo per garantire che la risposta sia appropriata per i chiamanti API. Se l'eccezione si propaga fino all'host, il chiamante visualizzerà lo schermo giallo della morte o una risposta fornita da un altro host che è in genere HTML e non una risposta di errore API appropriata. In questi casi, il risultato viene avviato non null e solo se un gestore di eccezioni personalizzato lo imposta in modo esplicito su null (non gestito) verrà propagata all'host. L'impostazione Result su null in questi casi può essere utile per due scenari:

  1. API Web ospitata da OWIN con middleware personalizzato registrato prima/all'esterno dell'API Web.
  2. Debug locale tramite un browser, in cui la schermata gialla della morte è effettivamente una risposta utile per un'eccezione non gestita.

Per i loggger di eccezioni e i gestori di eccezioni, non viene eseguita alcuna operazione di ripristino se il logger o il gestore stesso genera un'eccezione. Diversamente da consentire la propagazione dell'eccezione, lasciare commenti e suggerimenti nella parte inferiore di questa pagina se si ha un approccio migliore. Il contratto per i loggger di eccezioni e i gestori è che non devono consentire alle eccezioni di propagarsi ai chiamanti; in caso contrario, l'eccezione verrà propagata, spesso tutto il modo in cui l'host genera un errore HTML ,ad esempio ASP. Schermata gialla di NET) inviata al client (che in genere non è l'opzione preferita per i chiamanti API che prevedono JSON o XML).

Esempio

Traccia del logger di eccezioni

Il logger di eccezione seguente invia i dati dell'eccezione alle origini 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 del messaggio di errore personalizzato

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

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

Registrazione dei filtri delle eccezioni

Se si usa il modello di progetto "ASP.NET applicazione Web MVC 4" per creare il progetto, inserire 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 della classe di 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;
    }
}