Globalna obsługa błędów w interfejsie API sieci Web ASP.NET 2

Autor: David Matson, Rick Anderson

Ten temat zawiera omówienie globalnej obsługi błędów w interfejsie API sieci Web ASP.NET 2 dla ASP.NET 4.x. Obecnie w internetowym interfejsie API nie ma łatwego sposobu rejestrowania lub obsługi błędów globalnie. Niektóre nieobsługiwane wyjątki można przetworzyć za pomocą filtrów wyjątków, ale istnieje wiele przypadków, których filtry wyjątków nie mogą obsłużyć. Na przykład:

  1. Zgłoszone wyjątki przez konstruktorów kontrolerów.
  2. Zgłoszone wyjątki przez programy obsługi komunikatów.
  3. Zgłoszone wyjątki podczas routingu.
  4. Zgłoszone wyjątki podczas serializacji zawartości odpowiedzi.

Chcemy zapewnić prosty, spójny sposób rejestrowania i obsługi (tam, gdzie to możliwe) tych wyjątków.

Istnieją dwa główne przypadki obsługi wyjątków, w przypadku gdy możemy wysłać odpowiedź o błędzie i przypadek, w którym wszystko, co możemy zrobić, to rejestrowanie wyjątku. Przykładem tego ostatniego przypadku jest zgłoszenie wyjątku w środku zawartości odpowiedzi przesyłania strumieniowego; w takim przypadku jest za późno, aby wysłać nowy komunikat odpowiedzi, ponieważ kod stanu, nagłówki i częściowa zawartość zostały już przecięty przez przewody, więc po prostu przerwamy połączenie. Mimo że nie można obsłużyć wyjątku w celu utworzenia nowego komunikatu odpowiedzi, nadal obsługujemy rejestrowanie wyjątku. W przypadkach, gdy możemy wykryć błąd, możemy zwrócić odpowiednią odpowiedź o błędzie, jak pokazano poniżej:

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

Istniejące opcje

Oprócz filtrów wyjątków programy obsługi komunikatów mogą być obecnie używane do obserwowania wszystkich odpowiedzi na poziomie 500, ale działanie na tych odpowiedziach jest trudne, ponieważ brakuje im kontekstu dotyczącego oryginalnego błędu. Programy obsługi komunikatów mają również niektóre z tych samych ograniczeń co filtry wyjątków dotyczące przypadków, które mogą obsłużyć. Chociaż internetowy interfejs API ma infrastrukturę śledzenia, która przechwytuje warunki błędu, infrastruktura śledzenia jest przeznaczona do celów diagnostycznych i nie jest przeznaczona ani nadaje się do uruchamiania w środowiskach produkcyjnych. Globalna obsługa wyjątków i rejestrowanie powinny być usługami, które mogą być uruchamiane podczas produkcji i podłączane do istniejących rozwiązań monitorowania (na przykład ELMAH).

Omówienie rozwiązania

Udostępniamy dwie nowe usługi, które można zastąpić użytkownikami, IExceptionLogger i IExceptionHandler, aby rejestrować i obsługiwać nieobsługiwane wyjątki. Usługi są bardzo podobne, z dwoma głównymi różnicami:

  1. Obsługujemy rejestrowanie wielu rejestratorów wyjątków, ale tylko jedną procedurę obsługi wyjątków.
  2. Dzienniki wyjątków zawsze są wywoływane, nawet jeśli mamy przerwać połączenie. Procedury obsługi wyjątków są wywoływane tylko wtedy, gdy nadal możemy wybrać, który komunikat odpowiedzi ma być wysyłany.

Obie usługi zapewniają dostęp do kontekstu wyjątku zawierającego odpowiednie informacje z punktu, w którym wykryto wyjątek, szczególnie httpRequestMessage, httpRequestContext, zgłoszony wyjątek i źródło wyjątku (szczegóły poniżej).

