Globální zpracování chyb ve webovém rozhraní API ASP.NET 2

David Matson, Rick Anderson

Toto téma obsahuje přehled globálního zpracování chyb v ASP.NET webovém rozhraní API 2 pro ASP.NET 4.x. Webové rozhraní API dnes nemá snadný způsob, jak protokolovat nebo zpracovávat chyby globálně. Některé neošetřené výjimky je možné zpracovat pomocí filtrů výjimek, ale existuje řada případů, které filtry výjimek nezvládnou. Například:

  1. Výjimky vyvolané konstruktory kontroleru
  2. Výjimky vyvolané obslužnými rutinami zpráv
  3. Výjimky vyvolané během směrování
  4. Výjimky vyvolané při serializaci obsahu odpovědi

Chceme poskytnout jednoduchý konzistentní způsob, jak tyto výjimky protokolovat a zpracovávat (pokud je to možné).

Existují dva hlavní případy zpracování výjimek, kdy můžeme odeslat chybovou odpověď a případ, kdy vše, co můžeme udělat, je protokolovat výjimku. Příkladem pro druhý případ je, když se během obsahu streamované odpovědi vyvolá výjimka; v takovém případě je příliš pozdě odeslat novou zprávu odpovědi, protože stavový kód, hlavičky a částečný obsah již byly odeslány, takže připojení jednoduše přerušujeme. I když výjimku nelze zpracovat za účelem vytvoření nové zprávy odpovědi, stále podporujeme protokolování výjimky. V případech, kdy můžeme zjistit chybu, můžeme vrátit odpovídající chybovou odpověď, jak je znázorněno v následujícím příkladu:

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

Existující možnosti

Kromě filtrů výjimek lze obslužné rutiny zpráv použít dnes k pozorování všech odpovědí na úrovni 500, ale reakce na tyto odpovědi je obtížná, protože nemají kontext původní chyby. Obslužné rutiny zpráv mají také určitá omezení jako filtry výjimek ve vztahu k případům, které mohou zpracovávat. I když webové rozhraní API obsahuje infrastrukturu trasování, která zachycuje chybové stavy, infrastruktura trasování je určená pro účely diagnostiky a není navržená nebo vhodná pro provoz v produkčních prostředích. Globální zpracování výjimek a protokolování by měly být služby, které se dají spustit v produkčním prostředí a připojit se k existujícím řešením monitorování (například ELMAH).

Přehled řešení

Poskytujeme dvě nové uživatelsky nahraditelné služby , IExceptionLogger a IExceptionHandler, pro protokolování a zpracování neošetřených výjimek. Služby jsou velmi podobné, se dvěma hlavními rozdíly:

  1. Podporujeme registraci více protokolovacích rutin výjimek, ale pouze jednu obslužnou rutinu výjimky.
  2. Protokolování výjimek se vždy volají, i když se chystáme přerušit připojení. Obslužné rutiny výjimek se volají pouze tehdy, když jsme stále schopni vybrat, kterou zprávu odpovědi odeslat.

Obě služby poskytují přístup k kontextu výjimky, který obsahuje relevantní informace z bodu, kde byla zjištěna výjimka, zejména HttpRequestMessage, HttpRequestContext, vyvolaná výjimka a zdroj výjimky (podrobnosti níže).

