Przejdź do treści stopki
Iron Academy Logo
Naucz się C#
Naucz się C#

Inne Kategorie

Globalne obsługi błędów w C# Minimal APIs

[[academy-video-youtube({"vid": "B5NsgtdwOlg", "start_time": "0", "title": "Global Error Handling in C# Minimal APIs", "creator": "Tim Corey", "length": "13m 30s"})]]

Web API, które wyrzuca niezłapany wyjątek, domyślnie zwróci stronę błędu, która pomaga deweloperowi debugować lokalnie i pomaga obcej osobie zmapować stos wywołań. Numery linii, nazwy typów i ścieżka do pliku źródłowego wracają do tego, kto wykonał żądanie. Łapanie każdego błędu na końcu, gdzie może wystąpić, jest właściwym podejściem, ale działa tylko do następnego zapomnianego try/catch. Globalny obsługiwacz to siatka bezpieczeństwa, która przechwytuje to, co przegapił końcowy punkt.

W swoim filmie "Globalne obslugiwanie bledow w C# Minimal APIs" Tim Corey buduje mala minimalna API z umyslnie uszkodzonym punktem koncowym, demonstruje strone bledu dewelopera, ktora jest zwracana bez ochrony, a nastepnie podlacza app.UseExceptionHandler, aby przechwytywac kazdy nieprzechwycony wyjatek i odpowiadac z ogolnym kodem 500. On rowniez podkresla, dlaczego obslugiwanie na poziomie punktu koncowego pozostaje preferowanym sposobem: globalny handler jest ustawiany jako rezerwowy, nie jako strategia. Każdy, kto wysyła minimalne API i chce mieć pewność, że żaden stos wywołań nigdy nie wyjdzie z serwera, znajdzie poniżej konfigurację oprogramowania pośredniego i zasadę projektowania wokół tego.

Budowanie minimalnego API z uszkodzonym punktem

[1:08 - 3:01] Tim zaczyna od świeżego projektu ASP.NET Core Web API .NET 8 o nazwie ErrorDemoApp. Opcje szablonu projektu pozostają blisko domyślnych: HTTPS włączone, OpenAPI włączone, brak uwierzytelniania, włączone instrukcje górnego poziomu i nie zaznaczony checkbox kontrolerów, ponieważ jest to API minimalne. Wygenerowany Program.cs zachowuje Swagger, ale przykład punktu prognozy pogody i jego zapis są usuwane, więc plik pokazuje tylko podstawy.

Zamiast przykładu, dodaje jeden punkt końcowy na /demo, którego jedynym celem jest niepowodzenie:

app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});
app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});

Uruchomienie projektu przy pomocy Ctrl+F5 (uruchomienie bez debugowania) trzyma debuger Visual Studio przed przechwyceniem throw, więc niepowodzenie wychodzi tak, jak dla faktycznego wywołującego HTTP. Otwiera się Swagger, a punkt końcowy /demo jest jedynym dostępnym, a jego wykonanie zwraca odpowiedź 500. Treść odpowiedzi zawiera typ wyjątku, wiadomość i odwołanie do linii 18 w Program.cs.

Dlaczego domyślna strona błędu ujawnia szczegóły implementacji

[3:01 - 5:00] Uderzenie /demo bezpośrednio w przeglądarce (bez opakowania ?message= Swagger) pokazuje stronę wyjątków programisty zamiast odpowiedzi JSON. Strona wyświetla nazwę wyjątku, wiadomość, ścieżkę pliku i numer linii, gdzie wystąpił throw, surowe szczegóły wyjątku i klatki stosu nad throw. Dla programisty pracującego lokalnie to złoto. Dla każdego innego to darmowa mapa kodu.

Punkt Tima pada bez ozdabiania: ta strona istnieje, aby pomóc deweloperom i nie powinna nigdy dotrzeć do użytkowników końcowych. Fakt, że czasami tak się dzieje, jest powodem, dla którego globalny obsługiwacz ma znaczenie. Nawet zespoły, które pilnie obejmują każdy punkt końcowy obsługą wyjątków, ostatecznie przegapią jeden, a koszt pominięcia jednego jest taki, że cały stos wywołań trafia do tego, kto zapytał.

Łapanie błędów na poziomie punktu końcowego jako pierwsze

[5:00 - 6:30] Przed zainstalowaniem globalnego obsługiwacza, Tim obejmuje punkt końcowy demo w try/catch, aby zilustrować preferowaną drogę. Handler zwraca Results.BadRequest(ex.Message) dla kazdego wyrzuconego wyjatku:

app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});
app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});

Rezultatem jest 400, które niesie tylko ciąg wiadomości. Brak stosu wywołań, brak ścieżki pliku, brak numeru linii. To, czy sama wiadomość powinna być ujawniana, zależy od aplikacji; dla publicznego API nawet wiadomość może ujawniać więcej, niż chce zespół, w takim przypadku handler podstawia ogólny ciąg znaków. Lokalne łapanie daje punktowi końcowemu pełną kontrolę nad tym, co widzi wywołujący, w tym wybór, aby zwrócić bardziej specyficzny kod statusu niż 500, gdy tryb awarii jest faktycznie znany.

To, czego ten wzorzec nie moze zrobic, to przechwytywanie tego, co punkt koncowy zapomnial zawinac. Kazda nowa sciezka kodu, kazde ponowne rzucenie wyjatku z glebszej warstwy, kazdy Task, ktory rzuca na osobnym watku, omijaja try/catch punktu koncowego. To jest luka, którą wypełnia oprogramowanie pośrednie.

Podłączenie oprogramowania pośredniego UseExceptionHandler

