共用方式為


處理 ASP.NET Core 中的錯誤

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

作者:Tom Dykstra

本文說明處理 ASP.NET Core Web 應用程式錯誤的常見方法。 另請參閱處理 ASP.NET Core 控制器型 Web API 中的錯誤處理最少 API 中的錯誤

如需 Blazor 錯誤處理指引 (其會新增或取代本文中的指引),請參閱 處理 ASP.NET Core Blazor 應用程式中的錯誤

開發人員例外頁面

「開發人員例外狀況頁面」會顯示未處理要求例外狀況的詳細資訊。 它會使用 DeveloperExceptionPageMiddleware 從 HTTP 管線中擷取同步和非同步例外狀況,並產生錯誤回應。 開發人員例外狀況頁面會在中介軟體管線的早期執行,以便它可以攔截在後續中介軟體中擲回的未處理的例外狀況。

當符合以下兩項條件時,ASP.NET Core 應用程式預設會啟用開發人員例外狀況頁面:

使用舊版範本所建立的應用程式 (也就是使用 WebHost.CreateDefaultBuilder) 可以透過呼叫 app.UseDeveloperExceptionPage 來啟用開發人員例外狀況頁面。

警告

除非應用程式在開發環境中執行,否則請勿啟用 [開發人員例外狀況頁面]。 當應用程式在生產環境中執作時,請不要公開分享詳細的例外狀況資訊。 如需設定環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

「開發人員例外狀況頁面」可能包含下列有關例外狀況和要求的資訊:

  • 堆疊追蹤
  • 查詢字串參數 (如果有的話)
  • Cookie (如果有的話)
  • 標頭
  • 端點中繼資料 (如果有的話)

不保證「開發人員例外狀況頁面」提供任何資訊。 請使用記錄來取得完整的錯誤資訊。

下圖顯示了一個範例開發人員例外狀況頁面,並附有動畫來顯示索引標籤和顯示的資訊:

開發人員例外狀況頁面以動畫形式顯示所選的每個索引標籤。

為了回應具有 Accept: text/plain 標頭的要求,開發人員例外狀況頁面會傳回純文字,而不是 HTML。 例如:

Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f

例外處理常式頁面

若要針對生產環境設定自訂錯誤處理頁面,請呼叫 UseExceptionHandler。 此例外狀況處理中介軟體會:

  • 攔截並記錄未處理的例外狀況。
  • 使用指示的路徑在替代管線中重新執行要求。 如果回應已啟動,就不會重新執行要求。 範本產生的程式碼會使用 /Error 路徑重新執行要求。

警告

如果替代管線擲回自己的例外狀況,則「例外狀況處理中介軟體」會重新擲回原始的例外狀況。

因為此中介軟體可以重新執行要求管線,所以:

  • 中介軟體必須處理同一個要求的可重新進入性。 這通常意味著不是在呼叫 _next 之後清除它們的狀態,就是在 HttpContext 上快取它們的處理結果以避免重做。 在處理要求本文時,這意味著會像表單讀取器一樣緩衝或快取結果。
  • 對於在範本中使用的 UseExceptionHandler(IApplicationBuilder, String) 多載,只會修改要求路徑,並清除路由資料。 要求資料 (例如標頭、方法和項目) 都會按原樣重複使用。
  • 限定範圍的服務維持不變。

在下列範例中,UseExceptionHandler 會在非開發環境中加入例外狀況處理中介軟體:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages 應用程式範本會在 Pages 資料夾中提供一個錯誤頁面 (.cshtml) 與 PageModel 類別 (ErrorModel)。 對於 MVC 應用程式,專案範本會包含 Home 控制器的一個 Error 動作方法和一個 Error 檢視。

例外狀況處理中介軟體會使用原始 HTTP 方法重新執行要求。 如果錯誤處理常式端點僅限於一組特定的 HTTP 方法,它只會針對這些 HTTP 方法執行。 例如,使用 [HttpGet] 屬性的 MVC 控制器動作只會針對 GET 要求執行。 為了確保所有 要求都會到達自訂錯誤處理頁面,請勿將它們限制為一組特定的 HTTP 方法。

若要根據原始 HTTP 方法進行不同的例外狀況處理:

  • 針對 Razor Pages,建立多個處理常式方法。 例如,使用 OnGet 來處理 GET 例外狀況,並使用 OnPost 來處理 POST 例外狀況。
  • 針對 MVC,將 HTTP 動詞屬性套用至多個動作。 例如,使用 [HttpGet] 來處理 GET 例外狀況,並使用 [HttpPost] 來處理 POST 例外狀況。

若要允許未經驗證的使用者檢視自訂錯誤處理頁面,請確定它支援匿名存取。

存取例外狀況

使用 IExceptionHandlerPathFeature 來存取錯誤處理常式中的例外狀況和原始要求路徑。 下列範例使用 IExceptionHandlerPathFeature 來取得擲回之例外狀況的詳細資訊:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

例外處理常式 Lambda

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 lambda 可讓您在傳回回應之前存取錯誤。

下列程式碼使用 Lambda 來進行例外狀況處理:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

使用 Lambda 的另一種方法是根據例外狀況類型來設定狀態程式碼,如下列範例所示:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(new ExceptionHandlerOptions
    {
        StatusCodeSelector = ex => ex is TimeoutException
            ? StatusCodes.Status503ServiceUnavailable
            : StatusCodes.Status500InternalServerError
    });
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

IExceptionHandler

IExceptionHandler 是一個介面,可讓開發人員回撥以處理中央位置的已知例外狀況。

IExceptionHandler 實作會呼叫 IServiceCollection.AddExceptionHandler<T> 來進行註冊。 IExceptionHandler 實例的存留期是單一實例。 您可以新增多個實作,並以已註冊的順序呼叫這些實作。

如果例外處理常式處理要求,其可能會傳回 true 以停止處理。 如果任何例外處理常式未處理例外狀況,則控制項會回復為中介軟體的預設行為和選項。 系統會針對已處理與未處理的例外狀況發出不同的計量與記錄。

下列範例顯示 IExceptionHandler 的實作:

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

下列範例示範如何註冊 IExceptionHandler 實作以進行相依性插入:

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

當上面的程式碼在開發環境中執行時:

  • 會先呼叫 CustomExceptionHandler 來處理例外狀況。
  • 在記錄例外狀況之後,TryHandleAsync 方法會傳回 false,因此會顯示開發人員例外狀況頁面

在其他環境中:

  • 會先呼叫 CustomExceptionHandler 來處理例外狀況。
  • 在記錄例外狀況之後,TryHandleAsync 方法會傳回 false,因此會顯示 /Error 頁面

UseStatusCodePages

根據預設,ASP.NET Core 應用程式不會提供 HTTP 錯誤狀態碼 (例如「404 - 找不到」) 的狀態碼頁面。 當應用程式設定沒有本文的 HTTP 400-599 錯誤狀態碼時,它會傳回該狀態碼和一個空白的回應本文。 若要針對常見的錯誤狀態碼啟用預設的純文字處理常式,請在 Program.cs 中呼叫 UseStatusCodePages

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

在要求處理中介軟體之前呼叫 UseStatusCodePages。 例如,在靜態檔案中介軟體和端點中介軟體之前呼叫 UseStatusCodePages

若未使用 UseStatusCodePages,則導覽至沒有端點的 URL 會傳回與瀏覽器相依的錯誤訊息,指示無法找到端點。 當呼叫 UseStatusCodePages 時,瀏覽器會傳回下列回應:

Status Code: 404; Not Found

UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

注意

狀態碼頁面中介軟體不會攔截例外狀況。 若要提供自訂錯誤處理頁面,請使用例外狀況處理常式頁面

具格式字串的 UseStatusCodePages

若要自訂回應內容類型和文字,請使用 UseStatusCodePages 的多載,其會採用內容類型和格式字串:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

在上面的程式碼中,{0} 是錯誤碼的預留位置。

含格式化字串的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

具 Lambda 的 UseStatusCodePages

若要指定自訂錯誤處理和回應撰寫程式碼,請使用 UseStatusCodePages 的多載,其會採用 Lambda 運算式:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

含 Lambda 的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 擴充方法:

  • 傳送「302 - 已找到」狀態碼傳送給用戶端。
  • 將用戶端重新導向至 URL 範本中所提供的錯誤處理端點。 錯誤處理端點通常會顯示錯誤資訊,並傳回 HTTP 200。
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

URL 範本可為狀態碼包含一個 {0} 預留位置,如上面的程式碼中所示。 如果 URL 範本是以 ~ (波狀符號) 為開頭,該 ~ 會被應用程式的 PathBase 取代。 若在應用程式中指定端點時,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 應用程式應將用戶端重新導向至不同的端點時 (通常是在由其他應用程式處理錯誤的情況下)。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映重新導向後的端點。
  • 應用程式不應該保留並傳回原始狀態碼與初始重新導向回應時。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 擴充方法:

  • 可透過使用替代路徑重新執行要求管線來產生回應本文。
  • 不會在重新執行管線之前或之後改變狀態碼。

新的管線執行可能會改變回應的狀態碼,因為新的管線對狀態碼具有完全的控制權。 如果新的管線未改變狀態碼,則會將原始狀態碼傳送到用戶端。

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

如果在應用程式內指定端點,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 在不重新導向至其他端點的情況下處理要求。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映原始要求的端點。
  • 保留並傳回原始狀態碼與回應時。

URL 範本必須以 / 開頭,而且可為狀態碼包含一個預留位置 {0}。 若要將狀態碼作為查詢字串參數來傳遞,請將第二個引數傳入 UseStatusCodePagesWithReExecute。 例如:

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

處理錯誤的端點可以取得產生該錯誤的原始 URL,如下列範例所示:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

因為此中介軟體可以重新執行要求管線,所以:

  • 中介軟體必須處理同一個要求的可重新進入性。 這通常意味著不是在呼叫 _next 之後清除它們的狀態,就是在 HttpContext 上快取它們的處理結果以避免重做。 在處理要求本文時,這意味著會像表單讀取器一樣緩衝或快取結果。
  • 限定範圍的服務維持不變。

停用狀態碼頁面

若要停用 MVC 控制器或動作方法的狀態碼頁面,請使用 [SkipStatusCodePages] 屬性。

若要停用 Razor 頁面處理常式方法或 MVC 控制器中的特定要求狀態碼頁面,請使用 IStatusCodePagesFeature

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

例外狀況處理程式碼