Principy návrhu

  1. Žádné zásadní změny Vzhledem k tomu, že se tato funkce přidává v dílčí verzi, je jedním z důležitých omezení, které má vliv na řešení, že nedojde k žádným zásadním změnám, a to buď u kontraktů typu, nebo chování. Toto omezení vyloučilo některé vyčištění, které bychom chtěli provést z hlediska stávajících bloků catch, které změní výjimky na 500 odpovědí. Toto další vyčištění je něco, co bychom mohli zvážit pro následující hlavní verzi.
  2. Zachování konzistence s konstrukcemi webového rozhraní API Filtrovací kanál webového rozhraní API je skvělý způsob, jak zvládnout průřezové aspekty s flexibilitou aplikovat logiku na úrovni akce, kontroleru nebo globálně. Filtry, včetně filtrů výjimek, vždy mají kontexty akcí a kontroleru, i když jsou zaregistrované v globálním oboru. Tento kontrakt dává smysl pro filtry, ale znamená to, že filtry výjimek, dokonce i globálně vymezené, nejsou vhodné pro některé případy zpracování výjimek, jako jsou výjimky z obslužných rutin zpráv, kde neexistuje žádný kontext akce nebo kontroleru. Pokud chceme použít flexibilní rozsah, který filtry poskytují pro zpracování výjimek, musíme stále použít filtry výjimek. Pokud ale potřebujeme zpracovat výjimku mimo kontext kontroleru, potřebujeme také samostatný konstruktor pro úplné globální zpracování chyb (něco bez omezení kontextu kontroleru a kontextu akce).

Kdy použít

  • Protokolovací nástroje výjimek představují řešení, ve které se zobrazují všechny neošetřené výjimky zachycené webovým rozhraním API.
  • Obslužné rutiny výjimek jsou řešením pro přizpůsobení všech možných odpovědí na neošetřené výjimky zachycené webovým rozhraním API.
  • Filtry výjimek představují nejjednodušší řešení pro zpracování neošetřených výjimek v podmnožině souvisejících s konkrétní akcí nebo kontrolerem.

Podrobnosti o službě

Rozhraní služeb pro protokolování a obsluhu výjimek jsou jednoduché asynchronní metody, které pracují s příslušnými kontexty:

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

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

Poskytujeme také základní třídy pro obě tato rozhraní. Přepsání základních (synchronizačních nebo asynchronních) metod je vše, co je potřeba k protokolování nebo zpracování v doporučených časech. Pro protokolování zajistí základní třída ExceptionLogger, že hlavní metoda protokolování je volána pouze jednou pro každou výjimku (i když se později šíří dále ve volacím zásobníku a je znovu zachycena). Základní ExceptionHandler třída bude volat metodu zpracování jádra pouze pro výjimky nacházející se na nejvyšší úrovni zásobníku volání a bude ignorovat zastaralé vnořené bloky catch. (Zjednodušené verze těchto základních tříd jsou uvedené v dodatku níže.) Obě IExceptionLogger a IExceptionHandler přijímají informace o výjimce prostřednictvím 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; }
}

Když rozhraní volá protokolovací rutinu výjimky nebo obslužnou rutinu výjimky, vždy poskytne Exception a Request. Kromě jednotkového testování vždy také poskytne RequestContext. Zřídka poskytne ControllerContext a ActionContext (pouze při volání z bloku catch pro filtry výjimek). Bude velmi zřídka poskytovat Response (pouze v některých případech IIS, když se nachází uprostřed pokusu o zápis odpovědi). Mějte na paměti, že protože některé z těchto vlastností mohou být null, je na spotřebiteli, aby zkontroloval null před přístupem ke členům třídy výjimky. CatchBlock je řetězec označující, který blok catch viděl výjimku. Řetězce bloku catch jsou následující:

  • HttpServer (metoda SendAsync)

  • HttpControllerDispatcher (metoda SendAsync)

  • HttpBatchHandler (metoda SendAsync)

  • IExceptionFilter (zpracování kanálu filtru výjimek v ExecuteAsync apiController)

  • Hostitel OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (pro bufferování obsahu odpovědi)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (pro výstup streamování)
  • Webový hostitel:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (pro ukládání výstupu do bufferu)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (pro výstup streamování)
    • HttpControllerHandler.WriteErrorResponseContentAsync (při selhání obnovy chyb ve vyrovnávacím režimu výstupu)

