Obsługa wyjątków API ASP.NET Core Web API

280

Korzystam z programu ASP.NET Core do nowego projektu interfejsu API REST po wielu latach używania zwykłego interfejsu API sieci Web platformy ASP.NET. Nie widzę żadnego dobrego sposobu obsługi wyjątków w interfejsie API sieci Web platformy ASP.NET Core. Próbowałem zaimplementować filtr / atrybut obsługi wyjątków:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

A oto moja rejestracja filtra startowego:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

Problem polegał na tym, że gdy w moim przypadku występuje wyjątek AuthorizationFilter, nie jest on obsługiwany ErrorHandlingFilter. Spodziewałem się, że zostanie tam złapany tak, jak działa ze starym interfejsem API sieci Web ASP.NET.

Jak więc wychwycić wszystkie wyjątki aplikacji, a także wyjątki z filtrów akcji?

Andrei
źródło
3
Próbowałeś UseExceptionHandleroprogramowania pośredniego?
Paweł
Mam tutaj przykład korzystania z UseExceptionHandleroprogramowania pośredniego
Ilya Chernomordik

Odpowiedzi:

538

Wyjątek Obsługa oprogramowania pośredniego

Po wielu eksperymentach z różnymi metodami obsługi wyjątków skończyłem na oprogramowaniu pośrednim. Działa najlepiej dla mojej aplikacji ASP.NET Core Web API. Obsługuje wyjątki aplikacji, a także wyjątki od filtrów akcji, a ja mam pełną kontrolę nad obsługą wyjątków i odpowiedzią HTTP. Oto mój wyjątek dotyczący oprogramowania pośredniego:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Zarejestruj go przed MVC wStartup klasie:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Możesz dodać ślad stosu, nazwę typu wyjątku, kody błędów lub cokolwiek chcesz. Bardzo elastyczny. Oto przykład odpowiedzi wyjątku:

{ "error": "Authentication token is not valid." }

Rozważ wprowadzenie IOptions<MvcJsonOptions>tej Invokemetody, aby następnie użyć jej podczas serializacji obiektu odpowiedzi w celu wykorzystania ustawień serializacji programu ASP.NET MVC w JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)celu uzyskania lepszej spójności serializacji we wszystkich punktach końcowych.

Podejście 2

Istnieje inny nieoczywisty interfejs API o nazwie UseExceptionHandler„ok” dla prostych scenariuszy:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

To nie jest bardzo oczywisty, ale łatwy sposób na skonfigurowanie obsługi wyjątków. Jednak nadal wolę podejście pośrednie od niego, ponieważ mam większą kontrolę dzięki możliwości wstrzykiwania niezbędnych zależności.

Andrei
źródło
4
Walę głową o biurko, próbując dziś uzyskać niestandardowe oprogramowanie pośrednie, i działa w zasadzie w ten sam sposób (używam go do zarządzania jednostką pracy / transakcji dla żądania). Problem, z którym się zmagam, polega na tym, że podniesione wyjątki w „next” nie są uwięzione w oprogramowaniu pośrednim. Jak możesz sobie wyobrazić, jest to problematyczne. Co robię źle / brakuje mi? Wszelkie wskazówki lub sugestie?
brappleye3
5
@ brappleye3 - Zorientowałem się, na czym polega problem. Właśnie rejestrowałem oprogramowanie pośrednie w niewłaściwym miejscu w klasie Startup.cs. Przeprowadziłem app.UseMiddleware<ErrorHandlingMiddleware>();się tuż przedtem app.UseStaticFiles();. Wydaje się, że wyjątek został teraz poprawnie wychwycony. To prowadzi mnie do przekonania. app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();Wykonaj wewnętrzne magiczne modyfikacje oprogramowania pośredniego, aby poprawnie zamówić oprogramowanie pośrednie.
Jamadan
4
Zgadzam się, że niestandardowe oprogramowanie pośrednie może być bardzo przydatne, ale kwestionuje stosowanie wyjątków w sytuacjach NotFound, Unauthorized i BadRequest. Dlaczego po prostu nie ustawić kodu stanu (za pomocą NotFound () itp.), A następnie obsłużyć go w niestandardowym oprogramowaniu pośrednim lub za pomocą UseStatusCodePagesWithReExecute? Zobacz devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api więcej informacji
Paul Hiles
4
Jest źle, ponieważ zawsze serializuje do JSON, całkowicie ignorując negocjacje treści.
Konrad
5
@Konrad ważny punkt. Dlatego powiedziałem, że w tym przykładzie możesz zacząć, a nie wynik końcowy. Dla 99% API JSON to więcej niż wystarczające. Jeśli uważasz, że ta odpowiedź nie jest wystarczająco dobra, nie krępuj się.
Andrei
60

