# CSP-003　錯誤處理與回應碼規範（Error Handling & Response Code）

| 項目 | 內容 |
|---|---|
| 文件編號 | CSP-003 |
| 版本 | 1.3 |
| 狀態 | 已核定 |
| 撰寫者 | SA / Tech Lead |
| 審查者 | Security Architect |
| 對應需求 | NFR-REL 可靠性群組；BRD-001 §6 例外與補償 |

---

## 1. 目的

統一全系統之**錯誤分類、錯誤碼、HTTP 狀態對應、回應封套與例外處理策略**，
確保：①前端可據錯誤碼精準處理；②不洩漏內部實作；③可觀測、可追蹤。

---

## 2. 錯誤碼分類

錯誤碼格式：`CC-<SVC>-<NNNN>`，其中 `<SVC>` 為 **2–4 字母服務代碼**（依服務別長度不一：如 `AUTH`=4、`LIM`=3、`GW`=2），與 SDD-006 §3.2 登錄表、OpenAPI `Error.code` 之 `^CC-[A-Z]{2,4}-[0-9]{4}$` 完全一致。

> **唯一登錄表**：所有可用錯誤碼以 `SDD-006 §3.4` 為準（共 50+ 碼）。**Coder 不得自創錯誤碼**；新增碼須先擴充 SDD-006 §3.4 並同步 gen_openapi.py 之 `Error.code.pattern`。

| 類別位（首位） | 意義 | HTTP | 可否重試 | 代表碼 |
|---:|---|---|:--:|:--:|---|
| 1 | 業務規則拒絕 | 422 | ❌ | `CC-AUTH-1001`、`CC-LIM-1001`、`CC-POST-1001` |
| 2 | 併發／冪等衝突 | 409 | 部分可 | `CC-POST-2001`、`CC-POST-2002` |
| 3 | 狀態機衝突 | 409 | ❌ | `CC-CARD-3001`、`CC-POST-3001` |
| 4 | **請求與資源** | **400 / 404 / 405 / 409** | ❌ | `CC-PLT-4001`、`CC-PLT-4002`、`CC-PLT-4003`、`CC-PLT-4040`、`CC-PLT-4050`、`CC-POST-4001` |
| 5 | 外部與基礎設施 | 503 / 500 | ✅（冪等者） | `CC-PLT-5000`、`CC-PLT-5001`、`CC-PLT-5002`、`CC-AUTH-5001` |

> **v1.2 更正**：原表類別位 4 之代表碼誤引 `CC-AUTH-4004`——該碼**不存在於**
> `SDD-006 §3.4` 登錄表（登錄表目前**尚無任何 404 碼**）。依本文件 §2「唯一登錄表」原則，
> 更正為已登錄之 `CC-POST-4001`／`CC-POST-4002`。
> 若日後確有「資源不存在」情境，應**先擴充 SDD-006 §3.4** 再使用，不得直接引用未登錄碼。

### 2.1 類別位 4「請求與資源」之語意（v1.3 擴充，`CR-004`）

**類別位 4 ＝「請求本身或其所指資源無法被受理」**，涵蓋四個子語意，
**皆屬請求層，尚未進入業務邏輯**：

| 子語意 | HTTP | 何時使用 | 代表碼 |
|---|---:|---|---|
| 請求不合法 | 400 | 必填標頭／查詢參數缺失、主體格式錯誤、參數型別或 Bean Validation 失敗 | `CC-PLT-4001`／`CC-PLT-4002`／`CC-PLT-4003` |
| 資源或路徑不存在 | 404 | 無對應路由、聚合根查無 | `CC-PLT-4040`、`CC-PTY-4040`、`CC-POST-4040` |
| 方法不支援 | 405 | HTTP 動詞與端點不符 | `CC-PLT-4050` |
| 資源重複 | 409 | 業務冪等鍵已存在 | `CC-POST-4001`、`CC-POST-4002` |

**與其他類別位之區別（選碼判準）**：

| 情境 | 用哪個類別位 | 理由 |
|---|---|---|
| 請求缺 `X-Channel`、JSON 壞掉、`DELETE` 不支援、路徑打錯 | **4** | **尚未進入業務邏輯**，是「請求送錯了」 |
| 請求合法但業務規則拒絕（額度不足、點數不足） | 1 | 已進入業務邏輯，是「業務上不允許」 |
| 樂觀鎖／冪等鍵衝突 | 2 | 併發語意 |
| 狀態機不允許轉換 | 3 | 狀態語意 |

