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 rozhraní ASP.NET Web API 2 pro ASP.NET 4.x. Dnes není ve webovém rozhraní API snadný způsob, jak chyby protokolovat nebo zpracovávat 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. Příklad:

  1. Výjimky vyvolané konstruktory kontrolerů
  2. Výjimky vyvolané obslužnými rutinami zpráv
  3. Výjimky vyvolané během směrování
  4. Výjimky vyvolané během serializace obsahu odpovědi

Chceme poskytnout jednoduchý a konzistentní způsob protokolování a zpracování těchto výjimek (pokud je to možné).

Existují dva hlavní případy zpracování výjimek: případ, kdy můžeme odeslat chybovou odpověď, a případ, kdy můžeme jen zaznamenat výjimku. Příkladem pro druhý případ je, když je vyvolán výjimka uprostřed streamování obsahu odpovědi; v takovém případě je příliš pozdě na odeslání nové zprávy s odpovědí, protože stavový kód, hlavičky a částečný obsah už prošly přenosem, takže připojení jednoduše přerušíme. I když výjimku nelze zpracovat tak, aby se vygenerovala nová zpráva 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 je dnes možné použít obslužné rutiny zpráv ke sledování všech odpovědí na úrovni 500, ale reakce na tyto odpovědi je obtížná, protože v nich chybí kontext původní chyby. Obslužné rutiny zpráv mají také některá stejná omezení jako filtry výjimek, pokud jde o případy, které můžou zpracovat. I když webové rozhraní API má trasovací infrastrukturu, která zachycuje chybové stavy, je infrastruktura trasování určená pro diagnostické účely a není navržená ani vhodná pro spouštění 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 můžou spouštět během výroby a připojovat se k existujícím monitorovacím řešením (například ELMAH).

Přehled řešení

Poskytujeme dvě nové uživatelem nahraditelné služby , IExceptionLogger a IExceptionHandler, které protokolují a zpracovávají neošetřené výjimky. 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. Protokolovací nástroje výjimek se budou volat vždy, i když se chystáme přerušit připojení. Obslužné rutiny výjimek se volají jenom tehdy, když stále můžeme zvolit, která zpráva odpovědi se má odeslat.

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

Principy návrhu

  1. Žádné zásadní změny Vzhledem k tomu, že se tato funkce přidává do dílčí verze, jedním z důležitých omezení ovlivňujících řešení je, že nedochází k žádným zásadním změnám, ať už jde o typy kontraktů nebo chování. Toto omezení vyloučilo některé čištění, které bychom chtěli provést, pokud jde o existující bloky catch, které mění výjimky na 500 odpovědí. Toto další vyčištění je něco, co bychom mohli zvážit pro další hlavní verzi.
  2. Zachování konzistence s konstrukcemi webového rozhraní API Kanál filtru webového rozhraní API je skvělý způsob, jak si poradit s různými aspekty a flexibilitou použití logiky v oboru specifickém pro konkrétní akci, v konkrétním řadiči nebo v globálním rozsahu. Filtry, včetně filtrů výjimek, mají vždy kontexty akcí a kontroleru, a to i při registraci v globálním oboru. Tento kontrakt dává smysl pro filtry, ale znamená to, že filtry výjimek, ani filtry 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 kontext akce nebo kontroleru. Pokud chceme pro zpracování výjimek použít flexibilní vymezení rozsahu, které filtry poskytují, stále potřebujeme 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í pro zobrazení všech neošetřených výjimek zachycených webovým rozhraním API.
  • Obslužné rutiny výjimek představují řešení 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 jsou nejjednodušším řešením pro zpracování neošetřených výjimek podmnožinu souvisejících s konkrétní akcí nebo kontrolerem.

Podrobnosti služby