Najnowsze Asp.Net Core(przynajmniej od 2.2, prawdopodobnie wcześniej) ma wbudowane oprogramowanie pośrednie, które sprawia, że ​​jest nieco łatwiejsze niż implementacja w zaakceptowanej odpowiedzi:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Powinien zrobić prawie to samo, tylko trochę mniej kodu do napisania.

Ważne: Pamiętaj, aby dodać go przed UseMvc(lub UseRoutingw .Net Core 3), ponieważ kolejność jest ważna.

Ilya Chernomordik
źródło
Czy obsługuje DI jako argument dla procedury obsługi, czy też należy użyć wzorca lokalizatora usług w module obsługi?
lp
32

Najlepiej jest użyć oprogramowania pośredniego, aby uzyskać rejestrowanie, którego szukasz. Chcesz umieścić wyjątek rejestrujący w jednym oprogramowaniu pośrednim, a następnie obsługiwać strony błędów wyświetlane użytkownikowi w innym oprogramowaniu pośrednim. Pozwala to na rozdzielenie logiki i jest zgodne z projektem Microsoft opracowanym z 2 komponentami oprogramowania pośredniego. Oto dobry link do dokumentacji Microsoft: Obsługa błędów w ASP.Net Core

Dla konkretnego przykładu możesz użyć jednego z rozszerzeń oprogramowania pośredniego StatusCodePage lub utworzyć własne w ten sposób .

Tutaj możesz znaleźć przykład rejestrowania wyjątków: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Jeśli nie podoba Ci się ta konkretna implementacja, możesz także użyć oprogramowania pośredniego ELM , a oto kilka przykładów: Oprogramowanie pośrednie Elm Exception

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Jeśli to nie działa na Twoje potrzeby, zawsze możesz rzucić własny komponent Middleware, patrząc na ich implementacje ExceptionHandlerMiddleware i ElmMiddleware, aby uchwycić koncepcje budowy własnego.

Ważne jest, aby dodać oprogramowanie pośredniczące do obsługi wyjątków poniżej oprogramowania pośredniego StatusCodePages, ale przede wszystkim do innych składników oprogramowania pośredniego. W ten sposób oprogramowanie pośrednie Exception przechwytuje wyjątek, rejestruje go, a następnie zezwala na przejście do oprogramowania pośredniego StatusCodePage, które wyświetli przyjazną stronę błędu dla użytkownika.

Ashley Lee
źródło
Nie ma za co. Podałem również link do przykładu zastąpienia domyślnych UseStatusPages na krawędziach, które mogą lepiej spełnić twoje żądanie.
Ashley Lee,
1
Zauważ, że Elm nie przechowuje dzienników i zaleca się użycie Serilog lub NLog w celu zapewnienia serializacji. Zobacz dzienniki ELM znika. Czy możemy zachować go do pliku lub bazy danych?
Michael Freidgeim
2
Link jest teraz zepsuty.
Mathias Lykkegaard Lorenzen
@AshleyLee, pytam, co UseStatusCodePagesjest przydatne w implementacjach usług Web API. Brak widoków lub HTML, tylko odpowiedzi JSON ...
Paul Michalik
23

Dobrze przyjęta odpowiedź bardzo mi pomogła, ale chciałem przekazać HttpStatusCode w moim oprogramowaniu pośrednim, aby zarządzać kodem błędu w czasie wykonywania.

