跳至頁尾內容
Iron Academy Logo
學習C#
學習C#

其他類別

C#最小API中的全域性錯誤處理

[[academy-video-youtube({"vid": "B5NsgtdwOlg", "start_time": "0", "title": "全球錯誤處理在C#精簡API中", "creator": "Tim Corey", "length": "13m 30s"})]]

一個拋出未處理異常的Web API,預設會返回一種錯誤頁面,這幫助開發者在本地除錯,也幫助陌生人映射您的呼叫堆疊。 行號、型別名稱和源文件的路徑都會流回給請求者。擷取每個錯誤的正確方法是在端點處,但它僅能在未被記住的try/catch中工作。 一個全域處理器是捕捉端點遺漏的安全網。

在他的视频"全球錯誤處理在C#精簡API中"中,Tim Corey 構建了一個故意破壞的精簡API端點,演示了沒有保護的情況下返回的開發者錯誤頁面,然後設計 app.UseExceptionHandler 以攔截任何未捕捉到的異常,並以通用的500響應返回。他還加強了為什麼端點級處理是首選途徑:全域處理器是後備,不是策略。 任何希望確保沒有堆疊追蹤離開伺服器的精簡API交付者,都會在下面找到有關的中間件設置和設計理由。

建立具有破損端點的精簡API

[1:08 - 3:01] Tim 開始了一個全新的 .NET 8 ASP.NET Core Web API 專案,名為 ErrorDemoApp。專案模板選項接近預設:開啟 HTTPS,開啟 OpenAPI,無認證,頂級語句啟用,控制器複選框未選,因為這是一個精簡API。 生成的 Program.cs 保留了 Swagger,但天氣預報範例端點及其記錄被刪除,讓文件只呈現基本內容。

取樣的地方,他新增了一個 /demo 的單一端點,其目的是要失敗:

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

using Ctrl+F5(開始無除錯)運行專案可防止 Visual Studio 除錯器截獲 throw,因此失敗像在實際 HTTP 呼叫者的面前一樣顯示。 Swagger打開,/demo端點是唯一可用的,並執行它返回500響應。 響應體包含例外型別、消息以及對 Program.cs 第18行的引用。

為什麼預設錯誤頁洩露實施細節

[3:01 - 5:00] 直接在瀏覽器中點擊 /demo(沒有 ?message= Swagger 包裝器)顯示的是開發人員例外頁面而不是 JSON 響應。 頁面渲染例外名稱、消息、發生拋出的位置的文件路徑和行號、原始例外詳細資訊,以及拋出上方的堆疊幀。 對於本地工作的開發者來說,這無疑是金礦。 對於其他任何人,這是程式碼庫的免費地圖。

Tim 的觀點直言不諱的介紹:這個頁面為幫助開發人員而存在,絕不應該傳遞給終端使用者。 它有時會被傳達出去的事實是設立全域處理器的重要原因。 即使是勤奮的團隊也終將錯過包裹每個端點在 異常處理中的某一個,錯過一個的代價是整個堆疊追蹤送到請求者手中。

首先在端點捕獲錯誤

[5:00 - 6:30] 在安裝全域處理器之前,Tim 用try/catch包裝了demo端點,以示範首選路徑。 處理器會為任何拋出的例外返回 Results.BadRequest(ex.Message)

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

結果是一個僅攜帶消息字串的400。 沒有堆疊追蹤,沒有文件路徑,沒有行號。 消息本身是否應該曝露取決於應用; 對於公共API,即使是消息也可能泄露超出團隊想要的,在這種情況下,處理器會提供一個通用字串。 本地捕捉給予端點對調用者看到內容的全權控制,包含選擇在已知失敗模式下返回比500更具體的狀態碼。

這種模式無法做到的是捕捉端點遺忘包裹的東西。任何新程式碼路徑,任何更深層的重新拋出,任何 Task 在單獨的執行緒中拋出的都會繞過端點的try/catch。 這就是中間件填補的空隙。