例外狀況處理頁面中的程式碼也可能會擲回例外狀況。 生產錯誤頁面應該進行徹底的測試,並特別小心以避免擲回自己的例外狀況。

回應標頭

一旦傳送回應的標頭之後:

  • 應用程式就無法變更回應的狀態碼。
  • 無法執行任何例外狀況頁面或處理常式。 回應必須完成,否則會中止連線。

伺服器例外狀況處理

除了應用程式中的例外狀況處理邏輯之外,HTTP 伺服器實作也可以處理一些例外狀況。 如果伺服器在回應標頭傳送之前攔截到例外狀況,則伺服器會傳送一個沒有回應本文的 500 - Internal Server Error 回應。 如果伺服器在回應標頭傳送之後攔截到例外狀況,伺服器會關閉連線。 應用程式未處理的要求會由伺服器來處理。 當伺服器處理要求時,任何發生的例外狀況均由伺服器的例外狀況處理功能來處理。 應用程式的自訂錯誤頁面、例外狀況處理中介軟體或篩選條件並不會影響此行為。

啟動例外狀況處理

只有裝載層可以處理應用程式啟動期間發生的例外狀況。 可以將主機設定為會擷取啟動錯誤擷取詳細錯誤

只有在錯誤是於主機位址/連接埠繫結之後發生的情況下,裝載層才能顯示已擷取之啟動錯誤的錯誤頁面。 如果繫結失敗:

  • 裝載層會記錄重大例外狀況。
  • Dotnet 會處理損毀狀況。
  • 當 HTTP 伺服器是 Kestrel 時,不會顯示任何錯誤頁面。

IIS (或 Azure App Service) 或 IIS Express 上執行時,如果無法啟動處理序,模組會傳回 502.5 - 處理序失敗。 如需詳細資訊,請參閱針對 Azure App Service 和 IIS 上的 ASP.NET Core 進行疑難排解

資料庫錯誤頁面

資料庫開發人員頁面例外狀況篩選條件 AddDatabaseDeveloperPageExceptionFilter 會擷取可使用 Entity Framework Core 移轉來解決的資料庫相關例外狀況。 發生這些例外狀況時,會產生一個 HTML 回應,其中包含了解決該問題的可能動作的詳細資料。 此頁面只會在開發環境中啟用。 下列程式碼新增了資料庫開發人員頁面例外狀況篩選條件:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

例外狀況篩選條件

在 MVC 應用程式中,您能以全域設定例外狀況篩選條件,或是以每個控制器或每個動作為基礎的方式設定。 在 Razor Pages 應用程式中,您能以全域或每個頁面模型的方式設定它們。 這些篩選條件會處理在控制器動作或其他篩選條件執行期間發生但的任何未處理例外狀況。 如需詳細資訊,請參閱 ASP.NET Core 中的篩選條件

例外狀況篩選條件適合用來截獲 MVC 動作中發生的例外狀況,但是它們並不像內建例外狀況處理中介軟體 (UseExceptionHandler) 那麼有彈性。 我們建議使用 UseExceptionHandler (除非您需要根據選擇的 MVC 動作以不同方式執行錯誤處理)。

模型狀態錯誤

如需如何處理模型狀態錯誤的相關資訊,請參閱模型繫結模型驗證

問題詳細資料

問題詳細資料 並不是描述 HTTP API 錯誤的唯一回應格式,不過,它們通常會用來報告 HTTP API 的錯誤。

問題詳細資料服務會實作 IProblemDetailsService 介面 (其支援在 ASP.NET Core 中建立問題詳細資料)。 IServiceCollection 上的 AddProblemDetails(IServiceCollection) 擴充方法會註冊預設的 IProblemDetailsService 實作。

在 ASP.NET Core 應用程式中,下列中介軟體會在呼叫 AddProblemDetails 時產生問題詳細資料 HTTP 回應,除非 Accept 要求 HTTP 標頭不包含註冊的 IProblemDetailsWriter 所支援的其中一個內容類型 (預設值:application/json):

下列程式碼會將應用程式設定為產生所有 HTTP 用戶端和伺服器錯誤回應的問題詳細資料回應,而這些錯誤回應「還沒有本文內容」

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

下一節說明如何自訂問題詳細資料回應本文。

自訂問題詳細資料

可使用下列任一選項來自訂 ProblemDetails 的自動建立:

  1. 使用 ProblemDetailsOptions.CustomizeProblemDetails
  2. 使用自訂 IProblemDetailsWriter
  3. 呼叫中介軟體中的 IProblemDetailsService

CustomizeProblemDetails 作業

可以使用 CustomizeProblemDetails 來自訂產生的問題詳細資料,而自訂內容會套用至所有自動產生的問題詳細資料中。

下列程式碼使用 ProblemDetailsOptions 來設定 CustomizeProblemDetails

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

例如,HTTP Status 400 Bad Request 端點結果會產生下列的問題詳細資料回應本文:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

自訂 IProblemDetailsWriter

可以針對進階自訂來建立 IProblemDetailsWriter 實作。

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

注意: 使用自訂 IProblemDetailsWriter 時,必須在呼叫 AddRazorPagesAddControllersAddControllersWithViewsAddMvc 之前先註冊自訂 IProblemDetailsWriter

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

中介軟體的問題詳細資料

ProblemDetailsOptionsCustomizeProblemDetails 一起使用的另一種方法是在中介軟體中設定 ProblemDetails。 可以透過呼叫 IProblemDetailsService.WriteAsync 來撰寫問題詳細資料回應:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

在上面的程式碼中,Mnimal API 端點 /divide/squareroot 在錯誤輸入時傳回了預期的自訂問題回應。

API 控制器端點在錯誤輸入時傳回了預設的問題回應,而不是自訂的問題回應。 因為在呼叫 IProblemDetailsService.WriteAsync 之前,API 控制器已將「錯誤狀態碼的問題詳細資料」寫入回應串流中,而且不會再次寫入回應中,所以傳回了預設的問題回應。

下列的 ValuesController 傳回了 BadRequestResult (其寫入回應串流中,因而阻止傳回自訂的問題回應)。

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

下列的 Values3Controller 傳回了 ControllerBase.Problem,因此傳回了預期的自訂問題結果:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

產生例外狀況的 ProblemDetails 承載

請思考下列應用程式:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

在非開發環境中,當發生例外狀況時,以下是傳回給用戶端的標準 ProblemDetails 回應

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

對於大部分的應用程式,上面的程式碼就是例外狀況所需的全部內容。 不過,下節將示範如何取得更詳細的問題回應。

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 Lambda 可讓您存取錯誤,並使用 IProblemDetailsService.WriteAsync 來撰寫問題詳細資料回應:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

產生問題詳細資料的另一種方法是使用協力廠商 NuGet 套件 Hellang.Middleware.ProblemDetails (該套件可用來將例外狀況和用戶端錯誤對應至問題詳細資料)。

其他資源

作者:Tom Dykstra

本文說明處理 ASP.NET Core Web 應用程式錯誤的常見方法。 另請參閱處理 ASP.NET Core 控制器型 Web API 中的錯誤處理最少 API 中的錯誤

開發人員例外頁面

「開發人員例外狀況頁面」會顯示未處理要求例外狀況的詳細資訊。 當符合以下兩項條件時,ASP.NET Core 應用程式預設會啟用開發人員例外狀況頁面:

開發人員例外狀況頁面會在中介軟體管線的早期執行,以便它可以攔截在後續中介軟體中擲回的未處理的例外狀況。

當應用程式在生產環境中執行時,不應該公開顯示詳細的例外狀況資訊。 如需設定環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

「開發人員例外狀況頁面」可能包含下列有關例外狀況和要求的資訊:

  • 堆疊追蹤
  • 查詢字串參數 (如果有的話)
  • Cookie (如果有的話)
  • 標頭

不保證「開發人員例外狀況頁面」提供任何資訊。 請使用記錄來取得完整的錯誤資訊。

例外處理常式頁面

若要針對生產環境設定自訂錯誤處理頁面,請呼叫 UseExceptionHandler。 此例外狀況處理中介軟體會:

  • 攔截並記錄未處理的例外狀況。
  • 使用指示的路徑在替代管線中重新執行要求。 如果回應已啟動,就不會重新執行要求。 範本產生的程式碼會使用 /Error 路徑重新執行要求。

警告

如果替代管線擲回自己的例外狀況,則「例外狀況處理中介軟體」會重新擲回原始的例外狀況。

因為此中介軟體可以重新執行要求管線,所以:

  • 中介軟體必須處理同一個要求的可重新進入性。 這通常意味著不是在呼叫 _next 之後清除它們的狀態,就是在 HttpContext 上快取它們的處理結果以避免重做。 在處理要求本文時,這意味著會像表單讀取器一樣緩衝或快取結果。
  • 對於在範本中使用的 UseExceptionHandler(IApplicationBuilder, String) 多載,只會修改要求路徑,並清除路由資料。 要求資料 (例如標頭、方法和項目) 都會按原樣重複使用。
  • 限定範圍的服務維持不變。

在下列範例中,UseExceptionHandler 會在非開發環境中加入例外狀況處理中介軟體:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages 應用程式範本會在 Pages 資料夾中提供一個錯誤頁面 (.cshtml) 與 PageModel 類別 (ErrorModel)。 對於 MVC 應用程式,專案範本會包含 Home 控制器的一個 Error 動作方法和一個 Error 檢視。

例外狀況處理中介軟體會使用原始 HTTP 方法重新執行要求。 如果錯誤處理常式端點僅限於一組特定的 HTTP 方法,它只會針對這些 HTTP 方法執行。 例如,使用 [HttpGet] 屬性的 MVC 控制器動作只會針對 GET 要求執行。 為了確保所有 要求都會到達自訂錯誤處理頁面,請勿將它們限制為一組特定的 HTTP 方法。

若要根據原始 HTTP 方法進行不同的例外狀況處理:

  • 針對 Razor Pages,建立多個處理常式方法。 例如,使用 OnGet 來處理 GET 例外狀況,並使用 OnPost 來處理 POST 例外狀況。
  • 針對 MVC,將 HTTP 動詞屬性套用至多個動作。 例如,使用 [HttpGet] 來處理 GET 例外狀況,並使用 [HttpPost] 來處理 POST 例外狀況。

若要允許未經驗證的使用者檢視自訂錯誤處理頁面,請確定它支援匿名存取。

存取例外狀況

使用 IExceptionHandlerPathFeature 來存取錯誤處理常式中的例外狀況和原始要求路徑。 下列範例使用 IExceptionHandlerPathFeature 來取得擲回之例外狀況的詳細資訊:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

