Project

General

Profile

CR #242 » CR-041_B2實測揭露之請求層三項發現之裁定與修補.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

CR-041 B2 實測揭露之請求層三項發現——裁定與修補

項目 內容
文件編號 CR-041
版本 1.1
狀態 已立/已執行(#A-184/#A-186 已修並實測通過;#A-185 經查證後更正為非缺陷);待 BEAM PMO 核定
提出日期 2026-10-05
提出者 開發團隊(B2 橫向複製 @WebMvcTest 切片測試時實測揭露;見 TRP-003 §6.8.3)
影響文件 CSP-003 §7、SDD-006 §3.4(引用)、TRP-003 §6.8、CSP-001 §7(設計規則)
影響服務 card-service(#A-184)、authorization-service/product-service(#A-186)
優先級 #A-184 高(P1)——契約宣告 404 實回 500,且落入「以 500 掩飾」之禁區;#A-186 中(P2)——契約 required 未被 web 層強制
相關前例 CR-004(平台層 500-vs-400 錯誤映射)——本 CR 之 #A-184 為其未涵蓋之殘留缺口

1. 摘要

B2 批次(TRP-003 §6.8)為 16 個服務/25 個 @WebMvcTest 建立切片測試時,
實測揭露三項請求層發現。★ 三項皆非文件所述,而是第一次真正把端點打過一遍才浮現
(「未測到的路徑=未驗證的假設」)。

# 發現 初判 查證後之裁定
#A-184 CardController.lookup 查無卡片 → 回 500 CC-PLT-5000(契約要求 404) 缺陷 ✅ 真缺陷——修
#A-185 Jackson 3 預設 FAIL_ON_NULL_FOR_PRIMITIVES=true → 請求 DTO 之 primitive 欄位一省略即 400 缺陷 ⚠️ 更正為「非缺陷」——契約將該等欄位列為 required,故 400 正確(詳 §3)
#A-186 部分 @RequestBody 未加 @Valid → 巢狀 record 之驗證不生效 缺陷 ✅ 真缺陷——修(契約 required 未被強制)

★ 本 CR 含一項「我方誤判之更正」(#A-185)——依「發現矛盾要講」原則明列,不掩飾。


2. #A-184:ResponseStatusException 未被 GlobalExceptionHandler 處理

2.1 實測證據(CardControllerTest,@WebMvcTest)

★ GET /cards/lookup/{hash}:查無 → 應 404
  java.lang.AssertionError: Status expected:<404> but was:<500>

2.2 根因

CardController.lookup:

return queryService.lookupByCardNoHash(cardNoHash)
        .map(view -> ApiResponse.success(TraceContext.currentTraceId(), view))
        .orElseThrow(() -> new ResponseStatusException(
                HttpStatus.NOT_FOUND, "查無此卡號(雜湊值不符)"));   // ← 拋 ResponseStatusException

common-spring 之 GlobalExceptionHandler 之 handler 清單(7 個,CR-004 建立):

涵蓋 例外型別
領域 DomainException
請求層 MissingRequestHeaderException/MissingServletRequestParameterException/HttpMessageNotReadableException/MethodArgumentNotValidException/HandlerMethodValidationException/ConstraintViolationException/MethodArgumentTypeMismatchException/NoResourceFoundException/HttpRequestMethodNotSupportedException
兜底 Exception → 500 CC-PLT-5000

★ ResponseStatusException 不在其中 → 落兜底 → 500。

2.3 影響範圍(實查)

項 結果
全庫 ResponseStatusException 使用處 1 處(CardController.lookup;grep 全庫)
AuthorizationController 之 import ⚠️ 死 import(有 import、無使用)
契約宣告 GET /api/v1/cards/lookup/{cardNoHash} → 404 資源不存在(card-service.yaml 實查)
服務自身慣例 card-service 其餘 8+ 處查無卡片一律拋 BusinessRuleException(ErrorCodes.CC_CARD_4040, …)(CC-CARD-4040 已登錄為 404)

2.4 修法(採 A 案)

案 做法 評估
(A)★ 採 控制器改拋 BusinessRuleException(ErrorCodes.CC_CARD_4040, …) ★ 與該服務自身 8+ 處慣例一致;CC-CARD-4040 已登錄(404);不動平台、零 blast radius;★ 正與 CR-004 §3.4 #1「移除繞道」同旨——ResponseStatusException 即「繞過錯誤碼登錄表」之繞道
(B) 平台加 @ExceptionHandler(ResponseStatusException.class) ✗ 不採:為單一服務之繞道新增平台行為,違 CR-004 §3.5「不為『可能用到』而預立碼」;且其 reason 若直接外洩將違 CSP-003 §7 E2;狀態碼→錯誤碼之對映亦無登錄依據

★ 殘留風險(如實揭露):本修正移除唯一使用處,但未在平台層設防——
日後若有服務再拋 ResponseStatusException,仍將回 500。
已記入 CSP-001 §7(檢查點:控制器不得以 ResponseStatusException 取代已登錄之錯誤碼),
列後續以掃描器把關(見 §5 待辦)。


3. #A-185:經查證後更正——非缺陷(我方初判有誤)

3.1 初判

Jackson 3(CR-038 升版)預設 FAIL_ON_NULL_FOR_PRIMITIVES=true(Jackson 2 為 false)→
請求 DTO 之 primitive 欄位一省略即 400 CC-PLT-4002。實測:

MerchantSettlementRequest 省略 grossAmount/feeAmount/taxAmount
→ HttpMessageNotReadableException: Cannot map `null` into type `long`

3.2 ★★ 查證:契約怎麼說

契約 Schema required
merchant-service.yaml MerchantSettlementRequest merchantId/settlementDate/grossAmount/feeAmount/taxAmount
limit-service.yaml HoldRequest accountId/holdAmount/holdType
authorization-service.yaml AuthorizeRequest …/amount/…

★ 全部列為 required。

3.3 裁定:不修,並更正判讀

  • 省略契約明定為 required 之欄位 → 400 為正確行為。
  • ★ Jackson 3 之嚴格性反而「把契約的 required 落到了實作」——Jackson 2 會靜默補 0,
    使「漏送必填金額」表現為「金額 0」,這才是危險(帳務情境下尤甚)。
  • 故本項非缺陷,不應「修回」Jackson 2 之寬鬆行為。
    ⚠️ 亦不採 spring.jackson.use-jackson2-defaults=true(Spring Boot 4 確有此旗標,JacksonProperties 實查)
    ——那會連帶改變其他未經評估之預設,違「不求快,求精準無誤」。

3.4 ★ 併立設計規則(記入 CSP-001 §7)

數值欄位之型別依「是否必填」決定:
必填 → 可用 primitive(long/int)(省略即 400,與契約一致);
選填 → 必須用 wrapper(Long/Integer)(null = 未提供)。

⚠️ 反例:MerchantSettlementRequest.batchCount 為 Integer(選填、預設 0)——正確;
若誤寫為 int,選填將變成必填(且編譯與既有測試都不會報)。


4. #A-186:@Valid 未加於部分 @RequestBody

4.1 實測證據

POST /api/v1/authorizations/{authNo}/reversals  body={}  → 未回 400(服務層才擋)
PUT  /api/v1/parameters/{ns}/{key}              body={}  → 200(應 400)
POST /api/v1/products                           body={}  → 201(應 400)

4.2 全庫盤點(實查:45 個 @RequestBody 端點,6 個未加 @Valid)

服務 端點 請求型別 契約 required 判定
authorization-service POST /authorizations/{authNo}/reversals ReversalRequest reversalType/reason ✅ 缺陷
authorization-service POST /authorizations/{authNo}/advices AdviceRequest [](全選填) ⚪ 非缺陷(@Valid 為 no-op;惟一併補上以保持一致)
notification-service POST /notifications/surenotify/webhook @RequestBody String rawBody —(原文簽章驗證) ⚪ 不適用
product-service PUT /api/v1/parameters/{ns}/{key} SetParameterRequest paramValue/valueType/effectiveFrom ✅ 缺陷
product-service POST /api/v1/products CreateProductRequest productCode/cardName/org/cardGrade/effectiveFrom ✅ 缺陷
product-service PUT /api/v1/products/{productCode} CreateProductRequest 同上 ✅ 缺陷

4.3 ★ 併揭露一個可能之 500(同一根因)

AuthorizationController.reverse 之 @RequestBody 無 @Valid → 空 body 之 reversalType 為 null
→ ReversalService.reverse 之 AuthReversal.Type.valueOf(null) 拋 NullPointerException
(未被其 catch (IllegalArgumentException) 涵蓋)→ 500(應 400)。
★ 補 @Valid 後,@NotBlank 於 web 層即擋下 → 400,該 NPE 路徑不可達。

4.4 修法

於上表 4 個端點(reverse/ParameterController.set/ProductController.create/update)
之 @RequestBody 補 @Valid;advise 一併補(一致性)。
★ 契約與 @Valid 之雙向一致性由此建立:契約 required ↔ 巢狀 record 之 @NotBlank/@NotNull ↔ 參數之 @Valid(三者缺一即失效)。


5. 實作與驗證

5.1 實作清單

# 檔案 變更
1 card-service … CardController lookup 之 orElseThrow 改拋 BusinessRuleException(ErrorCodes.CC_CARD_4040, …);移除 ResponseStatusException import
2 authorization-service … AuthorizationController 移除死 import ResponseStatusException;reverse/advise 之 @RequestBody 補 @Valid
3 product-service … ParameterController set 之 @RequestBody 補 @Valid
4 product-service … ProductController create/update 之 @RequestBody 補 @Valid
5 CardControllerTest 移除 @Disabled;改斷言 404 + 錯誤碼 CC-CARD-4040
6 AuthorizationControllerTest reverse/advise 補「空 body → 400(@Valid 生效)」之測試
7 ParameterControllerTest/ProductControllerTest 補「空 body → 400」之測試

5.2 驗證條件

# 條件 期望
1 GET /api/v1/cards/lookup/{不存在的雜湊} 404 CC-CARD-4040(非 500)
2 POST …/reversals body={}(標頭齊備) 400(CC-PLT-4003),不得 500
3 PUT …/parameters/{ns}/{key} body={} 400
4 POST /products body={} 400
5 PUT /products/{code} body={} 400
6 既有全部測試(全量) 0 失敗
7 掃描器(scan_scaffold/scan_mockdata/scan_faketest/validate_sql/validate_mermaid/gen_service_skeleton --check) 全 0 命中

5.3 待辦(本 CR 不含)

# 項 理由
1 CSP-001 §7 新增檢查點「控制器不得以 ResponseStatusException 取代已登錄錯誤碼」 ★ 規則已立(CSP-001 §7.2 G-9);掃描器實作列後續(避免一次動掃描器)
2 CSP-001 §7 新增設計規則「選填數值用 wrapper」 ★ 規則已立(CSP-001 §7.2 G-10);掃描器實作列後續
3 全庫 useJackson2Defaults 之整體評估 ★ 非必要——#A-185 已證明 Jackson 3 之嚴格符合契約
4 ⚠️ 既有之契約↔實作不符(tools/verify_contract_dto.py 掃出 4 處) ★ 非本 CR 引入(實查:本 CR 未動任何回應型別或路徑)。其中 reversals/overrides 之回應型別(契約 AuthorizeResponse vs 實作 ReversalResult/OverrideResult)疑為 CR-019 遺留之待裁定項——如實揭露,另案處理

5.4 ★★ 實測結果(2026-10-05 17:4x)

A. 目標情境(逐項實測)

# 情境 修正前 修正後 判定
1 GET /api/v1/cards/lookup/{不存在的雜湊} 500 CC-PLT-5000 ❌ 404 CC-CARD-4040 ✅
2 POST …/reversals body={}(標頭齊備) 200(mock 未擋;真機恐 500 NPE)❌ 400(@Valid 於 web 層擋下) ✅
3 PUT …/parameters/{ns}/{key} body={} 200 ❌ 400 ✅
4 POST /api/v1/products body={} 201 ❌ 400 ✅
5 PUT /api/v1/products/{code} body={} 201 ❌ 400 ✅

★ 併保留「缺必要標頭 → 400」之測試(reversals/advices/parameters/products),
使 @Valid 與 @RequestHeader 兩類 400 皆有守門。

B. 自動化驗證

項 指令 結果
受影響服務(切片) :services:{card,authorization,product}-service:test --tests "*ControllerTest" BUILD SUCCESSFUL(9 條目標斷言全 PASS)
全量 ./gradlew test --continue ★ BUILD SUCCESSFUL——1,361 測/0 失敗/0 錯誤/1 略過(略過=statement 真機 PDF;★ #A-184 之 @Disabled 已移除,故由 2 降為 1)
掃描器 scan_scaffold/scan_mockdata/scan_faketest/validate_sql/validate_mermaid/gen_service_skeleton --check 全 0 命中(SQL 70 檔 0 違規、Mermaid 0 錯誤、骨架缺漏 0)

C. ★ 一個「順手查到」的既有問題(如實揭露)

tools/verify_contract_dto.py 掃出 4 處不符(reversals/overrides 之回應型別與契約不符等)。
★ 經核對:本 CR 未動任何回應型別或路徑 → 非本 CR 引入,疑為 CR-019 遺留之待裁定項。
不併入本 CR(避免「順手改契約」擴散範圍),已列 §5.3 #4 另案處理。


6. 修訂記錄

版本 日期 修訂者 說明
1.0 2026-10-05 開發團隊 初版。依 BEAM PMO 指示立 CR(「#A-184 是明確的契約違反,那非得立 CR 不可」)。載明 B2 實測揭露之三項發現、逐項查證(含契約 required 之實查)、裁定與實作計畫。★ 含一項我方誤判之更正(#A-185 經契約查證後判定為非缺陷)
1.1 2026-10-05 開發團隊 執行完成。① 狀態改「已立/已執行;待 BEAM PMO 核定」。② §5.3 更新待辦(G-9/G-10 規則已立於 CSP-001 §7.2,掃描器待實作)+ 新增 #4 既有契約不符之如實揭露。③ 新增 §5.4 實測結果——5 條目標情境前後對比、全量 1,361 測/0 失敗/1 略過、掃描器全 0 命中。④ 併載「順手查到之既有問題」(verify_contract_dto 4 處不符,非本 CR 引入)
    (1-1/1)