> ⚠️ **不得**用類別位 5（5xx）掩飾請求層錯誤——此即 `CSP-003 §7 E3`「用 500 掩飾所有錯誤」
> 所禁止之行為：前端無法區分、監控告警失準。**請求層錯誤若回 500，每個客戶端誤用都會觸發
> ERROR 告警**，造成告警疲勞，真正的伺服器異常將被淹沒。

> **實作歸屬**：本類別位之 `CC-PLT-4xxx` 五碼由 **`common-spring` 之 `GlobalExceptionHandler`
> 統一拋出**，**服務不需自行實作**。服務僅在需要更精確語意時才使用自有 4xxx 碼
> （如 `CC-PTY-4040`「歸戶查無」）。Spring 例外之對應關係見 `SDD-006 §3.4`。

**HTTP 狀態對應原則**：

| 類別 | HTTP | 說明 |
|---|---|---|
| **請求不合法** | **400** | 標頭／參數缺失、主體格式錯誤、參數驗證失敗（**類別位 4**，`CC-PLT-4001`～`4003`） |
| 客戶端輸入錯誤 | 400 / 422 | 欄位缺失（400）、商業規則拒絕（422，類別位 1） |
| 身分／權限 | 401 / 403 | 未登入、無權限（`CC-GW-1002`／`CC-GW-1003`） |
| 資源或路徑不存在 | 404 | 無對應路由、聚合根查無（類別位 4） |
| **方法不支援** | **405** | HTTP 動詞與端點不符（類別位 4，`CC-PLT-4050`） |
| 商業衝突 | 409 | 冪等鍵衝突、版本衝突、狀態衝突、資源重複（類別位 2/3/4） |
| 下游／依賴失敗 | 503 / 500 | 逾時、不可用（類別位 5） |
| 系統內部 | 500 | 未預期（須告警，類別位 5） |

> **服務代碼總表**（摘要）：`AUTH`(4) `CARD`(4) `POST`(4) `LIM`(3) `PTY`(3) `PRD`(3) `PAY`(3) `STM`(3) `PRC`(3) `INS`(3) `RWD`(3) `CLR`(3) `DSP`(3) `MRC`(3) `NOT`(3) `RPT`(3) `MIG`(3) `PLT`(3) `GW`(2)。**不存在 `CMN` 等偽代碼**。

---

## 3. 回應封套（統一格式）

所有回應（成功與失敗）一律套用統一信封（**與 OpenAPI `Envelope` / `Error` schema 完全一致**）：

```json
{
  "traceId": "01HZX8K2M4N5P6Q7R8S9T0V1W2",
  "data": { },
  "error": null
}
```

失敗時 `data` 為 `null`，`error` 攜帶結構化資訊：

```json
{
  "traceId": "01HZX8K2M4N5P6Q7R8S9T0V1W2",
  "data": null,
  "error": {
    "code": "CC-LIM-1001",
    "message": "可用額度不足",
    "details": [
      { "field": "amount", "reason": "exceeds available limit" }
    ]
  }
}
```

| 欄位 | 型別 | 說明 |
|---|---|---|
| `traceId` | string | 信封層追蹤號（ULID），成功／失敗皆帶 |
| `data` | object｜null | 成功為資料本體；失敗為 `null` |
| `error.code` | string | `CC-<SVC4>-<NNNN>`，須為 SDD-006 §3.4 登錄碼 |
| `error.message` | string | 使用者可讀訊息 |
| `error.details[]` | array | 欄位級原因（可多筆）；非欄位錯誤可省略 |

> **禁止**（對應 CSP-001 §8 B4）：不得將例外堆疊、SQL 語句、框架內部錯誤原樣回傳。
> 內部錯誤一律轉為 `CC-PLT-5xxx` + 通用訊息，細節僅寫入伺服器日誌（含 `traceId`）。

---

## 4. 例外處理階層

```mermaid
classDiagram
    class DomainException {
        +String code
        +String message
    }
    class ValidationException
    class BusinessRuleException
    class ConcurrencyException
    class InfrastructureException
    class DownstreamException
    DomainException <|-- ValidationException
    DomainException <|-- BusinessRuleException
    DomainException <|-- ConcurrencyException
    DomainException <|-- InfrastructureException
    InfrastructureException <|-- DownstreamException
```

- `domain/` 拋出 `DomainException` 子類（業務語意），**不含技術細節**。
- `interfaces/` 設 `ControllerAdvice` 將 `DomainException` 映對為 HTTP 狀態＋封套。
- `infrastructure/` 將 JDBC／HTTP 錯誤包裝為 `InfrastructureException`，**不向上洩漏**。