例外處理常式 Lambda

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 lambda 可讓您在傳回回應之前存取錯誤。

下列程式碼使用 Lambda 來進行例外狀況處理:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

IExceptionHandler

IExceptionHandler 是一個介面,可讓開發人員回撥以處理中央位置的已知例外狀況。

IExceptionHandler 實作會呼叫 IServiceCollection.AddExceptionHandler<T> 來進行註冊。 IExceptionHandler 實例的存留期是單一實例。 您可以新增多個實作,並以已註冊的順序呼叫這些實作。

如果例外處理常式處理要求,其可能會傳回 true 以停止處理。 如果任何例外處理常式未處理例外狀況,則控制項會回復為中介軟體的預設行為和選項。 系統會針對已處理與未處理的例外狀況發出不同的計量與記錄。

下列範例顯示 IExceptionHandler 的實作:

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

下列範例示範如何註冊 IExceptionHandler 實作以進行相依性插入:

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

當上面的程式碼在開發環境中執行時:

  • 會先呼叫 CustomExceptionHandler 來處理例外狀況。
  • 在記錄例外狀況之後,TryHandleException 方法會傳回 false,因此會顯示開發人員例外狀況頁面

在其他環境中:

  • 會先呼叫 CustomExceptionHandler 來處理例外狀況。
  • 在記錄例外狀況之後,TryHandleException 方法會傳回 false,因此會顯示 /Error 頁面

UseStatusCodePages

根據預設,ASP.NET Core 應用程式不會提供 HTTP 錯誤狀態碼 (例如「404 - 找不到」) 的狀態碼頁面。 當應用程式設定沒有本文的 HTTP 400-599 錯誤狀態碼時,它會傳回該狀態碼和一個空白的回應本文。 若要針對常見的錯誤狀態碼啟用預設的純文字處理常式,請在 Program.cs 中呼叫 UseStatusCodePages

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

在要求處理中介軟體之前呼叫 UseStatusCodePages。 例如,在靜態檔案中介軟體和端點中介軟體之前呼叫 UseStatusCodePages

若未使用 UseStatusCodePages,則導覽至沒有端點的 URL 會傳回與瀏覽器相依的錯誤訊息,指示無法找到端點。 當呼叫 UseStatusCodePages 時,瀏覽器會傳回下列回應:

Status Code: 404; Not Found

UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

注意

狀態碼頁面中介軟體不會攔截例外狀況。 若要提供自訂錯誤處理頁面,請使用例外狀況處理常式頁面

具格式字串的 UseStatusCodePages

若要自訂回應內容類型和文字,請使用 UseStatusCodePages 的多載,其會採用內容類型和格式字串:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

在上面的程式碼中,{0} 是錯誤碼的預留位置。

含格式化字串的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

具 Lambda 的 UseStatusCodePages

若要指定自訂錯誤處理和回應撰寫程式碼,請使用 UseStatusCodePages 的多載,其會採用 Lambda 運算式:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

含 Lambda 的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 擴充方法:

  • 傳送「302 - 已找到」狀態碼傳送給用戶端。
  • 將用戶端重新導向至 URL 範本中所提供的錯誤處理端點。 錯誤處理端點通常會顯示錯誤資訊,並傳回 HTTP 200。
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

URL 範本可為狀態碼包含一個 {0} 預留位置,如上面的程式碼中所示。 如果 URL 範本是以 ~ (波狀符號) 為開頭,該 ~ 會被應用程式的 PathBase 取代。 若在應用程式中指定端點時,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 應用程式應將用戶端重新導向至不同的端點時 (通常是在由其他應用程式處理錯誤的情況下)。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映重新導向後的端點。
  • 應用程式不應該保留並傳回原始狀態碼與初始重新導向回應時。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 擴充方法:

  • 可透過使用替代路徑重新執行要求管線來產生回應本文。
  • 不會在重新執行管線之前或之後改變狀態碼。

新的管線執行可能會改變回應的狀態碼,因為新的管線對狀態碼具有完全的控制權。 如果新的管線未改變狀態碼,則會將原始狀態碼傳送到用戶端。

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

如果在應用程式內指定端點,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 在不重新導向至其他端點的情況下處理要求。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映原始要求的端點。
  • 保留並傳回原始狀態碼與回應時。

URL 範本必須以 / 開頭,而且可為狀態碼包含一個預留位置 {0}。 若要將狀態碼作為查詢字串參數來傳遞,請將第二個引數傳入 UseStatusCodePagesWithReExecute。 例如:

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

處理錯誤的端點可以取得產生該錯誤的原始 URL,如下列範例所示:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

因為此中介軟體可以重新執行要求管線,所以:

  • 中介軟體必須處理同一個要求的可重新進入性。 這通常意味著不是在呼叫 _next 之後清除它們的狀態,就是在 HttpContext 上快取它們的處理結果以避免重做。 在處理要求本文時,這意味著會像表單讀取器一樣緩衝或快取結果。
  • 限定範圍的服務維持不變。

停用狀態碼頁面

若要停用 MVC 控制器或動作方法的狀態碼頁面,請使用 [SkipStatusCodePages] 屬性。

若要停用 Razor 頁面處理常式方法或 MVC 控制器中的特定要求狀態碼頁面,請使用 IStatusCodePagesFeature

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

例外狀況處理程式碼

例外狀況處理頁面中的程式碼也可能會擲回例外狀況。 生產錯誤頁面應該進行徹底的測試,並特別小心以避免擲回自己的例外狀況。

回應標頭

一旦傳送回應的標頭之後:

  • 應用程式就無法變更回應的狀態碼。
  • 無法執行任何例外狀況頁面或處理常式。 回應必須完成,否則會中止連線。

伺服器例外狀況處理

除了應用程式中的例外狀況處理邏輯之外,HTTP 伺服器實作也可以處理一些例外狀況。 如果伺服器在回應標頭傳送之前攔截到例外狀況,則伺服器會傳送一個沒有回應本文的 500 - Internal Server Error 回應。 如果伺服器在回應標頭傳送之後攔截到例外狀況,伺服器會關閉連線。 應用程式未處理的要求會由伺服器來處理。 當伺服器處理要求時,任何發生的例外狀況均由伺服器的例外狀況處理功能來處理。 應用程式的自訂錯誤頁面、例外狀況處理中介軟體或篩選條件並不會影響此行為。

啟動例外狀況處理

只有裝載層可以處理應用程式啟動期間發生的例外狀況。 可以將主機設定為會擷取啟動錯誤擷取詳細錯誤

只有在錯誤是於主機位址/連接埠繫結之後發生的情況下,裝載層才能顯示已擷取之啟動錯誤的錯誤頁面。 如果繫結失敗:

  • 裝載層會記錄重大例外狀況。
  • Dotnet 會處理損毀狀況。
  • 當 HTTP 伺服器是 Kestrel 時,不會顯示任何錯誤頁面。

IIS (或 Azure App Service) 或 IIS Express 上執行時,如果無法啟動處理序,模組會傳回 502.5 - 處理序失敗。 如需詳細資訊,請參閱針對 Azure App Service 和 IIS 上的 ASP.NET Core 進行疑難排解

資料庫錯誤頁面

資料庫開發人員頁面例外狀況篩選條件 AddDatabaseDeveloperPageExceptionFilter 會擷取可使用 Entity Framework Core 移轉來解決的資料庫相關例外狀況。 發生這些例外狀況時,會產生一個 HTML 回應,其中包含了解決該問題的可能動作的詳細資料。 此頁面只會在開發環境中啟用。 下列程式碼新增了資料庫開發人員頁面例外狀況篩選條件:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

例外狀況篩選條件

在 MVC 應用程式中,您能以全域設定例外狀況篩選條件,或是以每個控制器或每個動作為基礎的方式設定。 在 Razor Pages 應用程式中,您能以全域或每個頁面模型的方式設定它們。 這些篩選條件會處理在控制器動作或其他篩選條件執行期間發生但的任何未處理例外狀況。 如需詳細資訊,請參閱 ASP.NET Core 中的篩選條件

例外狀況篩選條件適合用來截獲 MVC 動作中發生的例外狀況,但是它們並不像內建例外狀況處理中介軟體 (UseExceptionHandler) 那麼有彈性。 我們建議使用 UseExceptionHandler (除非您需要根據選擇的 MVC 動作以不同方式執行錯誤處理)。

模型狀態錯誤

如需如何處理模型狀態錯誤的相關資訊,請參閱模型繫結模型驗證

問題詳細資料

問題詳細資料 並不是描述 HTTP API 錯誤的唯一回應格式,不過,它們通常會用來報告 HTTP API 的錯誤。

問題詳細資料服務會實作 IProblemDetailsService 介面 (其支援在 ASP.NET Core 中建立問題詳細資料)。 IServiceCollection 上的 AddProblemDetails(IServiceCollection) 擴充方法會註冊預設的 IProblemDetailsService 實作。

在 ASP.NET Core 應用程式中,下列中介軟體會在呼叫 AddProblemDetails 時產生問題詳細資料 HTTP 回應,除非 Accept 要求 HTTP 標頭不包含註冊的 IProblemDetailsWriter 所支援的其中一個內容類型 (預設值:application/json):

下列程式碼會將應用程式設定為產生所有 HTTP 用戶端和伺服器錯誤回應的問題詳細資料回應,而這些錯誤回應「還沒有本文內容」

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

下一節說明如何自訂問題詳細資料回應本文。

自訂問題詳細資料

可使用下列任一選項來自訂 ProblemDetails 的自動建立:

  1. 使用 ProblemDetailsOptions.CustomizeProblemDetails
  2. 使用自訂 IProblemDetailsWriter
  3. 呼叫中介軟體中的 IProblemDetailsService

CustomizeProblemDetails 作業

可以使用 CustomizeProblemDetails 來自訂產生的問題詳細資料,而自訂內容會套用至所有自動產生的問題詳細資料中。

下列程式碼使用 ProblemDetailsOptions 來設定 CustomizeProblemDetails

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

例如,HTTP Status 400 Bad Request 端點結果會產生下列的問題詳細資料回應本文:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

自訂 IProblemDetailsWriter

可以針對進階自訂來建立 IProblemDetailsWriter 實作。

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

注意: 使用自訂 IProblemDetailsWriter 時,必須在呼叫 AddRazorPagesAddControllersAddControllersWithViewsAddMvc 之前先註冊自訂 IProblemDetailsWriter

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

中介軟體的問題詳細資料