Zasady projektowania

  1. Brak zmian powodujących niezgodność Ponieważ ta funkcja jest dodawana w wersji pomocniczej, jednym z ważnych ograniczeń wpływających na rozwiązanie jest to, że nie ma żadnych zmian powodujących niezgodność, aby wpisać kontrakty lub zachowanie. To ograniczenie wykluczyło pewne oczyszczanie, które chcielibyśmy wykonać w odniesieniu do istniejących bloków catch zamieniając wyjątki w 500 odpowiedzi. To dodatkowe czyszczenie jest czymś, co możemy rozważyć w przypadku kolejnej wersji głównej.
  2. Utrzymywanie spójności przy użyciu konstrukcji internetowego interfejsu API Potok filtru internetowego interfejsu API to doskonały sposób obsługi zagadnień krzyżowych z elastycznością stosowania logiki w zakresie specyficznym dla działania, specyficznym dla kontrolera lub globalnym. Filtry, w tym filtry wyjątków, zawsze mają konteksty akcji i kontrolera, nawet w przypadku zarejestrowania w zakresie globalnym. Ten kontrakt ma sens dla filtrów, ale oznacza to, że filtry wyjątków, nawet globalnie ograniczone, nie są dobrym rozwiązaniem dla niektórych przypadków obsługi wyjątków, takich jak wyjątki od procedur obsługi komunikatów, gdzie nie istnieje żaden kontekst akcji lub kontrolera. Jeśli chcemy użyć elastycznego zakresu zapewnianego przez filtry do obsługi wyjątków, nadal potrzebujemy filtrów wyjątków. Jeśli jednak musimy obsłużyć wyjątek poza kontekstem kontrolera, potrzebujemy również oddzielnej konstrukcji do obsługi pełnego globalnego błędu (coś bez ograniczeń kontekstu kontrolera i kontekstu akcji).

Kiedy należy używać

  • Rejestratory wyjątków to rozwiązanie do wyświetlania wszystkich nieobsługiwanych wyjątków przechwyconych przez internetowy interfejs API.
  • Programy obsługi wyjątków to rozwiązanie do dostosowywania wszystkich możliwych odpowiedzi na nieobsługiwane wyjątki przechwycone przez internetowy interfejs API.
  • Filtry wyjątków są najprostszym rozwiązaniem do przetwarzania podzestawu nieobsługiwane wyjątki związane z określoną akcją lub kontrolerem.

Szczegóły usługi

Interfejsy usługi rejestratora wyjątków i obsługi to proste metody asynchroniczne, które przyjmują odpowiednie konteksty:

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

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

Udostępniamy również klasy podstawowe dla obu tych interfejsów. Zastępowanie podstawowych (synchronizacji lub asynchronicznych) metod jest wymagane do rejestrowania lub obsługi w zalecanych godzinach. W przypadku rejestrowania klasa bazowa zapewni, że podstawowa ExceptionLogger metoda rejestrowania jest wywoływana tylko raz dla każdego wyjątku (nawet jeśli później propaguje dalej stos wywołań i zostanie ponownie złapany). Klasa ExceptionHandler bazowa wywoła metodę obsługi rdzeni tylko w przypadku wyjątków w górnej części stosu wywołań, ignorując starsze zagnieżdżone bloki catch. (Uproszczone wersje tych klas bazowych znajdują się w dodatku poniżej). Zarówno, jak IExceptionLogger i IExceptionHandler odbierać informacje o wyjątku za pośrednictwem elementu 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; }
}

Gdy platforma wywołuje rejestrator wyjątków lub procedurę obsługi wyjątków, zawsze udostępnia element Exception i .Request Oprócz testowania jednostkowego zawsze będzie również dostarczać element RequestContext. Rzadko udostępnia element ControllerContext i ActionContext (tylko podczas wywoływania z bloku catch dla filtrów wyjątków). Bardzo rzadko podaje element Response(tylko w niektórych przypadkach usług IIS, gdy w środku próby zapisania odpowiedzi). Należy pamiętać, że ze względu na to, że niektóre z tych właściwości mogą być null konieczne do sprawdzenia null przez konsumenta przed uzyskaniem dostępu do składowych klasy wyjątków.CatchBlock to ciąg wskazujący, który blok catch widział wyjątek. Ciągi bloków przechwycenia są następujące:

  • HttpServer (metoda SendAsync)

  • HttpControllerDispatcher (metoda SendAsync)

  • HttpBatchHandler (metoda SendAsync)

  • IExceptionFilter (przetwarzanie potoku filtru wyjątku apiController w narzędziu ExecuteAsync)

  • Host OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (do buforowania danych wyjściowych)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (dla danych wyjściowych przesyłania strumieniowego)
  • Host internetowy:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (do buforowania danych wyjściowych)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (dla danych wyjściowych przesyłania strumieniowego)
    • HttpControllerHandler.WriteErrorResponseContentAsync (w przypadku błędów odzyskiwania w trybie danych wyjściowych buforowanych)

