Freigeben über


Globale Fehlerbehandlung in ASP.NET Web-API 2

von David Matson, Rick Anderson

Dieses Thema enthält eine Übersicht über die globale Fehlerbehandlung in ASP.NET Web-API 2 für ASP.NET 4.x. Heute gibt es in der Web-API keine einfache Möglichkeit, Fehler global zu protokollieren oder zu behandeln. Einige unbehandelte Ausnahmen können über Ausnahmefilter verarbeitet werden, es gibt jedoch eine Reihe von Fällen, in denen Ausnahmefilter nicht behandelt werden können. Beispiel:

  1. Von Controllerkonstruktoren ausgelöste Ausnahmen.
  2. Von Meldungshandlern ausgelöste Ausnahmen.
  3. Während des Routings ausgelöste Ausnahmen.
  4. Während der Serialisierung von Antwortinhalten ausgelöste Ausnahmen.

Wir möchten eine einfache, konsistente Möglichkeit zum Protokollieren und Behandeln (sofern möglich) dieser Ausnahmen bereitstellen.

Es gibt zwei Hauptfälle für die Behandlung von Ausnahmen, den Fall, in dem wir eine Fehlerantwort senden können, und der Fall, in dem wir nur die Ausnahme protokollieren können. Ein Beispiel für den letzteren Fall ist, wenn eine Ausnahme in der Mitte der Streamingantwortinhalte ausgelöst wird. In diesem Fall ist es zu spät, eine neue Antwortnachricht zu senden, da der Statuscode, die Kopfzeilen und der teilliche Inhalt bereits über das Kabel gegangen sind, sodass wir die Verbindung einfach abbrechen. Obwohl die Ausnahme nicht behandelt werden kann, um eine neue Antwortnachricht zu erstellen, wird weiterhin die Protokollierung der Ausnahme unterstützt. In Fällen, in denen wir einen Fehler erkennen können, können wir eine entsprechende Fehlerantwort zurückgeben, wie in der folgenden Abbildung gezeigt:

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

Vorhandene Optionen

Zusätzlich zu Ausnahmefiltern können Nachrichtenhandler heute verwendet werden, um alle Antworten auf 500 Ebenen zu beobachten, aber das Handeln auf diese Antworten ist schwierig, da sie keinen Kontext zum ursprünglichen Fehler haben. Nachrichtenhandler haben auch einige der gleichen Einschränkungen wie Ausnahmefilter in Bezug auf die Fälle, die sie behandeln können. Während die Web-API über eine Ablaufverfolgungsinfrastruktur verfügt, die Fehlerbedingungen erfasst, dient die Ablaufverfolgungsinfrastruktur zu Diagnosezwecken und ist nicht für die Ausführung in Produktionsumgebungen konzipiert oder geeignet. Globale Ausnahmebehandlung und Protokollierung sollten Dienste sein, die während der Produktion ausgeführt und in vorhandene Überwachungslösungen (z. B. ELMAH) angeschlossen werden können.

Lösungsübersicht

Wir bieten zwei neue benutzerersetzbare Dienste, IExceptionLogger und IExceptionHandler, um unbehandelte Ausnahmen zu protokollieren und zu behandeln. Die Dienste sind sehr ähnlich, mit zwei Hauptunterschieden:

  1. Wir unterstützen das Registrieren mehrerer Ausnahmeprotokollierer, aber nur einen einzelnen Ausnahmehandler.
  2. Ausnahmeprotokollierer werden immer aufgerufen, auch wenn die Verbindung abgebrochen werden soll. Ausnahmehandler werden nur aufgerufen, wenn weiterhin ausgewählt werden kann, welche Antwortnachricht gesendet werden soll.

Beide Dienste bieten Zugriff auf einen Ausnahmekontext, der relevante Informationen von dem Punkt enthält, an dem die Ausnahme erkannt wurde, insbesondere die HttpRequestMessage, der HttpRequestContext, die ausgelöste Ausnahme und die Ausnahmequelle (Details unten).