ProblemDetailsOptionsCustomizeProblemDetails 一起使用的另一種方法是在中介軟體中設定 ProblemDetails。 可以透過呼叫 IProblemDetailsService.WriteAsync 來撰寫問題詳細資料回應:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

在上面的程式碼中,Mnimal API 端點 /divide/squareroot 在錯誤輸入時傳回了預期的自訂問題回應。

API 控制器端點在錯誤輸入時傳回了預設的問題回應,而不是自訂的問題回應。 因為在呼叫 IProblemDetailsService.WriteAsync 之前,API 控制器已將「錯誤狀態碼的問題詳細資料」寫入回應串流中,而且不會再次寫入回應中,所以傳回了預設的問題回應。

下列的 ValuesController 傳回了 BadRequestResult (其寫入回應串流中,因而阻止傳回自訂的問題回應)。

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

下列的 Values3Controller 傳回了 ControllerBase.Problem,因此傳回了預期的自訂問題結果:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

產生例外狀況的 ProblemDetails 承載

請思考下列應用程式:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

在非開發環境中,當發生例外狀況時,以下是傳回給用戶端的標準 ProblemDetails 回應

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

對於大部分的應用程式,上面的程式碼就是例外狀況所需的全部內容。 不過,下節將示範如何取得更詳細的問題回應。

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 Lambda 可讓您存取錯誤,並使用 IProblemDetailsService.WriteAsync 來撰寫問題詳細資料回應:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

產生問題詳細資料的另一種方法是使用協力廠商 NuGet 套件 Hellang.Middleware.ProblemDetails (該套件可用來將例外狀況和用戶端錯誤對應至問題詳細資料)。

其他資源

作者:Tom Dykstra

本文說明處理 ASP.NET Core Web 應用程式錯誤的常見方法。 另請參閱處理 ASP.NET Core 控制器型 Web API 中的錯誤處理最少 API 中的錯誤

開發人員例外頁面

「開發人員例外狀況頁面」會顯示未處理要求例外狀況的詳細資訊。 當符合以下兩項條件時,ASP.NET Core 應用程式預設會啟用開發人員例外狀況頁面:

開發人員例外狀況頁面會在中介軟體管線的早期執行,以便它可以攔截在後續中介軟體中擲回的未處理的例外狀況。

當應用程式在生產環境中執行時,不應該公開顯示詳細的例外狀況資訊。 如需設定環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

「開發人員例外狀況頁面」可能包含下列有關例外狀況和要求的資訊:

  • 堆疊追蹤
  • 查詢字串參數 (如果有的話)
  • Cookie (如果有的話)
  • 標頭

不保證「開發人員例外狀況頁面」提供任何資訊。 請使用記錄來取得完整的錯誤資訊。

例外處理常式頁面

若要針對生產環境設定自訂錯誤處理頁面,請呼叫 UseExceptionHandler。 此例外狀況處理中介軟體會:

  • 攔截並記錄未處理的例外狀況。
  • 使用指示的路徑在替代管線中重新執行要求。 如果回應已啟動,就不會重新執行要求。 範本產生的程式碼會使用 /Error 路徑重新執行要求。

警告

如果替代管線擲回自己的例外狀況,則「例外狀況處理中介軟體」會重新擲回原始的例外狀況。

因為此中介軟體可以重新執行要求管線,所以:

  • 中介軟體必須處理同一個要求的可重新進入性。 這通常意味著不是在呼叫 _next 之後清除它們的狀態,就是在 HttpContext 上快取它們的處理結果以避免重做。 在處理要求本文時,這意味著會像表單讀取器一樣緩衝或快取結果。
  • 對於在範本中使用的 UseExceptionHandler(IApplicationBuilder, String) 多載,只會修改要求路徑,並清除路由資料。 要求資料 (例如標頭、方法和項目) 都會按原樣重複使用。
  • 限定範圍的服務維持不變。

在下列範例中,UseExceptionHandler 會在非開發環境中加入例外狀況處理中介軟體:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages 應用程式範本會在 Pages 資料夾中提供一個錯誤頁面 (.cshtml) 與 PageModel 類別 (ErrorModel)。 對於 MVC 應用程式,專案範本會包含 Home 控制器的一個 Error 動作方法和一個 Error 檢視。

例外狀況處理中介軟體會使用原始 HTTP 方法重新執行要求。 如果錯誤處理常式端點僅限於一組特定的 HTTP 方法,它只會針對這些 HTTP 方法執行。 例如,使用 [HttpGet] 屬性的 MVC 控制器動作只會針對 GET 要求執行。 為了確保所有 要求都會到達自訂錯誤處理頁面,請勿將它們限制為一組特定的 HTTP 方法。

若要根據原始 HTTP 方法進行不同的例外狀況處理:

  • 針對 Razor Pages,建立多個處理常式方法。 例如,使用 OnGet 來處理 GET 例外狀況,並使用 OnPost 來處理 POST 例外狀況。
  • 針對 MVC,將 HTTP 動詞屬性套用至多個動作。 例如,使用 [HttpGet] 來處理 GET 例外狀況,並使用 [HttpPost] 來處理 POST 例外狀況。

若要允許未經驗證的使用者檢視自訂錯誤處理頁面,請確定它支援匿名存取。

存取例外狀況

使用 IExceptionHandlerPathFeature 來存取錯誤處理常式中的例外狀況和原始要求路徑。 下列範例使用 IExceptionHandlerPathFeature 來取得擲回之例外狀況的詳細資訊:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

例外處理常式 Lambda

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 lambda 可讓您在傳回回應之前存取錯誤。

下列程式碼使用 Lambda 來進行例外狀況處理:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

UseStatusCodePages

根據預設,ASP.NET Core 應用程式不會提供 HTTP 錯誤狀態碼 (例如「404 - 找不到」) 的狀態碼頁面。 當應用程式設定沒有本文的 HTTP 400-599 錯誤狀態碼時,它會傳回該狀態碼和一個空白的回應本文。 若要針對常見的錯誤狀態碼啟用預設的純文字處理常式,請在 Program.cs 中呼叫 UseStatusCodePages

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

在要求處理中介軟體之前呼叫 UseStatusCodePages。 例如,在靜態檔案中介軟體和端點中介軟體之前呼叫 UseStatusCodePages

若未使用 UseStatusCodePages,則導覽至沒有端點的 URL 會傳回與瀏覽器相依的錯誤訊息,指示無法找到端點。 當呼叫 UseStatusCodePages 時,瀏覽器會傳回下列回應:

Status Code: 404; Not Found

UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

注意

狀態碼頁面中介軟體不會攔截例外狀況。 若要提供自訂錯誤處理頁面,請使用例外狀況處理常式頁面

具格式字串的 UseStatusCodePages

若要自訂回應內容類型和文字,請使用 UseStatusCodePages 的多載,其會採用內容類型和格式字串:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

在上面的程式碼中,{0} 是錯誤碼的預留位置。

含格式化字串的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

具 Lambda 的 UseStatusCodePages

若要指定自訂錯誤處理和回應撰寫程式碼,請使用 UseStatusCodePages 的多載,其會採用 Lambda 運算式:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

含 Lambda 的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 擴充方法:

  • 傳送「302 - 已找到」狀態碼傳送給用戶端。
  • 將用戶端重新導向至 URL 範本中所提供的錯誤處理端點。 錯誤處理端點通常會顯示錯誤資訊,並傳回 HTTP 200。
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

URL 範本可為狀態碼包含一個 {0} 預留位置,如上面的程式碼中所示。 如果 URL 範本是以 ~ (波狀符號) 為開頭,該 ~ 會被應用程式的 PathBase 取代。 若在應用程式中指定端點時,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 應用程式應將用戶端重新導向至不同的端點時 (通常是在由其他應用程式處理錯誤的情況下)。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映重新導向後的端點。
  • 應用程式不應該保留並傳回原始狀態碼與初始重新導向回應時。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 擴充方法:

  • 可透過使用替代路徑重新執行要求管線來產生回應本文。
  • 不會在重新執行管線之前或之後改變狀態碼。

新的管線執行可能會改變回應的狀態碼,因為新的管線對狀態碼具有完全的控制權。 如果新的管線未改變狀態碼,則會將原始狀態碼傳送到用戶端。

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

如果在應用程式內指定端點,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 在不重新導向至其他端點的情況下處理要求。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映原始要求的端點。
  • 保留並傳回原始狀態碼與回應時。

URL 範本必須以 / 開頭,而且可為狀態碼包含一個預留位置 {0}。 若要將狀態碼作為查詢字串參數來傳遞,請將第二個引數傳入 UseStatusCodePagesWithReExecute。 例如:

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

處理錯誤的端點可以取得產生該錯誤的原始 URL,如下列範例所示:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}

因為此中介軟體可以重新執行要求管線,所以:

  • 中介軟體必須處理同一個要求的可重新進入性。 這通常意味著不是在呼叫 _next 之後清除它們的狀態,就是在 HttpContext 上快取它們的處理結果以避免重做。 在處理要求本文時,這意味著會像表單讀取器一樣緩衝或快取結果。
  • 限定範圍的服務維持不變。

停用狀態碼頁面

若要停用 MVC 控制器或動作方法的狀態碼頁面,請使用 [SkipStatusCodePages] 屬性。

若要停用 Razor 頁面處理常式方法或 MVC 控制器中的特定要求狀態碼頁面,請使用 IStatusCodePagesFeature

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

例外狀況處理程式碼

例外狀況處理頁面中的程式碼也可能會擲回例外狀況。 生產錯誤頁面應該進行徹底的測試,並特別小心以避免擲回自己的例外狀況。

回應標頭

一旦傳送回應的標頭之後:

  • 應用程式就無法變更回應的狀態碼。
  • 無法執行任何例外狀況頁面或處理常式。 回應必須完成,否則會中止連線。

伺服器例外狀況處理

除了應用程式中的例外狀況處理邏輯之外,HTTP 伺服器實作也可以處理一些例外狀況。 如果伺服器在回應標頭傳送之前攔截到例外狀況,則伺服器會傳送一個沒有回應本文的 500 - Internal Server Error 回應。 如果伺服器在回應標頭傳送之後攔截到例外狀況,伺服器會關閉連線。 應用程式未處理的要求會由伺服器來處理。 當伺服器處理要求時,任何發生的例外狀況均由伺服器的例外狀況處理功能來處理。 應用程式的自訂錯誤頁面、例外狀況處理中介軟體或篩選條件並不會影響此行為。

啟動例外狀況處理