Rozhraní protokolovacího nástroje výjimek a služby obslužné rutiny jsou jednoduché asynchronní metody, které přebírají příslušné 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í. K protokolování nebo zpracování v doporučených časech stačí přepsání základních metod (sync nebo asynchronní). Základní třída pro protokolování zajistí, ExceptionLogger že základní metoda protokolování se pro každou výjimku volá pouze jednou (i když se později rozšíří dále do zásobníku volání a znovu se zachytí). Základní ExceptionHandler třída bude volat základní metodu zpracování pouze pro výjimky v horní části zásobníku volání a ignoruje starší vnořené bloky catch. (Zjednodušené verze těchto základních tříd jsou uvedeny v dodatku níže.) IExceptionHandler Prostřednictvím IExceptionLogger a obdržíte informace o výjimce ExceptionContextprostřednictvím .

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í nástroj výjimky nebo obslužnou rutinu výjimky, bude vždy poskytovat a ExceptionRequest. Kromě testování jednotek bude také vždy poskytovat RequestContext. Zřídka bude poskytovat ControllerContext a ActionContext (pouze při volání z bloku catch pro filtry výjimek). Velmi zřídka poskytne ( Responsepouze v některých případech, kdy se služba IIS pokouší napsat odpověď). Všimněte si, že některé z těchto vlastností mohou být null , je na spotřebiteli, aby před přístupem ke členům třídy výjimky zkontroloval null .CatchBlock je řetězec označující, u kterého bloku catch došlo k výjimce. Blokové řetězce catch jsou následující:

  • HttpServer (metoda SendAsync)

  • HttpControllerDispatcher (metoda SendAsync)

  • HttpBatchHandler (metoda SendAsync)

  • IExceptionFilter (zpracování kanálu filtru výjimek v ExecuteAsync ze služby ApiController)

  • Hostitel OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (pro ukládání výstupu do vyrovnávací paměti)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (pro výstup streamování)
  • Webový hostitel:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (pro ukládání výstupu do vyrovnávací paměti)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (pro výstup streamování)
    • HttpControllerHandler.WriteErrorResponseContentAsync (pro chyby při zotavení po chybě v režimu výstupu ve vyrovnávací paměti)

Seznam řetězců bloku catch je k dispozici také prostřednictvím statických vlastností jen pro čtení. (Základní řetězec bloku catch se nachází ve statickém objektu ExceptionCatchBlocks; zbytek se nachází v jedné statické třídě pro OWIN a webového hostitele).IsTopLevelCatchBlock je vhodný pro dodržová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 500 odpovědí všude, kde dojde k vnořenému bloku catch, může obslužná rutina výjimky nechat rozšířit výjimky, dokud je hostitel nebude vidět.

Kromě objektu ExceptionContextzíská protokolovací nástroj ještě jednu informaci prostřednictvím úplného ExceptionLoggerContextsouboru :

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

Druhá vlastnost , CanBeHandledumožňuje protokolovacímu nástroji identifikovat výjimku, kterou nelze zpracovat. Pokud 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ě objektu ExceptionContextzíská obslužná rutina ještě jednu vlastnost, na které může nastavit úplnou ExceptionHandlerContext výjimku:

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án.

V případě výjimek v horní části zásobníku volání jsme provedli další krok, abychom zajistili, že odpověď je vhodná pro volající rozhraní API. Pokud se výjimka rozšíří až na hostitele, volajícímu se zobrazí žlutá obrazovka se smrtí nebo odpověď poskytnutá jiným hostitelem, která je obvykle ve formátu HTML a obvykle není vhodná chybová odpověď rozhraní API. V těchto případech začne výsledek bez hodnoty null a pouze v případě, že vlastní obslužná rutina výjimky explicitně nastaví zpět na null (neošetřené), výjimka se rozšíří do hostitele. Nastavení Result na null v takových případech může být užitečné pro dva scénáře:

  1. Webové rozhraní API hostované službou OWIN s vlastním middlewarem pro zpracování výjimek zaregistrovaným před nebo mimo webové rozhraní API
  2. Místní ladění prostřednictvím prohlížeče, kde žlutá obrazovka smrti je ve skutečnosti užitečnou odpovědí na neošetřenou výjimku.

Pro protokolovací nástroje výjimek i obslužné rutiny výjimek neděláme nic pro obnovení, pokud samotný protokolovací nástroj nebo obslužná rutina vyvolá výjimku. (Pokud máte lepší přístup, ponechte zpětnou vazbu v dolní části této stránky( kromě toho, že necháte výjimku rozšířit.) Smlouva pro protokolovací a obslužné rutiny výjimek je, že by neměly umožnit šíření výjimek až na jejich volající; jinak se výjimka jenom rozšíří, často až na hostitele, což vede k chybě HTML (například ASP. Žlutá obrazovka platformy NET) odesílaná zpět do klienta (což obvykle není upřednostňovaná možnost pro volající rozhraní API, kteří očekávají JSON nebo XML).

Příklady

Trasovací protokolovací rutina výjimek

Protokolovací nástroj výjimek níže odesílá data výjimek do nakonfigurovaných zdrojů trasování (včetně okna výstupu ladění v sadě Visual Studio).

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

Vlastní obslužná rutina výjimky chybové zprávy

Následující obslužná rutina výjimky vygeneruje klientům vlastní chybovou odpověď, 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 "webová aplikace 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 o základní třídě

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