共用方式為


ASP.NET Web API 2 中的全域錯誤處理

作者:David MatsonRick Anderson

本主題提供 ASP.NET 4.x 的 ASP.NET Web API 2 中的全域錯誤處理概觀。 目前 Web API 中沒有任何簡單的方法可全域記錄或處理錯誤。 有些未處理的例外狀況可以透過例外狀況篩選條件來處理,但很多案例是例外狀況篩選條件無法處理的。 例如:

  1. 從控制器建構函式擲回的例外狀況。
  2. 從訊息處理常式擲回的例外狀況。
  3. 在路由期間擲回的例外狀況。
  4. 在回應內容序列化期間擲回的例外狀況。

我們想要提供簡單、一致的方式來記錄和處理這些例外狀況 (可能的話)。

處理例外狀況有兩個主要案例、我們可以傳送錯誤回應的案例,以及我們所能執行的案例是記錄例外狀況。 後一種案例的範例是,在串流回應內容中間擲回例外狀況;在這種情況下,傳送新的回應訊息為時已晚,因為狀態碼、標頭和部分內容已經通過線路,因此我們只需中止連線即可。 即使無法處理例外狀況來產生新的回應訊息,我們仍支援記錄例外狀況。 如果我們可以偵測到錯誤,我們可以傳回適當的錯誤回應,如下所示:

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

現有的選項

除了例外狀況篩選條件之外,現在還可以使用訊息處理常式來觀察所有 500 層級的回應,但對這些回應採取行動很困難,因為它們缺乏有關原始錯誤的內容。 訊息處理常式也有一些與例外狀況篩選條件相同的限制,這些例外狀況篩選條件可以處理的情況。 雖然 Web API 確實具有可擷取錯誤狀況的追蹤基礎結構,但追蹤基礎結構是為了診斷目的,而且不適合在生產環境中執行。 全域例外狀況處理和記錄應該是可以在生產期間執行並插入現有監視解決方案的服務 (例如,ELMAH)。

解決方案概觀

我們提供兩個新的使用者可替換服務:IExceptionLogger 和 IExceptionHandler,用於記錄和處理未處理的例外狀況。 服務非常類似,有兩個主要差異:

  1. 我們支援註冊多個例外狀況記錄器,但只註冊單一例外狀況處理常式。
  2. 例外狀況記錄器一律會呼叫,即使我們即將中止連線也一樣。 只有在我們仍然能夠選擇要傳送的回應訊息時,才會呼叫例外狀況處理常式。

這兩個服務都提供對例外狀況內容的存取,其中包含偵測到例外狀況時的相關資訊,特別是 HttpRequestMessageHttpRequestContext、擲回的例外狀況和例外狀況來源 (詳細資料如下)。

設計原則

  1. 無重大變更 由於此功能是在次要版本中新增的,因此影響該解決方案的一個重要條件約束是,無論是類型合約還是行為都沒有重大變更。 此條件約束排除了我們想要在將例外狀況變成 500 個回應的現有 Catch 區塊方面所做的一些清除。 這個額外的清除是後續主要版本可能考慮的事項。
  2. 維持與 Web API 建構的一致性 Web API 篩選條件管線是處理橫切問題的好方法,可以靈活地在特定於動作、特定於控制器或全域範圍內套用邏輯。 篩選條件,包括例外狀況篩選條件,一律有動作和控制器內容,即使在全域範圍註冊也一樣。 該合約對篩選條件很有意義,但表示例外狀況篩選條件,即使是全域範圍篩選條件,也不適合某些例外狀況處理案例,例如訊息處理常式的例外狀況,其中沒有任何動作或控制器內容存在。 如果我們想要使用篩選條件所提供的彈性範圍來處理例外狀況,我們仍然需要例外狀況篩選條件。 但是,如果我們需要在控制器內容之外處理例外狀況,我們也需要個別的建構來處理完整的全域錯誤處理 (沒有控制器內容和動作內容條件約束)。

使用時機

  • 例外狀況記錄器是查看 Web API 攔截到所有未處理的例外狀況的解決方案。
  • 例外狀況處理常式是自訂 Web API 所攔截之未處理例外狀況之所有可能回應的解決方案。
  • 例外狀況篩選條件是處理與特定動作或控制器相關的子集未處理例外狀況的最簡單解決方案。

服務詳細資料

例外狀況記錄器和處理常式服務介面是採用個別內容的簡單非同步方法:

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

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

我們也提供這兩個介面的基底類別。 重寫核心 (同步或非同步) 方法就是在建議時間記錄或處理所需的全部內容。 針對記錄,ExceptionLogger 基底類別可確保核心記錄方法只會針對每個例外狀況呼叫一次 (即使稍後會進一步傳播呼叫堆疊並再次攔截)。 ExceptionHandler 基底類別只會針對呼叫堆疊頂端的例外狀況呼叫核心處理方法,忽略舊版巢狀 catch 區塊。 (這些基底類別的簡化版本位於下列附錄中。)IExceptionLoggerIExceptionHandler 都透過 ExceptionContext 接收有關例外狀況的資訊。

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