只有裝載層可以處理應用程式啟動期間發生的例外狀況。 可以將主機設定為會擷取啟動錯誤擷取詳細錯誤

只有在錯誤是於主機位址/連接埠繫結之後發生的情況下,裝載層才能顯示已擷取之啟動錯誤的錯誤頁面。 如果繫結失敗:

  • 裝載層會記錄重大例外狀況。
  • Dotnet 會處理損毀狀況。
  • 當 HTTP 伺服器是 Kestrel 時,不會顯示任何錯誤頁面。

IIS (或 Azure App Service) 或 IIS Express 上執行時,如果無法啟動處理序,模組會傳回 502.5 - 處理序失敗。 如需詳細資訊,請參閱針對 Azure App Service 和 IIS 上的 ASP.NET Core 進行疑難排解

資料庫錯誤頁面

資料庫開發人員頁面例外狀況篩選條件 AddDatabaseDeveloperPageExceptionFilter 會擷取可使用 Entity Framework Core 移轉來解決的資料庫相關例外狀況。 發生這些例外狀況時,會產生一個 HTML 回應,其中包含了解決該問題的可能動作的詳細資料。 此頁面只會在開發環境中啟用。 下列程式碼新增了資料庫開發人員頁面例外狀況篩選條件:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

例外狀況篩選條件

在 MVC 應用程式中,您能以全域設定例外狀況篩選條件,或是以每個控制器或每個動作為基礎的方式設定。 在 Razor Pages 應用程式中,您能以全域或每個頁面模型的方式設定它們。 這些篩選條件會處理在控制器動作或其他篩選條件執行期間發生但的任何未處理例外狀況。 如需詳細資訊,請參閱 ASP.NET Core 中的篩選條件

例外狀況篩選條件適合用來截獲 MVC 動作中發生的例外狀況,但是它們並不像內建例外狀況處理中介軟體 (UseExceptionHandler) 那麼有彈性。 我們建議使用 UseExceptionHandler (除非您需要根據選擇的 MVC 動作以不同方式執行錯誤處理)。

模型狀態錯誤

如需如何處理模型狀態錯誤的相關資訊,請參閱模型繫結模型驗證

問題詳細資料

問題詳細資料 並不是描述 HTTP API 錯誤的唯一回應格式,不過,它們通常會用來報告 HTTP API 的錯誤。

問題詳細資料服務會實作 IProblemDetailsService 介面 (其支援在 ASP.NET Core 中建立問題詳細資料)。 IServiceCollection 上的 AddProblemDetails(IServiceCollection) 擴充方法會註冊預設的 IProblemDetailsService 實作。

在 ASP.NET Core 應用程式中,下列中介軟體會在呼叫 AddProblemDetails 時產生問題詳細資料 HTTP 回應,除非 Accept 要求 HTTP 標頭不包含註冊的 IProblemDetailsWriter 所支援的其中一個內容類型 (預設值:application/json):

下列程式碼會將應用程式設定為產生所有 HTTP 用戶端和伺服器錯誤回應的問題詳細資料回應,而這些錯誤回應「還沒有本文內容」

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

下一節說明如何自訂問題詳細資料回應本文。

自訂問題詳細資料

可使用下列任一選項來自訂 ProblemDetails 的自動建立:

  1. 使用 ProblemDetailsOptions.CustomizeProblemDetails
  2. 使用自訂 IProblemDetailsWriter
  3. 呼叫中介軟體中的 IProblemDetailsService

CustomizeProblemDetails 作業

可以使用 CustomizeProblemDetails 來自訂產生的問題詳細資料,而自訂內容會套用至所有自動產生的問題詳細資料中。

下列程式碼使用 ProblemDetailsOptions 來設定 CustomizeProblemDetails

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

例如,HTTP Status 400 Bad Request 端點結果會產生下列的問題詳細資料回應本文:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

自訂 IProblemDetailsWriter

可以針對進階自訂來建立 IProblemDetailsWriter 實作。

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

注意: 使用自訂 IProblemDetailsWriter 時,必須在呼叫 AddRazorPagesAddControllersAddControllersWithViewsAddMvc 之前先註冊自訂 IProblemDetailsWriter

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();

中介軟體的問題詳細資料

ProblemDetailsOptionsCustomizeProblemDetails 一起使用的另一種方法是在中介軟體中設定 ProblemDetails。 可以透過呼叫 IProblemDetailsService.WriteAsync 來撰寫問題詳細資料回應:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();

在上面的程式碼中,Mnimal API 端點 /divide/squareroot 在錯誤輸入時傳回了預期的自訂問題回應。

API 控制器端點在錯誤輸入時傳回了預設的問題回應,而不是自訂的問題回應。 因為在呼叫 IProblemDetailsService.WriteAsync 之前,API 控制器已將「錯誤狀態碼的問題詳細資料」寫入回應串流中,而且不會再次寫入回應中,所以傳回了預設的問題回應。

下列的 ValuesController 傳回了 BadRequestResult (其寫入回應串流中,因而阻止傳回自訂的問題回應)。

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

下列的 Values3Controller 傳回了 ControllerBase.Problem,因此傳回了預期的自訂問題結果:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}

產生例外狀況的 ProblemDetails 承載

請思考下列應用程式:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

在非開發環境中,當發生例外狀況時,以下是傳回給用戶端的標準 ProblemDetails 回應

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

對於大部分的應用程式,上面的程式碼就是例外狀況所需的全部內容。 不過,下節將示範如何取得更詳細的問題回應。

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 Lambda 可讓您存取錯誤,並使用 IProblemDetailsService.WriteAsync 來撰寫問題詳細資料回應:

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

產生問題詳細資料的另一種方法是使用協力廠商 NuGet 套件 Hellang.Middleware.ProblemDetails (該套件可用來將例外狀況和用戶端錯誤對應至問題詳細資料)。

其他資源

作者:Tom Dykstra

本文說明處理 ASP.NET Core Web 應用程式錯誤的常見方法。 如需 Web API,請參閱處理 ASP.NET Core 控制器型 Web API 中的錯誤

開發人員例外頁面

「開發人員例外狀況頁面」會顯示未處理要求例外狀況的詳細資訊。 當符合以下兩項條件時,ASP.NET Core 應用程式預設會啟用開發人員例外狀況頁面:

開發人員例外狀況頁面會在中介軟體管線的早期執行,以便它可以攔截在後續中介軟體中擲回的未處理的例外狀況。

當應用程式在生產環境中執行時,不應該公開顯示詳細的例外狀況資訊。 如需設定環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

「開發人員例外狀況頁面」可能包含下列有關例外狀況和要求的資訊:

  • 堆疊追蹤
  • 查詢字串參數 (如果有的話)
  • Cookie (如果有的話)
  • 標頭

不保證「開發人員例外狀況頁面」提供任何資訊。 請使用記錄來取得完整的錯誤資訊。

例外處理常式頁面

若要針對生產環境設定自訂錯誤處理頁面,請呼叫 UseExceptionHandler。 此例外狀況處理中介軟體會:

  • 攔截並記錄未處理的例外狀況。
  • 使用指示的路徑在替代管線中重新執行要求。 如果回應已啟動,就不會重新執行要求。 範本產生的程式碼會使用 /Error 路徑重新執行要求。

警告

如果替代管線擲回自己的例外狀況,則「例外狀況處理中介軟體」會重新擲回原始的例外狀況。

在下列範例中,UseExceptionHandler 會在非開發環境中加入例外狀況處理中介軟體:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages 應用程式範本會在 Pages 資料夾中提供一個錯誤頁面 (.cshtml) 與 PageModel 類別 (ErrorModel)。 對於 MVC 應用程式,專案範本會包含 Home 控制器的一個 Error 動作方法和一個 Error 檢視。

例外狀況處理中介軟體會使用原始 HTTP 方法重新執行要求。 如果錯誤處理常式端點僅限於一組特定的 HTTP 方法,它只會針對這些 HTTP 方法執行。 例如,使用 [HttpGet] 屬性的 MVC 控制器動作只會針對 GET 要求執行。 為了確保所有 要求都會到達自訂錯誤處理頁面,請勿將它們限制為一組特定的 HTTP 方法。

若要根據原始 HTTP 方法進行不同的例外狀況處理:

  • 針對 Razor Pages,建立多個處理常式方法。 例如,使用 OnGet 來處理 GET 例外狀況,並使用 OnPost 來處理 POST 例外狀況。
  • 針對 MVC,將 HTTP 動詞屬性套用至多個動作。 例如,使用 [HttpGet] 來處理 GET 例外狀況,並使用 [HttpPost] 來處理 POST 例外狀況。

若要允許未經驗證的使用者檢視自訂錯誤處理頁面,請確定它支援匿名存取。

存取例外狀況

使用 IExceptionHandlerPathFeature 來存取錯誤處理常式中的例外狀況和原始要求路徑。 下列範例使用 IExceptionHandlerPathFeature 來取得擲回之例外狀況的詳細資訊:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

例外處理常式 Lambda

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 lambda 可讓您在傳回回應之前存取錯誤。

下列程式碼使用 Lambda 來進行例外狀況處理:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

UseStatusCodePages

根據預設,ASP.NET Core 應用程式不會提供 HTTP 錯誤狀態碼 (例如「404 - 找不到」) 的狀態碼頁面。 當應用程式設定沒有本文的 HTTP 400-599 錯誤狀態碼時,它會傳回該狀態碼和一個空白的回應本文。 若要針對常見的錯誤狀態碼啟用預設的純文字處理常式,請在 Program.cs 中呼叫 UseStatusCodePages

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

在要求處理中介軟體之前呼叫 UseStatusCodePages。 例如,在靜態檔案中介軟體和端點中介軟體之前呼叫 UseStatusCodePages

若未使用 UseStatusCodePages,則導覽至沒有端點的 URL 會傳回與瀏覽器相依的錯誤訊息,指示無法找到端點。 當呼叫 UseStatusCodePages 時,瀏覽器會傳回下列回應:

Status Code: 404; Not Found

UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

注意

狀態碼頁面中介軟體不會攔截例外狀況。 若要提供自訂錯誤處理頁面,請使用例外狀況處理常式頁面

具格式字串的 UseStatusCodePages

若要自訂回應內容類型和文字,請使用 UseStatusCodePages 的多載,其會採用內容類型和格式字串:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

在上面的程式碼中,{0} 是錯誤碼的預留位置。

含格式化字串的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

具 Lambda 的 UseStatusCodePages

