Exception Handling

💵 站台建立的目的

這是一個專門串接多家第三方金流的站台,前台可以依照情況切換不同金流提供者。整個站台的角色是「金流中介者」,幫助前台整合各家金流流程。

在這樣的架構下,例外處理(Exception Handling)變得非常關鍵。因為錯誤不僅可能來自系統內部,也可能是來自外部金流系統的 timeout、錯誤參數或伺服器無回應等狀況。

因此,我們使用 ExceptionHandlerMiddleware 來當作 API 的防爆層(Fallback Layer),讓:

  • 內部開發者能清楚知道發生什麼錯
  • 外部串接方只看到簡單明確的錯誤資訊

這樣能確保資訊安全,同時提升除錯效率。



💵 Global ExceptionMiddleware

設計的重點:

  • 捕捉所有未處理的 Exception
  • 記錄 Log
  • 給用戶一種統一格式的 JSON 回應
  • 在 Debug 模式下額外附上 StackTrace

實作參考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    public class ExceptionHandlingMiddleware
{
private const string ResponseMessage = "Not Handling Exception Has Happened!!";
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task Invoke(HttpContext context)
{
Stream originalBodyStream = context.Response.Body;
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
this._logger.LogError(ex, $"{ResponseMessage} - Request-id {context.GetTraceId()} - Message {ex.Message}");
await using Stream exceptionResponseStream = this.GetExeptionSteam(ex);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = MediaTypeNames.Application.Json;
await exceptionResponseStream.CopyToAsync(originalBodyStream);
}
}

private Stream GetExeptionSteam(Exception ex)
{
object responseObj = new { Message = ex.Message, ReturnCode = ReturnCodes.UnknownException };
#if DEBUG
responseObj = new { Message = ex.Message, StackTrace = ex.StackTrace, ReturnCode = ReturnCodes.UnknownException };
#endif
var json = JsonSerializer.Serialize(responseObj);
var bytes = Encoding.UTF8.GetBytes(json);
var stream = new MemoryStream(bytes);
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}



註冊方式:

1
2
3

app.UseMiddleware<ExceptionHandlerMiddleware>();

這段 Middleware 是一個「最低限度的防爆機制」,可有效攔截所有未處理例外。對於金流中介站台而言,它提供了以下幫助:

  • 將錯誤訊息、堆疊、時間點、Request ID 等記錄到日誌中(Log)以利追蹤
  • 在 Debug 模式下提供完整的 StackTrace 來幫助開發除錯
  • 設定正確的 Content-Type = application/json,讓前端知道格式如何解析
  • 避免內部堆疊訊息或機密資訊直接暴露在外部使用者眼前


💵 改善方向

現有程式能執行錯誤捕捉,但針對金流中介者的設計,還可更進一步改善:

🔁 更精簡的錯誤分類

目前只攔截 Exception,建議針對不同類型的錯誤明確分類:

目前只抓 Exception → 建議加入更多 error type:

1
2
3
4
catch (PaymentException ex) {}
catch (TimeoutException ex) {}
catch (HttpRequestException ex) {}
catch (Exception ex) {}
  • 可 Retry 的錯誤:如 HTTP 503
  • 第三方 SDK 錯誤:如 TapPayTimeout
  • 用戶傳入的錯誤資料
  • 對應程式可分別給予 HTTP 400, 422, 503 等合理回應碼

🧱 解耦錯誤包裝邏輯

目前 GetExeptionSteam() 是直接寫死在 middleware 裡,這會造成:

  • 測試困難
  • 無法在多專案共用
  • 不易維護

建議將錯誤格式轉換抽出成服務:

1
2
3
4
5
6

public interface IErrorResponseBuilder
{
string Build(Exception ex, bool isDebug, string traceId);
}

➡️ Middleware 中就只需要:

1
2
3

context.Response.WriteAsync(_errorResponseBuilder.Build(ex, ...));



💵 API 本身的錯誤處理

我們再回到 API 本身的例外處理機制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

/// <summary>
/// 付款
/// </summary>
/// <param name="_">Headers</param>
/// <param name="request">付款參數</param>
/// <param name="payMethod">付款方式</param>
/// <returns>付款結果</returns>
public async Task<PaymentResponseEntity<IDictionary<string, object>>> Pay(PaymentRequestEntity<CreatePaymentRequestExtendInfo> request, RequestHeaders _, string payMethod)
{
try
{
//// 執行付款邏輯,打 Stripe API...
}
catch (ApiException ex)
{
ErrorResponseEntity error = await ex.GetContentAsAsync<ErrorResponseEntity>();

return new PaymentResponseEntity<IDictionary<string, object>>
{
ReturnCode = ReturnCodes.Failed,
ReturnMessage = error.Error.Message,
TransactionId = string.Empty,
RequestId = request.RequestId,
TradesOrderGroupCode = request.TradesOrderGroupCode,
};
}
catch (Exception e)
{
this._logger.LogError(e, $"Unhandled Payment Error - {e.Message}");
throw;
}
}

使用 Refit 套件管理三方金流的串接,好處是可以透過定義介面的方式定義怎麼打,並且幫你處理序列話語反序列化,還有,如果三方 api 回錯誤,會被包成 ApiException,藉以區分是三方發生的錯誤而不影響站台的運作,所以我們有包裝成統一的 ResponseEntity回給使用者端

☘️ 結語

錯誤是不可避免的,但錯誤不該讓使用者或開發團隊措手不及。

Exception Middleware 不只是為了「不炸掉」,更是為了:

  • 給開發者準確的除錯線索
  • 給前端簡潔的一致格式
  • 給 API 對接方一份「可預期的回應」

在金流整合這種高風險、高變動的場景中,這樣的設計不只是加分,是基本生存配備。

別讓錯誤拖垮你辛苦建好的系統,從一個好的 Middleware 開始,把錯誤收進框框裡,用乾淨的方式告訴世界:「我有錯,但我掌控它。」