Entwurfsprinzipien

  1. Keine Breaking Changes Da diese Funktionalität in einem Minor-Release hinzugefügt wird, besteht eine wichtige Einschränkung, die sich auf die Lösung auswirkt, darin, dass es keine Änderungen, die bestehende Funktionalitäten brechen, weder bei Typverträgen noch beim Verhalten geben darf. Diese Einschränkung schließt einige Bereinigungen aus, die wir in Bezug auf vorhandene Catch-Blöcke durchgeführt haben möchten, die Ausnahmen in 500 Antworten umwandeln. Diese zusätzliche Bereinigung ist etwas, das wir für eine nachfolgende Hauptversion in Betracht ziehen könnten.
  2. Beibehalten der Konsistenz mit Web-API-Konstrukten Die Filterpipeline der Web-API ist eine hervorragende Möglichkeit, um übergreifende Probleme mit der Flexibilität der Anwendung der Logik auf einen aktionsspezifischen, controllerspezifischen oder globalen Bereich zu behandeln. Filter, einschließlich Ausnahmefiltern, verfügen immer über Aktions- und Controllerkontexte, auch wenn sie im globalen Bereich registriert sind. Dieser Vertrag ist für Filter sinnvoll, bedeutet jedoch, dass Ausnahmefilter, auch global bereichsbezogene Filter, nicht für einige Ausnahmebehandlungsfälle geeignet sind, z. B. Ausnahmen von Nachrichtenhandlern, bei denen kein Aktions- oder Controllerkontext vorhanden ist. Wenn wir die flexible Bereichsdefinition verwenden möchten, die von Filtern für die Ausnahmebehandlung geboten wird, benötigen wir weiterhin Ausnahmefilter. Wenn wir jedoch eine Ausnahme außerhalb eines Controllerkontexts behandeln müssen, benötigen wir auch ein separates Konstrukt für die vollständige globale Fehlerbehandlung (etwas ohne Controllerkontext- und Aktionskontexteinschränkungen).

Wann verwendet werden soll

  • Ausnahmeprotokollierer sind die Lösung, um alle unbehandelten Ausnahmen, die von der Web-API abgefangen werden, anzuzeigen.
  • Ausnahmehandler sind die Lösung zum Anpassen aller möglichen Antworten auf unbehandelte Ausnahmen, die von der Web-API erfasst werden.
  • Ausnahmefilter sind die einfachste Lösung für die Verarbeitung der nicht behandelten Teilmengenausnahmen im Zusammenhang mit einer bestimmten Aktion oder einem bestimmten Controller.

Dienstdetails

Die Ausnahmeprotokollierer- und Handlerdienstschnittstellen sind einfache asynchrone Methoden, die die jeweiligen Kontexte verwenden:

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

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

Wir stellen auch Basisklassen für beide Schnittstellen bereit. Das Überschreiben der Kernmethoden (synchron oder asynchron) ist alles, was erforderlich ist, um zu den empfohlenen Zeiten zu protokollieren oder zu verarbeiten. Bei der Protokollierung stellt die ExceptionLogger Basisklasse sicher, dass die Kernprotokollierungsmethode nur einmal für jede Ausnahme aufgerufen wird (auch wenn sie später weiter oben im Aufrufstapel verteilt und erneut abgefangen wird). Die ExceptionHandler Basisklasse ruft die Kernbehandlungsmethode nur für Ausnahmen am oberen Rand des Aufrufstapels auf, wobei ältere geschachtelte Catch-Blöcke ignoriert werden. (Vereinfachte Versionen dieser Basisklassen sind im Anhang unten aufgeführt.) Sowohl als IExceptionLogger auch IExceptionHandler Empfangen von Informationen über die Ausnahme über eine 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; }
}

Wenn das Framework einen Ausnahmeprotokollierer oder einen Ausnahmebehandler aufruft, stellt es immer ein Exception und ein Request. Mit Ausnahme von Unittests wird es immer auch ein RequestContext bereitstellen. Es wird selten ein ControllerContext und ActionContext bereitgestellt (nur beim Aufrufen des Catch-Blocks für Ausnahmefilter). Es wird nur sehr selten eine Response bereitgestellt (nur in bestimmten IIS-Fällen, wenn man gerade versucht, die Antwort zu schreiben). Beachten Sie, dass einige dieser Eigenschaften null sein können, sodass der Anwender null aufrufen muss, bevor auf die Member der Ausnahmeklasse zugegriffen wird. CatchBlock ist eine Zeichenfolge, die angibt, welcher Catch-Block die Ausnahme sah. Die Catch-Blockzeichenfolgen sind wie folgt:

  • HttpServer (SendAsync-Methode)

  • HttpControllerDispatcher (SendAsync-Methode)

  • HttpBatchHandler (SendAsync-Methode)

  • IExceptionFilter (ApiControllers Verarbeitung der Ausnahmefilterpipeline in ExecuteAsync)

  • OWIN-Host:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (zum Puffern der Ausgabe)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (für Streamingausgabe)
  • Webhoster

    • HttpControllerHandler.WriteBufferedResponseContentAsync (zum Puffern der Ausgabe)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (für Streamingausgabe)
    • HttpControllerHandler.WriteErrorResponseContentAsync (bei Fehlern in der Fehlerwiederherstellung im gepufferten Ausgabemodus)