若要指定自訂錯誤處理和回應撰寫程式碼,請使用 UseStatusCodePages 的多載,其會採用 Lambda 運算式:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

含 Lambda 的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 擴充方法:

  • 傳送「302 - 已找到」狀態碼傳送給用戶端。
  • 將用戶端重新導向至 URL 範本中所提供的錯誤處理端點。 錯誤處理端點通常會顯示錯誤資訊,並傳回 HTTP 200。
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

URL 範本可為狀態碼包含一個 {0} 預留位置,如上面的程式碼中所示。 如果 URL 範本是以 ~ (波狀符號) 為開頭,該 ~ 會被應用程式的 PathBase 取代。 若在應用程式中指定端點時,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 應用程式應將用戶端重新導向至不同的端點時 (通常是在由其他應用程式處理錯誤的情況下)。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映重新導向後的端點。
  • 應用程式不應該保留並傳回原始狀態碼與初始重新導向回應時。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 擴充方法:

  • 傳回原始狀態碼給用戶端。
  • 可透過使用替代路徑重新執行要求管線來產生回應本文。
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

如果在應用程式內指定端點,請為該端點建立一個 MVC 檢視或 Razor 頁面。

此方法通常是在下列應用程式相關情況下使用:

  • 在不重新導向至其他端點的情況下處理要求。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映原始要求的端點。
  • 保留並傳回原始狀態碼與回應時。

URL 範本必須以 / 開頭,而且可為狀態碼包含一個預留位置 {0}。 若要將狀態碼作為查詢字串參數來傳遞,請將第二個引數傳入 UseStatusCodePagesWithReExecute。 例如:

app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

處理錯誤的端點可以取得產生該錯誤的原始 URL,如下列範例所示:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}

停用狀態碼頁面

若要停用 MVC 控制器或動作方法的狀態碼頁面,請使用 [SkipStatusCodePages] 屬性。

若要停用 Razor 頁面處理常式方法或 MVC 控制器中的特定要求狀態碼頁面,請使用 IStatusCodePagesFeature

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

例外狀況處理程式碼

例外狀況處理頁面中的程式碼也可能會擲回例外狀況。 生產錯誤頁面應該進行徹底的測試,並特別小心以避免擲回自己的例外狀況。

回應標頭

一旦傳送回應的標頭之後:

  • 應用程式就無法變更回應的狀態碼。
  • 無法執行任何例外狀況頁面或處理常式。 回應必須完成,否則會中止連線。

伺服器例外狀況處理

除了應用程式中的例外狀況處理邏輯之外,HTTP 伺服器實作也可以處理一些例外狀況。 如果伺服器在回應標頭傳送之前攔截到例外狀況,則伺服器會傳送一個沒有回應本文的 500 - Internal Server Error 回應。 如果伺服器在回應標頭傳送之後攔截到例外狀況,伺服器會關閉連線。 應用程式未處理的要求會由伺服器來處理。 當伺服器處理要求時,任何發生的例外狀況均由伺服器的例外狀況處理功能來處理。 應用程式的自訂錯誤頁面、例外狀況處理中介軟體或篩選條件並不會影響此行為。

啟動例外狀況處理

只有裝載層可以處理應用程式啟動期間發生的例外狀況。 可以將主機設定為會擷取啟動錯誤擷取詳細錯誤

只有在錯誤是於主機位址/連接埠繫結之後發生的情況下,裝載層才能顯示已擷取之啟動錯誤的錯誤頁面。 如果繫結失敗:

  • 裝載層會記錄重大例外狀況。
  • Dotnet 會處理損毀狀況。
  • 當 HTTP 伺服器是 Kestrel 時,不會顯示任何錯誤頁面。

IIS (或 Azure App Service) 或 IIS Express 上執行時,如果無法啟動處理序,模組會傳回 502.5 - 處理序失敗。 如需詳細資訊,請參閱針對 Azure App Service 和 IIS 上的 ASP.NET Core 進行疑難排解

資料庫錯誤頁面

資料庫開發人員頁面例外狀況篩選條件 AddDatabaseDeveloperPageExceptionFilter 會擷取可使用 Entity Framework Core 移轉來解決的資料庫相關例外狀況。 發生這些例外狀況時,會產生一個 HTML 回應,其中包含了解決該問題的可能動作的詳細資料。 此頁面只會在開發環境中啟用。 下列程式碼新增了資料庫開發人員頁面例外狀況篩選條件:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

例外狀況篩選條件

在 MVC 應用程式中,您能以全域設定例外狀況篩選條件,或是以每個控制器或每個動作為基礎的方式設定。 在 Razor Pages 應用程式中,您能以全域或每個頁面模型的方式設定它們。 這些篩選條件會處理在控制器動作或其他篩選條件執行期間發生但的任何未處理例外狀況。 如需詳細資訊,請參閱 ASP.NET Core 中的篩選條件

例外狀況篩選條件適合用來截獲 MVC 動作中發生的例外狀況,但是它們並不像內建例外狀況處理中介軟體 (UseExceptionHandler) 那麼有彈性。 我們建議使用 UseExceptionHandler (除非您需要根據選擇的 MVC 動作以不同方式執行錯誤處理)。

模型狀態錯誤

如需如何處理模型狀態錯誤的相關資訊,請參閱模型繫結模型驗證

其他資源

作者:Kirk LarkinTom DykstraSteve Smith

本文說明處理 ASP.NET Core Web 應用程式錯誤的常見方法。 如需 Web API,請參閱處理 ASP.NET Core 控制器型 Web API 中的錯誤

檢視或下載範例程式碼。 (如何下載。)測試範例應用程式時,F12 瀏覽器開發人員工具上的網路索引標籤很有用。

開發人員例外狀況頁面

「開發人員例外狀況頁面」會顯示未處理要求例外狀況的詳細資訊。 ASP.NET Core 範本會產生下列程式碼:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

當應用程式在開發環境中執行時,上面反白顯示的程式碼會啟用開發人員例外狀況頁面。

範本會將 UseDeveloperExceptionPage 放在中介軟體管線的早期,以便它可以攔截後續中介軟體中擲回未處理的例外狀況。

當應用程式在開發環境中執行時,上面的程式碼才會啟用開發人員例外狀況頁面。 當應用程式在生產環境中執行時,不應該公開顯示詳細的例外狀況資訊。 如需設定環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

「開發人員例外狀況頁面」可能包含下列有關例外狀況和要求的資訊:

  • 堆疊追蹤
  • 查詢字串參數 (如果有的話)
  • Cookie (如果有的話)
  • 標頭

不保證「開發人員例外狀況頁面」提供任何資訊。 請使用記錄來取得完整的錯誤資訊。

例外處理常式頁面

若要針對生產環境設定自訂錯誤處理頁面,請呼叫 UseExceptionHandler。 此例外狀況處理中介軟體會:

  • 攔截並記錄未處理的例外狀況。
  • 使用指示的路徑在替代管線中重新執行要求。 如果回應已啟動,就不會重新執行要求。 範本產生的程式碼會使用 /Error 路徑重新執行要求。

警告

如果替代管線擲回自己的例外狀況,則「例外狀況處理中介軟體」會重新擲回原始的例外狀況。

在下列範例中,UseExceptionHandler 會在非開發環境中加入例外狀況處理中介軟體:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages 應用程式範本會在 Pages 資料夾中提供一個錯誤頁面 (.cshtml) 與 PageModel 類別 (ErrorModel)。 對於 MVC 應用程式,專案範本會包含 Home 控制器的一個 Error 動作方法和一個 Error 檢視。

例外狀況處理中介軟體會使用原始 HTTP 方法重新執行要求。 如果錯誤處理常式端點僅限於一組特定的 HTTP 方法,它只會針對這些 HTTP 方法執行。 例如,使用 [HttpGet] 屬性的 MVC 控制器動作只會針對 GET 要求執行。 為了確保所有 要求都會到達自訂錯誤處理頁面,請勿將它們限制為一組特定的 HTTP 方法。

若要根據原始 HTTP 方法進行不同的例外狀況處理:

  • 針對 Razor Pages,建立多個處理常式方法。 例如,使用 OnGet 來處理 GET 例外狀況,並使用 OnPost 來處理 POST 例外狀況。
  • 針對 MVC,將 HTTP 動詞屬性套用至多個動作。 例如,使用 [HttpGet] 來處理 GET 例外狀況,並使用 [HttpPost] 來處理 POST 例外狀況。

若要允許未經驗證的使用者檢視自訂錯誤處理頁面,請確定它支援匿名存取。

存取例外狀況

使用 IExceptionHandlerPathFeature 來存取錯誤處理常式中的例外狀況和原始要求路徑。 下列程式碼將 ExceptionMessage 新增至 ASP.NET Core 範本所產生的預設值 Pages/Error.cshtml.cs

[ResponseCache(Duration=0, Location=ResponseCacheLocation.None, NoStore=true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }
    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
        HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
            _logger.LogError(ExceptionMessage);
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

若要在範例應用程式中測試例外狀況:

  • 將環境設定為生產環境。
  • Program.cs 中的 webBuilder.UseStartup<Startup>(); 移除註解。
  • 在 home 頁面上,選取 [觸發例外狀況]

例外處理常式 Lambda

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 lambda 可讓您在傳回回應之前存取錯誤。

下列程式碼使用 Lambda 來進行例外狀況處理:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorApp =>
        {
            errorApp.Run(async context =>
            {
                context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;;
                context.Response.ContentType = "text/html";

                await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
                await context.Response.WriteAsync("ERROR!<br><br>\r\n");

                var exceptionHandlerPathFeature =
                    context.Features.Get<IExceptionHandlerPathFeature>();

                if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
                {
                    await context.Response.WriteAsync(
                                              "File error thrown!<br><br>\r\n");
                }

                await context.Response.WriteAsync(
                                              "<a href=\"/\">Home</a><br>\r\n");
                await context.Response.WriteAsync("</body></html>\r\n");
                await context.Response.WriteAsync(new string(' ', 512)); 
            });
        });
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

警告

IExceptionHandlerFeatureIExceptionHandlerPathFeature 提供錯誤資訊給用戶端。 提供錯誤有安全性風險。

若要在範例應用程式中測試例外狀況處理 Lambda:

  • 將環境設定為生產環境。
  • Program.cs 中的 webBuilder.UseStartup<StartupLambda>(); 移除註解。
  • 在 home 頁面上,選取 [觸發例外狀況]

UseStatusCodePages