---

## 5. 冪等與重試

1. **冪等鍵**：所有寫側 Command（授權、入帳、繳款…）攜帶 `Idempotency-Key`（ client 產生或上游傳入）；
   重複提交返回**首次結果**，不重複執行（SDD-004 §幂等）。
2. **重試**：下游暫態失敗（503 服務不可用／外部逾時）由呼叫方以**指數退避**重試（上限 3 次，含 jitter）；
   非暫態（4xx）不重試。
3. **逾時**：同步呼叫下游預設 800ms 逾時；超過即 `CC-PLT-5001`（外部系統逾時）並觸發 Stand-in / 補償（SDD-004）。

---

## 6. 交易與補償語意

- 寫側一律在 `application/` 層開交易；跨服務透過 **Saga + Outbox**（SDD-004）保證最終一致。
- 補償失敗（如回退視窗逾時、補償步驟失敗）拋 `CC-PLT-5002`（Saga 補償失敗），由協調者告警並進入人工處置佇列；
  與遷移相關之失敗使用 `CC-MIG-1001`（遷移類型不支援）等 `MIG` 登錄碼，不得自創。

---

## 7. 禁止事項

| 編號 | 禁止 | 原因 |
|---|---|---|
| E1 | `catch` 後吞掉 | 失去可觀測性（CSP-001 B3） |
| E2 | 直接 `throw e` 給前端 | 洩漏內部（B4） |
| E3 | 用 `500` 掩飾所有錯誤 | 前端無法區分，監控告警失準 |
| E4 | 回應含 SQL / 堆疊 / 連線字串 | 資安風險（SAD-005） |
| E5 | 以 `Thread.sleep` 當重試退避 | 佔用執行緒；應用排程／非同步重試 |

---

## 8. 變更記錄

| 版本 | 說明 |
|---|---|
| 1.0 | 初版：錯誤分類、回應封套、例外階層、冪等重試、禁止事項 |
| 1.1 | 與 SDD-006 §3.4 登錄表及 OpenAPI `Error` schema 對齊：①服務代碼改為 **2–4 字母**（原誤寫為 3 字母並引用不存在之 `CC-CMN-*`）；②回應封套修正為 `{traceId,data,error{details[]}}`，與 OpenAPI 完全一致（原 `detail`/`timestamp`/`path` 為錯誤形狀）；③下游失敗狀態改為 503、補償失敗碼改為 `CC-PLT-5002`；④明訂錯誤碼唯一登錄表，Coder 不得自創 |
| 1.2 | ①**更正 §2 類別位 4 之代表碼**：原引 `CC-AUTH-4004` 未登錄於 SDD-006 §3.4（且登錄表現無任何 404 碼），改引已登錄之 `CC-POST-4001`／`CC-POST-4002`；②§2 類別位 5 代表碼補入新增之 `CC-PLT-5000`。此二項係實作 `common-spring` 之 `ErrorCodes`（由 `tools/gen_error_codes.py` 自 SDD-006 §3.4 產生）時，以程式比對文件所發現 |
| 1.3 | **擴充 §2 類別位 4 語意為「請求與資源」**（`CR-004`，2026-09-19 BEAM PMO 核定）：<br>①類別位 4 之 HTTP 由「404／409」擴充為「**400／404／405／409**」，並新增 **§2.1** 明訂四個子語意與**選碼判準**（與類別位 1／2／3 之區別）。<br>②**新增「請求不合法（400）」與「方法不支援（405）」兩列**於「HTTP 狀態對應原則」表。<br>③**動機（實測）**：`CR-004` 於 8089 實測 6 項情境、**4 項錯誤**——缺標頭、壞 JSON、不支援方法、路徑不存在**全數回 500**。根因：Spring MVC 之請求層例外皆非 `DomainException` 子類，落 `GlobalExceptionHandler` 兜底。原類別位 4 僅有 404／409，**400 與 405 無處安放**，若借用類別位 1（422 業務規則）或 5（5xx 基礎設施）皆為語意錯置。<br>④明訂**選碼判準**：類別位 4 之判準為「**尚未進入業務邏輯**」，並重申 `§7 E3`「不得用 500 掩飾錯誤」——請求層錯誤若回 500，**每個客戶端誤用都會觸發 ERROR 告警**（告警疲勞）。<br>⑤**實作歸屬**：`CC-PLT-4xxx` 五碼由 `common-spring` 之 `GlobalExceptionHandler` 統一拋出，**服務不需自行實作**（服務僅在需更精確語意時用自有 4xxx 碼） |