當架構呼叫例外狀況記錄器或例外狀況處理常式時,它將隨時提供 ExceptionRequest。 除了單元測試之外,它還會一律提供 RequestContext。 它很少會提供 ControllerContextActionContext (僅當從例外狀況篩選條件的 catch 區塊呼叫時)。 它很少會提供 Response (僅在某些 IIS 情況下嘗試寫入回應時)。 請注意,由於其中一些屬性可能是 null,因此取用者需要在存取例外狀況類別的成員之前檢查 nullCatchBlock 是字串,指出看到例外狀況的 catch 區塊。 catch 區塊字串如下所示:

  • HttpServer (SendAsync 方法)

  • HttpControllerDispatcher (SendAsync 方法)

  • HttpBatchHandler (SendAsync 方法)

  • IExceptionFilter (ApiController 對 ExecuteAsync 中例外狀況篩選條件管線的處理)

  • OWIN 主機:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (用於緩衝輸出)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (適用於串流輸出)
  • Web 主機:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (用於緩衝輸出)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (適用於串流輸出)
    • HttpControllerHandler.WriteErrorResponseContentAsync (用於緩衝輸出模式下錯誤復原失敗)

catch 區塊字串的清單也可以透過靜態唯讀屬性來取得。 (核心 catch 區塊字串位於靜態 ExceptionCatchBlocks 上;其餘部分則分別出現在 OWIN 和 Web 主機上的一個靜態類別上。)IsTopLevelCatchBlock 對於遵循僅在呼叫堆疊頂部處理例外狀況的建議模式很有幫助。 例外狀況處理常式不會在巢狀 catch 區塊發生的任何位置將例外狀況轉換成 500 個回應,而是允許例外狀況傳播,直到主機即將看到例外狀況為止。

除了 ExceptionContext 之外,記錄器還透過完整的 ExceptionLoggerContext 取得另一個資訊:

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

第二個屬性,CanBeHandled,允許記錄器識別無法處理的例外狀況。 當連線即將中止且無法傳送新的回應訊息時,將呼叫記錄器但會呼叫處理常式,記錄器可以透過此屬性識別此案例。

除了 ExceptionContext 之外,處理常式還獲得一個可以在完整 ExceptionHandlerContext 上設定的屬性來處理例外狀況:

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

例外狀況處理常式透過將 Result 屬性設為動作結果 (例如 ExceptionResultInternalServerErrorResultStatusCodeResult 或自訂結果) 來表示它已處理例外狀況。 如果 Result 屬性為 Null,則例外狀況不會被處理,並且將重新擲回原始例外狀況。

針對呼叫堆疊頂端的例外狀況,我們採取了額外的步驟,以確保回應適用於 API 呼叫端。 如果例外狀況傳播至主機,則呼叫端會看到死亡的黃色畫面,或某些其他主機提供的回應,通常是 HTML,通常不是適當的 API 錯誤回應。 在這些情況下,Result 開始時為非 Null,且只有當自訂例外狀況處理常式明確將其設定回 null (未處理) 時,例外狀況才會傳播到主機。 在這種情況下將 Result 設定為 null 對於以下兩種案例很實用:

  1. OWIN 裝載 Web API,其中包含在 Web API 之前/外部註冊的自訂例外狀況處理中介軟體。
  2. 透過瀏覽器進行本機偵錯,其中黃色當機畫面實際上是對未處理例外狀況的實用回應。

對於例外狀況記錄器和例外狀況處理常式,如果記錄器或處理常式本身擲回例外狀況,我們不會執行任何動作來復原。 (除了讓例外狀況傳播之外,如果您有更好的方法,請在此頁面底部留下意見反應饋。) 例外狀況記錄器和處理常式的約定是,他們不應該讓例外狀況傳播到其呼叫端;否則,例外狀況只會傳播,通常會一直傳播到主機,導致 HTML 錯誤 (如 ASP.NET 的黃色當機畫面) 被傳送回客戶端 (對於期望 JSON 或 XML 的 API 呼叫端來說,這通常不是偏好選項)。

範例

追蹤例外狀況記錄器

下列例外狀況記錄器會將例外狀況資料傳送至已設定的追蹤來源 (包括 Visual Studio 中的 [偵錯輸出] 視窗)。

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

自訂錯誤訊息例外狀況處理常式

下列例外狀況處理常式會產生對用戶端的自訂錯誤回應,包括聯絡支援人員的電子郵件位址。

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

註冊例外狀況篩選條件

如果您使用「ASP.NET MVC 4 Web 應用程式」專案範本來建立專案,請將 Web API 設定程式碼放入 WebApiConfig 類別中,該類別位於 App_Start 資料夾中:

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

        // Other configuration code...
    }
}

附錄:基底類別詳細資料

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}