根據預設,ASP.NET Core 應用程式不會提供 HTTP 錯誤狀態碼 (例如「404 - 找不到」) 的狀態碼頁面。 當應用程式設定沒有本文的 HTTP 400-599 錯誤狀態碼時,它會傳回該狀態碼和一個空白的回應本文。 若要提供狀態碼頁面,請使用狀態碼頁面中介軟體。 若要針對常見的錯誤狀態碼啟用預設的純文字處理常式,請呼叫 Startup.Configure 方法中的 UseStatusCodePages

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

在要求處理中介軟體之前呼叫 UseStatusCodePages。 例如,在靜態檔案中介軟體和端點中介軟體之前呼叫 UseStatusCodePages

若未使用 UseStatusCodePages,則巡覽至沒有端點的 URL 會傳回與瀏覽器相依的錯誤訊息,指示無法找到端點。 例如,巡覽至 Home/Privacy2。 呼叫 UseStatusCodePages 時,瀏覽器會傳回:

Status Code: 404; Not Found

UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

若要在範例應用程式中測試 UseStatusCodePages

  • 將環境設定為生產環境。
  • Program.cs 中的 webBuilder.UseStartup<StartupUseStatusCodePages>(); 移除註解。
  • 在 home 頁面的 home 頁面上選取連結。

注意

狀態碼頁面中介軟體不會攔截例外狀況。 若要提供自訂錯誤處理頁面,請使用例外狀況處理常式頁面

具格式字串的 UseStatusCodePages

若要自訂回應內容類型和文字,請使用 UseStatusCodePages 的多載,其會採用內容類型和格式字串:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(
        "text/plain", "Status code page, status code: {0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

在上面的程式碼中,{0} 是錯誤碼的預留位置。

含格式化字串的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

若要在範例應用程式中測試 UseStatusCodePages,請從 Program.cs 中的 webBuilder.UseStartup<StartupFormat>(); 移除註解。

具 Lambda 的 UseStatusCodePages

若要指定自訂錯誤處理和回應撰寫程式碼,請使用 UseStatusCodePages 的多載,其會採用 Lambda 運算式:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(async context =>
    {
        context.HttpContext.Response.ContentType = "text/plain";

        await context.HttpContext.Response.WriteAsync(
            "Status code page, status code: " +
            context.HttpContext.Response.StatusCode);
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

含 Lambda 的 UseStatusCodePages 通常不會用於生產環境,因為它會傳回對使用者無用的訊息。

若要在範例應用程式中測試 UseStatusCodePages,請從 Program.cs 中的 webBuilder.UseStartup<StartupStatusLambda>(); 移除註解。

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 擴充方法:

  • 傳送「302 - 已找到」狀態碼傳送給用戶端。
  • 將用戶端重新導向至 URL 範本中所提供的錯誤處理端點。 錯誤處理端點通常會顯示錯誤資訊,並傳回 HTTP 200。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithRedirects("/MyStatusCode?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

URL 範本可為狀態碼包含一個 {0} 預留位置,如上面的程式碼中所示。 如果 URL 範本是以 ~ (波狀符號) 為開頭,該 ~ 會被應用程式的 PathBase 取代。 若在應用程式中指定端點時,請為該端點建立一個 MVC 檢視或 Razor 頁面。 如需 Razor Pages 範例,請參閱範例應用程式中的 Pages/MyStatusCode.cshtml

此方法通常是在下列應用程式相關情況下使用:

  • 應用程式應將用戶端重新導向至不同的端點時 (通常是在由其他應用程式處理錯誤的情況下)。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映重新導向後的端點。
  • 應用程式不應該保留並傳回原始狀態碼與初始重新導向回應時。

若要在範例應用程式中測試 UseStatusCodePages,請從 Program.cs 中的 webBuilder.UseStartup<StartupSCredirect>(); 移除註解。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 擴充方法:

  • 傳回原始狀態碼給用戶端。
  • 可透過使用替代路徑重新執行要求管線來產生回應本文。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithReExecute("/MyStatusCode2", "?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

如果在應用程式內指定端點,請為該端點建立一個 MVC 檢視或 Razor 頁面。 請確定 UseStatusCodePagesWithReExecute 放在 UseRouting 之前,以便可以將要求重新路由傳送至狀態頁面。 如需 Razor Pages 範例,請參閱範例應用程式中的 Pages/MyStatusCode2.cshtml

此方法通常是在下列應用程式相關情況下使用:

  • 在不重新導向至其他端點的情況下處理要求。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映原始要求的端點。
  • 保留並傳回原始狀態碼與回應時。

URL 和查詢字串範本可為狀態碼包含一個預留位置 {0}。 URL 範本必須以 / 開頭。

處理錯誤的端點可以取得產生該錯誤的原始 URL,如下列範例所示:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class MyStatusCode2Model : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string ErrorStatusCode { get; set; }

    public string OriginalURL { get; set; }
    public bool ShowOriginalURL => !string.IsNullOrEmpty(OriginalURL);

    public void OnGet(string code)
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        ErrorStatusCode = code;

        var statusCodeReExecuteFeature = HttpContext.Features.Get<
                                               IStatusCodeReExecuteFeature>();
        if (statusCodeReExecuteFeature != null)
        {
            OriginalURL =
                statusCodeReExecuteFeature.OriginalPathBase
                + statusCodeReExecuteFeature.OriginalPath
                + statusCodeReExecuteFeature.OriginalQueryString;
        }
    }
}

如需 Razor Pages 範例,請參閱範例應用程式中的 Pages/MyStatusCode2.cshtml

若要在範例應用程式中測試 UseStatusCodePages,請從 Program.cs 中的 webBuilder.UseStartup<StartupSCreX>(); 移除註解。

停用狀態碼頁面

若要停用 MVC 控制器或動作方法的狀態碼頁面,請使用 [SkipStatusCodePages] 屬性。

若要停用 Razor 頁面處理常式方法或 MVC 控制器中的特定要求狀態碼頁面,請使用 IStatusCodePagesFeature

public void OnGet()
{
    // using Microsoft.AspNetCore.Diagnostics;
    var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature != null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

例外狀況處理程式碼

例外狀況處理頁面中的程式碼也可能會擲回例外狀況。 生產錯誤頁面應該進行徹底的測試,並特別小心以避免擲回自己的例外狀況。

回應標頭

一旦傳送回應的標頭之後:

  • 應用程式就無法變更回應的狀態碼。
  • 無法執行任何例外狀況頁面或處理常式。 回應必須完成,否則會中止連線。

伺服器例外狀況處理

除了應用程式中的例外狀況處理邏輯之外,HTTP 伺服器實作也可以處理一些例外狀況。 如果伺服器在回應標頭傳送之前攔截到例外狀況,則伺服器會傳送一個沒有回應本文的 500 - Internal Server Error 回應。 如果伺服器在回應標頭傳送之後攔截到例外狀況,伺服器會關閉連線。 應用程式未處理的要求會由伺服器來處理。 當伺服器處理要求時,任何發生的例外狀況均由伺服器的例外狀況處理功能來處理。 應用程式的自訂錯誤頁面、例外狀況處理中介軟體或篩選條件並不會影響此行為。

啟動例外狀況處理

只有裝載層可以處理應用程式啟動期間發生的例外狀況。 可以將主機設定為會擷取啟動錯誤擷取詳細錯誤

只有在錯誤是於主機位址/連接埠繫結之後發生的情況下,裝載層才能顯示已擷取之啟動錯誤的錯誤頁面。 如果繫結失敗:

  • 裝載層會記錄重大例外狀況。
  • Dotnet 會處理損毀狀況。
  • 當 HTTP 伺服器是 Kestrel 時,不會顯示任何錯誤頁面。

IIS (或 Azure App Service) 或 IIS Express 上執行時,如果無法啟動處理序,模組會傳回 502.5 - 處理序失敗。 如需詳細資訊,請參閱針對 Azure App Service 和 IIS 上的 ASP.NET Core 進行疑難排解

資料庫錯誤頁面

資料庫開發人員頁面例外狀況篩選條件 AddDatabaseDeveloperPageExceptionFilter 會擷取可使用 Entity Framework Core 移轉來解決的資料庫相關例外狀況。 發生這些例外狀況時,會產生一個 HTML 回應,其中包含了解決該問題的可能動作的詳細資料。 此頁面只會在開發環境中啟用。 當指定個別的使用者帳戶時,ASP.NET Core Razor Pages 範本會產生下列程式碼:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

例外狀況篩選條件

在 MVC 應用程式中,您能以全域設定例外狀況篩選條件,或是以每個控制器或每個動作為基礎的方式設定。 在 Razor Pages 應用程式中,您能以全域或每個頁面模型的方式設定它們。 這些篩選條件會處理在控制器動作或其他篩選條件執行期間發生但的任何未處理例外狀況。 如需詳細資訊,請參閱 ASP.NET Core 中的篩選條件

例外狀況篩選條件適合用來截獲 MVC 動作中發生的例外狀況,但是它們並不像內建例外狀況處理中介軟體 (UseExceptionHandler) 那麼有彈性。 我們建議使用 UseExceptionHandler (除非您需要根據選擇的 MVC 動作以不同方式執行錯誤處理)。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

模型狀態錯誤

如需如何處理模型狀態錯誤的相關資訊,請參閱模型繫結模型驗證

其他資源

作者:Tom DykstraSteve Smith

本文說明處理 ASP.NET Core Web 應用程式錯誤的常見方法。 如需 Web API,請參閱處理 ASP.NET Core 控制器型 Web API 中的錯誤

檢視或下載範例程式碼。 (如何下載。)

開發人員例外狀況頁面

「開發人員例外狀況頁面」會顯示要求例外狀況的詳細資訊。 ASP.NET Core 範本會產生下列程式碼:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

當應用程式在開發環境中執行時,上面的程式碼會啟用開發人員例外狀況頁面。

範本會將 UseDeveloperExceptionPage 放在任何中介軟體之前,以便在後面的中介軟體中攔截例外狀況。

僅當應用程式在開發環境中執行時,上面的程式碼才會啟用開發人員例外狀況頁面。 當應用程式在生產中執行時,不應該公開顯示詳細的例外狀況資訊。 如需設定環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

「開發人員例外狀況頁面」包含了下列有關例外狀況和要求的資訊:

  • 堆疊追蹤
  • 查詢字串參數 (如果有的話)
  • Cookie (如果有的話)
  • 標頭

例外處理常式頁面

若要針對生產環境設定自訂錯誤處理頁面,請使用例外狀況處理中介軟體。 中介軟體:

  • 攔截並記錄例外狀況。
  • 可在所指示頁面或控制器的替代管線中重新執行要求。 如果回應已啟動,就不會重新執行要求。 範本產生的程式碼會重新執行對 /Error 的要求。

在下列範例中,UseExceptionHandler 會在非開發環境中加入例外狀況處理中介軟體:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages 應用程式範本會在 Pages 資料夾中提供一個錯誤頁面 (.cshtml) 與 PageModel 類別 (ErrorModel)。 對於 MVC 應用程式,專案範本會包含 Home 控制器的一個 Error 動作方法和一個 Error 檢視。

請不要使用 HTTP 方法屬性 (如 HttpGet) 來標記錯誤處理常式動作方法。 明確的動詞命令可防止某些要求取得方法。 如果未經驗證的使用者應該看到錯誤檢視,則會允許匿名存取方法。

存取例外狀況

使用 IExceptionHandlerPathFeature 來存取例外狀況或和錯誤處理常式控制器或頁面中的原始要求路徑:

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

警告

提供敏感性的錯誤資訊給用戶端。 提供錯誤有安全性風險。

若要觸發上面的例外狀況處理頁面,請將環境設定為生產環境並強制引發例外狀況。

例外處理常式 Lambda

自訂例外處理常式頁面的替代方法,便是將 Lambda 提供給 UseExceptionHandler。 使用 lambda 可讓您在傳回回應之前存取錯誤。

以下是將 Lambda 用於例外狀況處理的範例:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler(errorApp =>
   {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
            context.Response.ContentType = "text/html";

            await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
            await context.Response.WriteAsync("ERROR!<br><br>\r\n");

            var exceptionHandlerPathFeature = 
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
            }

            await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");
            await context.Response.WriteAsync("</body></html>\r\n");
            await context.Response.WriteAsync(new string(' ', 512)); // IE padding
        });
    });
    app.UseHsts();
}