Według tego linku mam pomysł, aby zrobić to samo. Więc połączyłem z tym Andrei Answer. Więc mój końcowy kod jest poniżej:
1. Klasa podstawowa

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Typ klasy wyjątku niestandardowego

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Oprogramowanie pośrednie wyjątku niestandardowego

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Metoda rozszerzenia

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Skonfiguruj metodę w startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Teraz moja metoda logowania w kontrolerze konta:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

Powyżej możesz zobaczyć, czy nie znalazłem użytkownika, a następnie zgłaszając wyjątek HttpStatusCodeException, w którym przekazałem status HttpStatusCode.NotFound i komunikat niestandardowy
W oprogramowaniu pośrednim

catch (HttpStatusCodeException ex)

zostanie wywołany blok, który przekaże kontrolę

private Task HandleExceptionAsync (kontekst HttpContext, wyjątek HttpStatusCodeException)

.


Ale co, jeśli wcześniej wystąpił błąd czasu wykonywania? Do tego użyłem try catch catch, który wyrzuca wyjątek i zostanie złapany w catch (Exception wyjątekObj) i przekaże kontrolę do

Task HandleExceptionAsync (kontekst HttpContext, wyjątek Wyjątek)

metoda.

Użyłem pojedynczej klasy ErrorDetails dla zapewnienia jednolitości.

Arjun
źródło
Gdzie umieścić metodę rozszerzenia? Niestety w startup.csw void Configure(IapplicationBuilder app)otrzymuję komunikat o błędzie IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. I dodałem referencję, gdzie CustomExceptionMiddleware.csjest.
Spedo De La Rossa
nie chcesz używać wyjątków, ponieważ spowalniają one apis. wyjątki są bardzo drogie.
lnaie
@Inaie, nie mogę o tym powiedzieć ... ale wygląda na to, że nigdy nie miałeś żadnych wyjątków do
Arjun
19

Aby skonfigurować zachowanie obsługi wyjątków dla każdego typu wyjątku, możesz użyć oprogramowania pośredniego z pakietów NuGet:

Przykładowy kod:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}
Ihar Yakimush
źródło
16

Po pierwsze, dzięki Andreiowi, ponieważ swoje rozwiązanie oparłem na jego przykładzie.

Uwzględniam mój, ponieważ jest to bardziej kompletna próbka i może zaoszczędzić czytelnikom trochę czasu.

Ograniczeniem podejścia Andrei jest to, że nie obsługuje rejestrowania, przechwytywania potencjalnie użytecznych zmiennych żądań i negocjacji treści (zawsze zwróci JSON bez względu na to, czego zażądał klient - XML ​​/ zwykły tekst itp.).

Moje podejście polega na użyciu ObjectResult, który pozwala nam korzystać z funkcjonalności zapisanej w MVC.

Ten kod zapobiega również buforowaniu odpowiedzi.

Odpowiedź na błąd została ozdobiona w taki sposób, że może być serializowany przez serializator XML.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}
CountZero
źródło
9

Najpierw skonfiguruj program ASP.NET Core 2, Startupaby ponownie uruchamiał się na stronie błędów pod kątem błędów z serwera WWW i nieobsługiwanych wyjątków.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Następnie zdefiniuj typ wyjątku, który pozwoli Ci zgłaszać błędy przy użyciu kodów stanu HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Na koniec w kontrolerze strony błędu dostosuj odpowiedź na podstawie przyczyny błędu i tego, czy odpowiedź będzie widoczna bezpośrednio przez użytkownika końcowego. Ten kod zakłada, że ​​wszystkie adresy URL interfejsu API zaczynają się od /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

Program ASP.NET Core zarejestruje szczegóły błędu, za pomocą którego będziesz debugować, więc kod stanu może być wszystkim, co chcesz podać (potencjalnie niezaufanemu) requesterowi. Jeśli chcesz wyświetlić więcej informacji, możesz ulepszyć, HttpExceptionaby je podać. W przypadku błędów interfejsu API można wstawić informacje o błędzie zakodowane w formacie JSON w treści komunikatu, zastępując return StatusCode...je parametrem return Json....

Edward Brey
źródło
0

użyj oprogramowania pośredniego lub IExceptionHandlerPathFeature jest w porządku. jest inny sposób eshopie

utwórz filtr wyjątków i zarejestruj go

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
źródło