Die Liste der Catch-Blockzeichenfolgen ist auch über statische Readonly-Eigenschaften verfügbar. (Die Kernzeichenfolge des Catch-Blocks befindet sich in den statischen ExceptionCatchBlocks; der Rest erscheint jeweils in einer statischen Klasse für OWIN und Webhost). IsTopLevelCatchBlock ist hilfreich, um dem empfohlenen Muster der Behandlung von Ausnahmen nur am oberen Ende des Aufrufstapels zu folgen. Anstatt Ausnahmen überall dort, wo ein geschachtelter Catch-Block auftritt, in 500 Antworten zu verwandeln, kann ein Ausnahmehandler Ausnahmen fortschreiten lassen, bis sie vom Host empfangen werden.

Zusätzlich zu ExceptionContext erhält ein Logger eine zusätzliche Information über das vollständige ExceptionLoggerContext:

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

Die zweite Eigenschaft ermöglicht es einem Logger, CanBeHandledeine Ausnahme zu identifizieren, die nicht behandelt werden kann. Wenn die Verbindung abgebrochen wird und keine neue Antwortnachricht gesendet werden kann, werden die Logger aufgerufen, aber der Handler wird nicht aufgerufen, und die Logger können dieses Szenario aus dieser Eigenschaft identifizieren.

Zusätzlich zum ExceptionContext erhält ein Handler eine weitere Eigenschaft, die er am gesamten ExceptionHandlerContext festlegen kann, um die Ausnahme zu behandeln.

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

Ein Ausnahmehandler gibt an, dass er eine Ausnahme behandelt hat, indem die Result Eigenschaft auf ein Aktionsergebnis festgelegt wird (z. B. ein ExceptionResult, InternalServerErrorResult, StatusCodeResult oder ein benutzerdefiniertes Ergebnis). Wenn die Result Eigenschaft NULL ist, wird die Ausnahme nicht behandelt, und die ursprüngliche Ausnahme wird erneut ausgelöst.

Für Ausnahmen am oberen Rand des Aufrufstapels haben wir einen zusätzlichen Schritt unternommen, um sicherzustellen, dass die Antwort für API-Aufrufer geeignet ist. Wenn die Ausnahme an den Host weitergegeben wird, würde der Aufrufer den gelben Bildschirm des Todes oder eine andere vom Host bereitgestellte Antwort sehen, die in der Regel HTML und nicht in der Regel eine entsprechende API-Fehlerantwort ist. In diesen Fällen ist das Ergebnis zunächst nicht null, und nur wenn ein benutzerdefinierter Ausnahmebehandler es explizit auf null (nicht behandelt) zurücksetzt, wird die Ausnahme an den Host weitergegeben. Die Einstellung von Result auf null in solchen Fällen kann für zwei Szenarien nützlich sein:

  1. OWIN-gehostete Web-API mit benutzerdefinierter Fehlerbehandlungs-Middleware, die vor oder außerhalb der Web-API registriert wird.
  2. Lokales Debuggen über einen Browser, bei dem der Yellow Screen of Death tatsächlich eine hilfreiche Rückmeldung für eine nicht behandelte Ausnahme ist.

Für Ausnahmeprotokollierer und Ausnahmehandler tun wir nichts, um wiederherzustellen, wenn der Logger oder Handler selbst eine Ausnahme auslöst. (Abgesehen davon, dass die Ausnahme weitergegeben wird, geben Sie Feedback am unteren Rand dieser Seite ab, wenn Sie einen besseren Ansatz haben.) Der Vertrag für Ausnahmeprotokollierer und -handler besteht darin, dass sie die Weitergabe von Ausnahmen nicht an ihre Aufrufer zulassen sollten; andernfalls wird die Ausnahme nur weitergegeben, häufig bis zum Host, was zu einem HTML-Fehler (z. B. ASP.NETs gelber Bildschirm) führt, der an den Client zurückgesendet wird (was in der Regel nicht die bevorzugte Option für API-Aufrufer ist, die JSON oder XML erwarten).

Beispiele

Ablaufverfolgungs-Ausnahmeprotokollierer

Der folgende Ausnahmeprotokollierer sendet Ausnahmedaten an konfigurierte Trace-Quellen (einschließlich des Debug-Ausgabefensters in Visual Studio).

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

Benutzerdefinierter Ausnahmehandler für Fehlermeldungen

Der folgende Ausnahmehandler erzeugt eine benutzerdefinierte Fehlerantwort für Clients, einschließlich einer E-Mail-Adresse für die Kontaktaufnahme mit dem Support.

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

Registrieren von Ausnahmefiltern

Wenn Sie die Projektvorlage "ASP.NET MVC 4-Webanwendung" verwenden, um Ihr Projekt zu erstellen, platzieren Sie den Web-API-Konfigurationscode in der WebApiConfig Klasse im Ordner App_Start :

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

        // Other configuration code...
    }
}

Anhang: Basisklassendetails

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