未處理的例外若直接顯示給使用者,洩漏堆疊追蹤、資料庫結構、伺服器路徑等敏感資訊,構成安全風險;若僅顯示友善錯誤頁,開發團隊又難以第一時間發現問題。本文示範透過 `IHttpModule` 掛載 `HttpApplication.Error` 事件,建立**全域例外處理管線**:記錄詳細資訊至資料庫、產生唯一錯誤編號、導向友善錯誤頁、可選發送郵件通知,達成安全與可觀測性雙贏。
HTTP 請求管線
→ ... (正常處理)
→ 發生未捕獲例外
→ HttpApplication.Error 事件觸發
→ IHttpModule.Init() 註冊的處理器介入
→ 執行自訂邏輯:
1. 取得例外物件 (Server.GetLastError())
2. 過濾已知雜訊 (404、HttpException 等)
3. 擷取關鍵資訊 (型別、訊息、堆疊、URL、使用者、參數)
4. 寫入資料庫 / 串流日誌 / 訊息佇列
5. 產生錯誤編號 (GUID / 流水號)
6. ClearError() 清除例外
7. Response.Redirect 錯誤頁 + 帶入錯誤編號
8. (可選) 發送郵件/Slack/Teams 通知值班人員
using System;
using System.Data.SqlClient;
using System.Web;
namespace MyApp.Modules
{
public class GlobalErrorModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.Error += OnError;
}
private void OnError(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var context = app.Context;
var ex = context.Server.GetLastError();
if (ex == null) return;
// 1. 過濾雜訊:404、靜態檔案不存在等
if (ex is HttpException httpEx && httpEx.GetHttpCode() == 404)
{
// 可選:記錄 404 供 SEO 分析,但不觸發告警
return;
}
// 2. 取得真實例外 (解開 HttpUnhandledException)
var realEx = ex.GetBaseException();
// 3. 產生錯誤編號
string errorId = Guid.NewGuid().ToString("N").Substring(0, 12).ToUpper();
// 4. 建立錯誤記錄物件
var errorLog = new ErrorLogEntry
{
ErrorId = errorId,
Timestamp = DateTime.UtcNow,
ExceptionType = realEx.GetType().FullName,
Message = realEx.Message,
StackTrace = realEx.StackTrace,
Source = realEx.Source,
TargetSite = realEx.TargetSite?.ToString(),
Url = context.Request.RawUrl,
HttpMethod = context.Request.HttpMethod,
UserAgent = context.Request.UserAgent,
UserHostAddress = context.Request.UserHostAddress,
UserIdentity = context.User?.Identity?.Name ?? "Anonymous",
FormData = GetFormData(context.Request),
QueryString = context.Request.QueryString.ToString(),
ServerVariables = GetServerVars(context.Request)
};
// 5. 非同步寫入資料庫 (不阻塞請求執行緒)
Task.Run(() => PersistErrorLog(errorLog)).ConfigureAwait(false);
// 6. 發送即時通知 (高嚴重級才發送,避免風暴)
if (IsCritical(realEx))
{
Task.Run(() => NotifyOnCall(errorLog)).ConfigureAwait(false);
}
// 7. 清除例外,避免預設黃色死機畫面
context.Server.ClearError();
// 8. 導向友善錯誤頁,攜帶錯誤編號
context.Response.Redirect($"/Error/Friendly?errorId={errorId}");
}
// ... 輔助方法:GetFormData, GetServerVars, PersistErrorLog, NotifyOnCall, IsCritical
}
public class ErrorLogEntry
{
public string ErrorId { get; set; }
public DateTime Timestamp { get; set; }
public string ExceptionType { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
public string Source { get; set; }
public string TargetSite { get; set; }
public string Url { get; set; }
public string HttpMethod { get; set; }
public string UserAgent { get; set; }
public string UserHostAddress { get; set; }
public string UserIdentity { get; set; }
public string FormData { get; set; }
public string QueryString { get; set; }
public string ServerVariables { get; set; }
}
}
<system.webServer>
<modules>
<add name="GlobalErrorModule"
type="MyApp.Modules.GlobalErrorModule, MyApp"
preCondition="managedHandler" />
</modules>
</system.webServer>
<!-- 關閉預設 customErrors,交由模組處理 -->
<system.web>
<customErrors mode="Off" />
</system.web>
public partial class FriendlyError : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string errorId = Request.QueryString["errorId"];
if (!string.IsNullOrEmpty(errorId))
{
lblErrorId.Text = errorId;
// 可選:查詢資料庫顯示發生時間、簡略分類
}
}
}
<div class="error-page">
<h1>系統發生錯誤,我們已記錄並通知工程團隊處理</h1>
<p>錯誤參考編號:<strong><asp:Label ID="lblErrorId" runat="server" /></strong></p>
<p>請將此編號提供給客服或管理員,以利快速追蹤。</p>
<a href="/" class="btn">返回首頁</a>
</div>
| 細節 | 處理方式 |
|---|---|
| 取得真實例外 | `ex.GetBaseException()` 解開 `HttpUnhandledException` 包裝 |
| HRESULT 取得 | `Marshal.GetHRForException(ex)` (私有屬性 `HResult` 不可直接存取) |
| 過濾 404/靜態檔案 | `ex is HttpException && httpEx.GetHttpCode() == 404` |
| 非同步寫入日誌 | `Task.Run(() => ...)` 避免阻塞 IIS 執行緒池 |
| 防止通知風暴 | 錯誤去重 (同類例外 5 分鐘內只通知一次)、熔斷機制 |
| 敏感資料遮罩 | FormData/QueryString 中的密碼、Token、信用卡號自動遮罩 |
| 資料表結構建議 | 分區表 (按月)、堆疊追蹤壓縮 (GZip)、全文檢索索引 |
全域例外處理是應用程式的**最後一道防線**,也是**可觀測性的起點**。建立「記錄 → 編號 → 通知 → 友善回應」的標準化流程,讓每個例外都可追蹤、可審計、可改進,是從「能跑」邁向「可託付」的關鍵里程碑。