[6:30 - 10:00] Zaraz poniżej linii app.UseHttpsRedirection(), handler jest rejestrowany za pomocą app.UseExceptionHandler. Przeciążenie, które przyjmuje akcję konstruktora, ujawnia podstawowy pipeline, co pozwala obsługiwaczowi ustawić kształt odpowiedzi wyraźnie:

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});
app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});

Kilka wyborów w tym bloku ma znaczenie. Wymuszanie kodu statusu na 500 oznacza, że wywołujący nie może wywnioskować niczego z numeru odpowiedzi; bez względu na to, jaki był wewnętrzny typ wyjątku, powierzchnia wygląda tak samo. Wymuszanie typu zawartości na application/json dopasowuje się do reszty odpowiedzi API, co utrzymuje klientów na jednym parserze. IExceptionHandlerFeature ujawnia pierwotny wyjatek, aby prawdziwy handler mogl go zalogowac; Tim uzywa Console.WriteLine tutaj jako zastepnika dla jakiegokolwiek loggera, ktory projekt faktycznie by zawieral.

Ostateczne wywolanie WriteAsJsonAsync zwraca anonimowy obiekt z kodem stanu i ogolna wiadomoscia. Ciało nie mówi nic na temat, co nie zadziałało poza faktem, że coś rzeczywiście się nie udało, co jest sednem. Diagnostyka wewnętrzna należy do logu, nie do odpowiedzi.

Testowanie ścieżek obsłużonych i nieobsłużonych

[10:00 - 13:14] Z try/catch nadal na miejscu, punkt końcowy prowadzi lokalną ścieżką: Swagger pokazuje 400, które niesie "To jest przykładowy wyjątek". Oprogramowanie pośrednie nie widzi wyrzutu, ponieważ blok catch rozwiązuje to najpierw. Jest to projekt, który Tim chce mieć domyślnie: lokalne obsługiwacze robią swoją pracę, a globalny obsługiwacz jest uśpiony.

Usunięcie try/catch i ponowne uruchomienie ćwiczy fallback. To samo zapytanie teraz zwraca kod 500 z cialem JSON { "statusCode": 500, "message": "Internal Server Error" }. Nic w odpowiedzi nie ujawnia, gdzie wyrzucono wyjątek lub jaki to był typ. Konsola Visual Studio jednak pokazuje tekst oryginalnego wyjatku zalogowanego przez zastepnik Console.WriteLine, wlaczajac w to sciezke do pliku i numer linii. Pełna diagnostyka pozostaje tam, gdzie programiści mogą ją odczytać; odpowiedź pozostaje tam, gdzie nie może wyjść na zewnątrz.

Ten wzorzec ciągnie się do API minimalnego z middleware walidacyjnym, niestandardowym uwierzytelnianiem lub dowolnym innym komponentem pipeline. Obsługiwacz wyjątku znajduje się we wczesnej części pipeline i łapie wszystko, co propaguje się z późniejszego etapu.

Podsumowanie: Obrona w głębokościach

[13:14 - 13:30] Lokalna obsługa i globalna obsługa nie są alternatywami; są to warstwy. Lokalny handler daje końcowemu punktowi szansę odpowiedzi z sensem, gdy tryb awarii jest znany. Globalny obsługiwacz zapewnia, że wszystko, co lokalna warstwa pominęła, produkuje odpowiedź, która jest spójna, ogólna i bezpieczna. API oparte na kontrolerach korzystają z tej samej idei przy drobnych poprawkach składniowych, ale to forma API minimalna jest tą, z którą warto najpierw się zapoznać, ponieważ powierzchnia jest na tyle mała, że całość można zobaczyć w jednym pliku.

Wnioski

[13:14 - 13:30] Ustawienie globalnego obslugiwacza bledow w minimalnym API to trzy kroki: zarejestrowanie UseExceptionHandler wczesnie w potoku, ustawienie statusu odpowiedzi i typu zawartosci wewnatrz obslugiwacza oraz napisanie celowo ogolnego ciala, aby zadne szczegoly implementacji nie zostaly ujawnione. Sparuj to z lokalnymi blokami try/catch wokół ścieżek kodowych, które najczęściej mogą nie zadziałać, i masz model obrony wielostopniowej, gdzie globalny obsługiwacz jest siatką bezpieczeństwa, a nie strategią.

Przykład porady: Gdy obsługiwacz dzwoni do prawdziwego loggera, przekazuj cały obiekt wyjątku (nie tylko wiadomość), więc strukturalny pipeline logowania przechwytuje typ, stos i wszelkie wewnętrzne wyjątki. Logger, taki jak Serilog, zachowa to wszystko jako właściwości możliwe do zapytania, co oznacza, że alarm, który się uruchamia dla produkcyjnego 500, niesie wystarczający kontekst do odtworzenia lokalnie, bez konieczności ponownego uruchamiania żądania przez kogokolwiek.

Obejrzyj pełne wideo na jego kanale YouTube Channel i zdobądź więcej informacji na temat budowania gotowych do produkcji minimalnych API w serii 10-Minute Training.

Hero Worlddot related to Globalne obsługi błędów w C# Minimal APIs
Hero Affiliate related to Globalne obsługi błędów w C# Minimal APIs

Zarabiaj więcej, dzieląc się tym, co kochasz

Tworzysz treści dla deweloperów pracujących z .NET, C#, Java, Python, czy Node.js? Zamień swoją wiedzę specjalistyczną na dodatkowy dochód!

Zespół wsparcia Iron

Jesteśmy online 24 godziny, 5 dni w tygodniu.
Czat
E-mail
Zadzwoń do mnie