Lista ciągów bloków przechwycenia jest również dostępna za pośrednictwem statycznych właściwości odczytu. (Podstawowy ciąg bloków przechwytywania znajdują się w statycznych elementach ExceptionCatchBlocks; pozostałe są wyświetlane w jednej klasie statycznej dla OWIN i hosta internetowego).IsTopLevelCatchBlock Jest pomocna w przypadku obserwowania zalecanego wzorca obsługi wyjątków tylko w górnej części stosu wywołań. Zamiast przekształcać wyjątki w 500 odpowiedzi w dowolnym miejscu zagnieżdżonego bloku przechwytywania, program obsługi wyjątków może zezwolić na propagację wyjątków, dopóki nie zostaną one widoczne przez hosta.

Oprócz ExceptionContextprogramu rejestrator otrzymuje jeszcze jedną informację za pośrednictwem pełnej zawartości ExceptionLoggerContext:

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

Druga właściwość , CanBeHandledumożliwia rejestratorowi zidentyfikowanie wyjątku, którego nie można obsłużyć. Gdy połączenie zostanie przerwane i nie można wysłać żadnego nowego komunikatu odpowiedzi, rejestratory będą wywoływane, ale program obsługi nie zostanie wywołany, a rejestratory mogą zidentyfikować ten scenariusz z tej właściwości.

ExceptionContextDodatkowo program obsługi pobiera jeszcze jedną właściwość, która może być ustawiona w całościExceptionHandlerContext, aby obsłużyć wyjątek:

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

Procedura obsługi wyjątków wskazuje, że obsłużyła wyjątek, ustawiając Result właściwość na wynik akcji (na przykład ExceptionResult, InternalServerErrorResult, StatusCodeResult lub wynik niestandardowy). Result Jeśli właściwość ma wartość null, wyjątek jest nieobsługiwany, a oryginalny wyjątek zostanie ponownie zgłoszony.

W przypadku wyjątków w górnej części stosu wywołań wykonaliśmy dodatkowy krok, aby upewnić się, że odpowiedź jest odpowiednia dla wywołujących interfejs API. Jeśli wyjątek propaguje się do hosta, obiekt wywołujący zobaczy żółty ekran śmierci lub inny host pod warunkiem odpowiedzi, która jest zazwyczaj HTML, a zwykle nie jest to odpowiednia odpowiedź o błędzie interfejsu API. W takich przypadkach wynik rozpoczyna się od wartości innej niż null, a tylko wtedy, gdy niestandardowa procedura obsługi wyjątków jawnie ustawia ją z powrotem na null (nieobsługiwane) spowoduje propagację wyjątku do hosta. Ustawienie wartości Result w null takich przypadkach może być przydatne w przypadku dwóch scenariuszy:

  1. OWIN hostowany internetowy interfejs API z niestandardowym oprogramowaniem pośredniczącym obsługującym wyjątki zarejestrowane przed/poza internetowym interfejsem API.
  2. Lokalne debugowanie za pośrednictwem przeglądarki, w której żółty ekran śmierci jest rzeczywiście pomocną odpowiedzią dla nieobsługiwanego wyjątku.

W przypadku zarówno rejestratorów wyjątków, jak i procedur obsługi wyjątków nie wykonujemy żadnych czynności w celu odzyskania, jeśli sam rejestrator lub program obsługi zgłasza wyjątek. (Inne niż umożliwienie propagacji wyjątku, pozostaw opinię w dolnej części tej strony, jeśli masz lepsze podejście). Umowa dotycząca rejestratorów wyjątków i procedur obsługi polega na tym, że nie powinny zezwalać na propagowanie wyjątków do swoich rozmówców; w przeciwnym razie wyjątek będzie po prostu propagowany, często przez cały czas do hosta, co powoduje błąd HTML (na przykład ASP). Żółty ekran platformy NET) wysyłany z powrotem do klienta (zwykle nie jest to preferowana opcja dla wywołujących interfejs API, którzy oczekują kodu JSON lub XML).

Przykłady

Rejestrator wyjątków śledzenia

Poniższy rejestrator wyjątków wysyła dane wyjątków do skonfigurowanych źródeł śledzenia (w tym okno Debugowanie danych wyjściowych w programie Visual Studio).

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

Niestandardowa procedura obsługi wyjątków komunikatu o błędzie

Poniższa procedura obsługi wyjątków generuje niestandardową odpowiedź na błędy dla klientów, w tym adres e-mail na potrzeby kontaktowania się z pomocą techniczną.

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

Rejestrowanie filtrów wyjątków

Jeśli do utworzenia projektu użyjesz szablonu projektu "ASP.NET MVC 4 Web Application", umieść kod konfiguracji internetowego interfejsu API wewnątrz WebApiConfig klasy w folderze App_Start :

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

        // Other configuration code...
    }
}

Dodatek: Szczegóły klasy bazowej

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