Seznam řetězců bloku catch je také k dispozici prostřednictvím statických vlastností jen pro čtení. (Řetězec bloku zachytávání jádra je ve statickém objektu ExceptionCatchBlocks; zbytek se zobrazí v jedné statické třídě pro OWIN a webového hostitele). IsTopLevelCatchBlock je užitečné pro sledování doporučeného vzoru zpracování výjimek pouze v horní části zásobníku volání. Místo toho, aby se výjimky převáděly na odpovědi s kódem 500 kdekoli, kde se vyskytne vnořený blok catch, může obslužná rutina umožnit šíření výjimek, dokud se nechystá být zachyceny hostovacím systémem.

Kromě ExceptionContext získá protokolovací nástroj prostřednictvím plné ExceptionLoggerContext jednu další informaci.

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

Druhá vlastnost umožňuje CanBeHandledprotokolovacímu nástroji identifikovat výjimku, kterou nelze zpracovat. Když je připojení přerušeno a nelze odeslat žádnou novou zprávu odpovědi, budou volány protokolovací nástroje, ale obslužná rutina nebude volána a protokolovací nástroje mohou identifikovat tento scénář z této vlastnosti.

Kromě ExceptionContext získá obslužná rutina ještě jednu vlastnost, kterou lze nastavit na úplné ExceptionHandlerContext pro zpracování výjimky:

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

Obslužná rutina výjimky označuje, že zpracovala výjimku nastavením Result vlastnosti na výsledek akce (například ExceptionResult, InternalServerErrorResult, StatusCodeResult nebo vlastní výsledek). Result Pokud je vlastnost null, výjimka je neošetřená a původní výjimka bude znovu vyvolána.

U výjimek v horní části volacího zásobníku jsme provedli dodatečný krok, abychom zajistili, že odpověď odpovídá volajícím v rámci API. Pokud se výjimka rozšíří na hostitele, volající uvidí žlutou obrazovku smrti nebo jinou odpověď poskytnutou hostitelem, která je obvykle HTML, a nejedná se o vhodnou chybovou odpověď rozhraní API. V těchto případech začíná výsledek s nenulovou hodnotou a pouze pokud vlastní obslužná rutina výjimky explicitně nastaví výsledek zpět na null (neošetřenou), výjimka se rozšíří na hostitele. Result Nastavení null v takových případech může být užitečné pro dva scénáře:

  1. Hostované webové rozhraní API OWIN s vlastním middlewarem zpracovávajím výjimky zaregistrovaným před nebo mimo webové rozhraní API.
  2. Místní ladění pomocí prohlížeče, kdy je tzv. žlutá obrazovka smrti ve skutečnosti užitečnou odpovědí na neošetřenou výjimku.

U záznamníků a zpracovatelů výjimek neděláme nic k obnovení, pokud sám záznamník nebo zpracovatel vyvolá výjimku. (Pokud máte lepší přístup, zanechte zpětnou vazbu dole na této stránce, aniž byste nechali výjimku rozšířit.) Kontrakt pro logování a obsluhu výjimek je, že by neměly umožnit, aby se výjimky šířily až k jejich volajícím; jinak se výjimka jen šíří dál, často až k hostiteli, což vede k chybě HTML (jako je žlutá obrazovka ASP.NET), která se odesílá zpět klientovi (což obvykle není preferovanou možností pro API volání, která očekávají JSON nebo XML).

Příklady

Sledovací záznamník výjimek

Zaznamenávač výjimek níže posílá data o výjimkách do nakonfigurovaných zdrojů trasování (včetně okna Výstup ladění ve Visual Studiu).

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

Vlastní zpracovatel výjimek chybové zprávy

Obslužná rutina výjimky níže vytvoří vlastní chybovou odpověď pro klienty, včetně e-mailové adresy pro kontaktování podpory.

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

Registrace filtrů výjimek

Pokud k vytvoření projektu použijete šablonu projektu "ASP.NET MVC 4", vložte konfigurační kód webového WebApiConfig rozhraní API do třídy do složky App_Start :

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

        // Other configuration code...
    }
}

Příloha: Podrobnosti základní třídy

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