在上面的程式碼中,新增了 await context.Response.WriteAsync(new string(' ', 512));,以便 Internet Explorer 瀏覽器顯示該錯誤訊息,而不是 IE 錯誤訊息。 如需詳細資訊,請參閱這個 GitHub 問題。

警告

IExceptionHandlerFeatureIExceptionHandlerPathFeature 提供錯誤資訊給用戶端。 提供錯誤有安全性風險。

若要在範例應用程式中查看例外狀況處理 Lambda 的結果,請使用 ProdEnvironmentErrorHandlerLambda 前置處理器指示詞,並選取 home 上的 [觸發例外狀況]

UseStatusCodePages

根據預設,ASP.NET Core 應用程式不會提供 HTTP 狀態碼 (例如「404 - 找不到」) 等狀態碼頁面。 應用程式會傳回狀態碼和空白回應主體。 若要提供狀態碼頁面,請使用狀態碼頁面中介軟體。

該中介軟體是由 Microsoft.AspNetCore.Diagnostics 套件所提供。

若要針對常見的錯誤狀態碼啟用預設的純文字處理常式,請呼叫 Startup.Configure 方法中的 UseStatusCodePages

app.UseStatusCodePages();

要求處理中介軟體 (例如靜態檔案中介軟體和 MVC 中介軟體) 之前,應該先呼叫 UseStatusCodePages

若未使用 UseStatusCodePages,則巡覽至沒有端點的 URL 會傳回與瀏覽器相依的錯誤訊息,指示無法找到端點。 例如,巡覽至 Home/Privacy2。 呼叫 UseStatusCodePages 時,瀏覽器會傳回:

Status Code: 404; Not Found

具格式字串的 UseStatusCodePages

若要自訂回應內容類型和文字,請使用 UseStatusCodePages 的多載,其會採用內容類型和格式字串:

app.UseStatusCodePages(
    "text/plain", "Status code page, status code: {0}");

具 Lambda 的 UseStatusCodePages

若要指定自訂錯誤處理和回應撰寫程式碼,請使用 UseStatusCodePages 的多載,其會採用 Lambda 運算式:

app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain";

    await context.HttpContext.Response.WriteAsync(
        "Status code page, status code: " + 
        context.HttpContext.Response.StatusCode);
});

UseStatusCodePagesWithRedirects

UseStatusCodePagesWithRedirects 擴充方法:

  • 傳送「302 - 已找到」狀態碼傳送給用戶端。
  • 將用戶端重新導向到 URL 範本中提供的位置。
app.UseStatusCodePagesWithRedirects("/StatusCode?code={0}");

URL 範本可以針對狀態碼包含 {0} 預留位置,如範例所示。 如果 URL 範本是以 ~ (波狀符號) 為開頭,該 ~ 會被應用程式的 PathBase 取代。 如果您指向應用程式內的端點,請針對該端點建立 MVC 檢視或 Razor 頁面。 如需 Razor Pages 範例,請參閱範例應用程式中的 Pages/StatusCode.cshtml

此方法通常是在下列應用程式相關情況下使用:

  • 應用程式應將用戶端重新導向至不同的端點時 (通常是在由其他應用程式處理錯誤的情況下)。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映重新導向後的端點。
  • 應用程式不應該保留並傳回原始狀態碼與初始重新導向回應時。

UseStatusCodePagesWithReExecute

UseStatusCodePagesWithReExecute 擴充方法:

  • 傳回原始狀態碼給用戶端。
  • 可透過使用替代路徑重新執行要求管線來產生回應本文。
app.UseStatusCodePagesWithReExecute("/StatusCode","?code={0}");

如果您指向應用程式內的端點,請針對該端點建立 MVC 檢視或 Razor 頁面。 請確定 UseStatusCodePagesWithReExecute 放在 UseRouting 之前,以便可以將要求重新路由傳送至狀態頁面。 如需 Razor Pages 範例,請參閱範例應用程式中的 Pages/StatusCode.cshtml

此方法通常是在下列應用程式相關情況下使用:

  • 在不重新導向至其他端點的情況下處理要求。 針對 Web 應用程式,用戶端的瀏覽器網址列會反映原始要求的端點。
  • 保留並傳回原始狀態碼與回應時。

URL 和查詢字串範本可能會包含該狀態碼的預留位置 ({0})。 URL 範本的開頭必須是斜線 (/)。 在路徑中使用預留位置時,請確認端點 (頁面或控制器) 可以處理路徑線段。 例如適用於錯誤的 Razor Page 應接受具備 @page 指示詞的選擇性路徑區段值:

@page "{code?}"

處理錯誤的端點可以取得產生該錯誤的原始 URL,如下列範例所示:

var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null)
{
    OriginalURL =
        statusCodeReExecuteFeature.OriginalPathBase
        + statusCodeReExecuteFeature.OriginalPath
        + statusCodeReExecuteFeature.OriginalQueryString;
}

請不要使用 HTTP 方法屬性 (如 HttpGet) 來標記錯誤處理常式動作方法。 明確的動詞命令可防止某些要求取得方法。 如果未經驗證的使用者應該看到錯誤檢視,則會允許匿名存取方法。

停用狀態碼頁面

若要停用 MVC 控制器或動作方法的狀態碼頁面,請使用 [SkipStatusCodePages] 屬性。

若要停用 Razor Pages 處理常式方法或 MVC 控制器中特定要求的狀態碼頁面,請使用 IStatusCodePagesFeature

var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
    statusCodePagesFeature.Enabled = false;
}

例外狀況處理程式碼

例外狀況處理頁面中的程式碼,可擲回例外狀況。 一般來說,較好的做法是讓生產環境的錯誤頁面由純靜態內容組成。

回應標頭

一旦傳送回應的標頭之後:

  • 應用程式就無法變更回應的狀態碼。
  • 無法執行任何例外狀況頁面或處理常式。 回應必須完成,否則會中止連線。

伺服器例外狀況處理

除了應用程式中的例外狀況處理邏輯之外,HTTP 伺服器實作也可以處理一些例外狀況。 如果伺服器在回應標頭傳送之前攔截到例外狀況,伺服器會傳送「500 - 內部伺服器錯誤」回應,且沒有回應本文。 如果伺服器在回應標頭傳送之後攔截到例外狀況,伺服器會關閉連線。 應用程式未處理的要求會由伺服器來處理。 當伺服器處理要求時,任何發生的例外狀況均由伺服器的例外狀況處理功能來處理。 應用程式的自訂錯誤頁面、例外狀況處理中介軟體或篩選條件並不會影響此行為。

啟動例外狀況處理

只有裝載層可以處理應用程式啟動期間發生的例外狀況。 可以將主機設定為會擷取啟動錯誤擷取詳細錯誤

只有在錯誤是於主機位址/連接埠繫結之後發生的情況下,裝載層才能顯示已擷取之啟動錯誤的錯誤頁面。 如果繫結失敗:

  • 裝載層會記錄重大例外狀況。
  • Dotnet 會處理損毀狀況。
  • 當 HTTP 伺服器是 Kestrel 時,不會顯示任何錯誤頁面。

IIS (或 Azure App Service) 或 IIS Express 上執行時,如果無法啟動處理序,模組會傳回 502.5 - 處理序失敗。 如需詳細資訊,請參閱針對 Azure App Service 和 IIS 上的 ASP.NET Core 進行疑難排解

資料庫錯誤頁面

「資料庫錯誤頁面中介軟體」能擷取資料庫相關的例外狀況,其可透過使用 Entity Framework 移轉來解決。 發生這些例外狀況時,系統會產生具解決該問題之可能動作詳細資料的 HTML 回應。 此頁面僅應該於開發環境中啟用。 將程式碼加入 Startup.Configure 來啟用該頁面:

if (env.IsDevelopment())
{
    app.UseDatabaseErrorPage();
}

UseDatabaseErrorPage 需要 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 套件。

例外狀況篩選條件

在 MVC 應用程式中,您能以全域設定例外狀況篩選條件,或是以每個控制器或每個動作為基礎的方式設定。 在 Razor Pages 應用程式中,您能以全域或每個頁面模型的方式設定它們。 這些篩選會處理在控制器動作或其他篩選條件執行期間發生但的任何未處理例外狀況。 如需詳細資訊,請參閱 ASP.NET Core 中的篩選條件

提示

例外狀況篩選條件適合用來截獲 MVC 動作中發生的例外狀況,但是它們並不像例外狀況處理中介軟體那麼有彈性。 我們建議使用中介軟體。 請只在需要根據已選擇的 MVC 動作執行不同的錯誤處理時,才使用篩選條件。

模型狀態錯誤

如需如何處理模型狀態錯誤的相關資訊,請參閱模型繫結模型驗證

其他資源