連接UseExceptionHandler中間件

[6:30 - 10:00]app.UseHttpsRedirection() 行的下方,通過使用 app.UseExceptionHandler 註冊處理器。 接收建構器動作的重載暴露了底層管道,讓處理器可以明確設置響應形狀:

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

在該區塊中的幾項選擇很重要。 強制將狀態碼設為500意味著調用者無法從響應號中推斷任何資訊; 無論內部異常型別為何,表面看起來都是一樣的。 迫使內容型別為 application/json,與API其餘響應匹配,這讓客戶端使用單一解析器。 IExceptionHandlerFeature 暴露了原始例外,以便實際的處理器可以記錄它; Tim 在這裡使用 Console.WriteLine 作為該專案實際攜帶的任何記錄器的替身。

最終的 WriteAsJsonAsync 呼叫返回一個匿名物件,帶有狀態碼和通用消息。 主體只說明了故障的事實而無其他內容,這正是重點。 內部診斷應寫在日誌而非響應中。

測試已處理和未處理的路徑

[10:00 - 13:14] 有try/catch依然就位時,端點運行本地路徑:Swagger顯示一個攜帶"這是一個範例例外"的400。 中間件永遠不會看到拋出,因為catch塊先解析掉了它。這是Tim預設想要的設計:本地處理器完成其工作,全域處理器則應付時露面。

移除try/catch再次運行練習後備方案。 現在相同的請求返回一個500帶有JSON正文 { "statusCode": 500, "message": "Internal Server Error" }。 響應中沒有任何內容顯示異常出現的地點或型別。 然而,Visual Studio控制台窗口顯示的是通過 Console.WriteLine 佔位符記錄的原始例外文字,包括文件路徑和行號。 完整的診斷資訊保留在開發人員可以看到的地方; 響應保持在不會泄露的地方。

這個模式貫穿於具有驗證中間件、自定義認證或任何其他管道組件的精簡API。 異常處理器位於管道的早期,抓住從稍後階段傳遞上來的內容。

總結:深入防禦

[13:14 - 13:30] 本地處理和全域處理並不是替代品; 它們是層。 本地處理器在已知失敗模式下給予端點時,能作出有意義的響應。 全域處理器確保任何本地層未成功捕獲的內容產生的響應是一致的、通用又安全的。 基於控制器的API使用相同的原則,只是語法上有所調整,而精簡API形式是應該首先習慣的,因為表面積小到只需在單個文件中看到全貌。

結論

[13:14 - 13:30] 在精簡API中設置全域錯誤處理器的三個步驟:在管道早期註冊 UseExceptionHandler,在處理器內設置響應狀態和內容型別,並撰寫刻意通用的主體以避風格細節的泄密。 配合在最有可能失敗的程式碼路徑周圍加上本地try/catch塊,您就擁有了一個深入防禦模型,全域處理器是安全網而非戰略。

範例提示:當處理器調用實際記錄器時,傳遞整個異常物件(而不僅僅是消息),這樣結構化日誌管道便能捕獲型別、堆疊以及任何內部例外。 像 Serilog 這樣的記錄器將保留所有這些作為可查詢的屬性,這意味着觸發生產500的警報帶有足夠的上下文以在本地重現,而無需任何人重新運行請求。

觀看完整影片在他的 YouTube 頻道上,以獲得在10-Minute Training系列中構建生產就緒精簡API的更多見解。

Hero Worlddot related to C#最小API中的全域性錯誤處理
Hero Affiliate related to C#最小API中的全域性錯誤處理

分享您所愛以賺取更多報酬

您是否為使用 .NET、C#、Java、Python 或 Node.js 的開發者建立內容?將您的專業知識轉化為額外收入!

Iron 支援團隊

我們線上24小時,每週5天。
聊天
電子郵件
給我打電話