Microservices Contract Review — 跨服務 API 不一致的 8 個典型漏洞
微服務最大的痛不是寫不出來、是「服務 A 改了但 B 不知道」。Spec review 時除了單一 API、還要看跨服務的 contract 是不是會崩。這篇給你完整框架。
為什麼微服務 spec 特別難 review
flowchart LR
Mono[單體 app] --> M1[一個 spec / 一個 repo]
Mono --> M2[編譯時抓不一致]
Mono --> M3[一起發版]
Micro[微服務] --> N1[N 個 spec / N 個 team]
Micro --> N2[runtime 才抓錯]
Micro --> N3[獨立發版]
Micro --> N4[版本不同步]
Micro --> N5[網路 + 序列化問題]
style Mono fill:#10b981,color:#fff
style Micro fill:#ef4444,color:#fff
單體 review 看 1 個檔、微服務看 N × M 個介面。
8 個典型漏洞
mindmap
root((微服務 Contract<br>典型漏洞))
1 schema 漂移
Provider 加欄位
Consumer 沒處理 null
2 enum 擴張
Provider 加新狀態
Consumer switch 沒 default
3 版本不同步
v1 / v2 並存
不同 consumer 用不同版
4 錯誤碼變
ERR_001 → SERVICE_UNAVAILABLE
consumer 寫死字串
5 timeout 不一致
A 預設 5s
B 預設 30s
鏈條 timeout 不對
6 retry 衝突
consumer + provider 都 retry
變雪崩
7 序列化版本
date format
number precision
enum string vs int
8 訊息順序
Async 訊息亂序
consumer 沒設 idempotency
漏洞 1: Schema 漂移
sequenceDiagram
participant P as Provider Team
participant C as Consumer Team
P->>P: 加新欄位 `verified_at` (nullable)
P->>P: 沒通知 C、直接 deploy
C->>P: GET /users/123
P-->>C: {"id":123, "verified_at":null}
C->>C: ❌ Cannot read property of null
Review 該問: - 新增欄位有 nullable / default? - Consumer 端有 null check? - Schema 變更通知流程?
漏洞 2: Enum 擴張
// Provider 加新狀態
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled' | 'refunded'; // ← 新加
// Consumer 寫法
switch (status) {
case 'pending': return '處理中';
case 'paid': return '已付款';
case 'shipped': return '已出貨';
case 'cancelled': return '已取消';
// ❌ refunded 沒處理 → undefined
}
Review 該問: - 新增 enum 有對齊 consumer 嗎? - Default case 處理? - Enum 拓展屬 breaking change?(不全是、但要分類)
漏洞 3: 版本不同步
Service A (v1) ---calls---> Service B (v2)
↑
v1 早已 deprecate
但 Service A 沒升
Review 該問: - 版本 deprecation policy? - 多少 consumer 用 v1? - Migration 強制 deadline?
漏洞 4: 錯誤碼變
Provider v1: error.code = "INSUFFICIENT_BALANCE"
Provider v2: error.code = "INSUFFICIENT_FUNDS" ← 改了
Consumer:
if (err.code === "INSUFFICIENT_BALANCE") { /* 處理 */ }
// ❌ v2 後永遠跑不到這
Review 該問: - 錯誤碼有 changelog 嗎? - 是否視為 breaking change?
漏洞 5: Timeout 不一致
User → API Gateway (60s timeout)
→ Service A (10s)
→ Service B (30s)
→ DB (5s)
User 等了 60s 才看到錯誤、但 Service B 早超 A 的 timeout 了
Review 該問: - 每層 timeout 是否 propagate? - 是否設 deadline 一致? - Retry 策略誰負責?
漏洞 6: Retry 衝突
sequenceDiagram
participant C as Client
participant A as Service A
participant B as Service B
C->>A: POST /order (with retry 3)
A->>B: createOrder (with retry 3)
Note over A,B: B 慢 — A timeout
A->>B: retry 1 (createOrder)
A->>B: retry 2 (createOrder)
Note over C: client 也 timeout
C->>A: retry 1 (POST /order)
A->>B: createOrder × 3 again
Note over B: 💥 訂單建了 6 次
Review 該問: - 哪一層 retry? - Idempotency key? - Exponential backoff?
漏洞 7: 序列化版本
| 欄位 | Provider | Consumer 期待 |
|---|---|---|
| created_at | 1734567890 (Unix sec) |
2026-06-13T10:00:00Z (ISO) |
| price | 99 |
99.00 |
| status | "paid" |
1 (整數) |
Review 該問: - 日期一律 ISO 8601? - 數字 precision 規範? - Enum 是 string 還是 int?
漏洞 8: 訊息順序(async)
Producer 順序送:
msg1: order.created
msg2: order.paid
msg3: order.shipped
Consumer 收到:
msg3 (網路快)
msg1
msg2
Consumer 處理 msg3 時 → 「order 不存在」
Review 該問: - 訊息有 sequence number? - Consumer 有 idempotency? - Out-of-order 容錯機制?
Consumer-Driven Contract Testing (CDC)
flowchart LR
C[Consumer Team] -->|寫期望| Pact[Pact File<br>JSON]
Pact --> P[Provider Team]
P -->|跑 verify| Result{符合?}
Result -->|是| Pass[✓ Build pass]
Result -->|否| Fail[❌ Provider 知道<br>breaking change]
style Pact fill:#06b6d4,color:#fff
style Pass fill:#10b981,color:#fff
style Fail fill:#ef4444,color:#fff
Pact 工作流
// Consumer (寫期望)
import { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'web-app',
provider: 'user-service',
});
await provider.addInteraction({
state: 'user 123 exists',
uponReceiving: 'a request for user 123',
withRequest: { method: 'GET', path: '/users/123' },
willRespondWith: {
status: 200,
body: { id: 123, email: '[email protected]' },
},
});
# Provider (verify 跑 pact file)
pact-verifier --provider-base-url http://localhost:8080 \
--pact-url ./pacts/web-app-user-service.json
Provider 改 schema 不符合 → CI 直接擋。
微服務 Spec Review Checklist
mindmap
root((微服務<br>Spec Review))
Contract
欄位增刪
Enum 擴張
Schema 嚴格度
Versioning
Breaking change?
Deprecation period
Migration plan
Error
錯誤碼穩定
4xx vs 5xx 分類
Error payload 結構
Timeout
每層 timeout
Deadline propagate
Circuit breaker
Retry
策略誰負責
Idempotency
Exponential backoff
Async
訊息格式
順序保證
重複處理
Observability
Trace ID 傳遞
每跳 log
Metric 暴露
Auth
Service-to-service
Token TTL
Permission 傳遞
QA 角度的工具
| 工具 | 用途 |
|---|---|
| Pact | REST + async contract test |
| Schemathesis | OpenAPI fuzz testing |
| Dredd | OpenAPI 對 implementation 比對 |
| GraphQL Inspector | GraphQL schema diff |
| Postman Mocks | 模擬 provider |
| WireMock | 自架 mock server |
反模式
flowchart TD
Anti[微服務 contract 反模式] --> A1["每次改 schema 不通知"]
Anti --> A2["consumer 跟 provider 同 repo"]
Anti --> A3["所有 retry 全做"]
Anti --> A4["不寫 deprecation policy"]
Anti --> A5["錯誤碼 freely 改"]
Anti --> A6["timeout 各自設"]
Anti --> A7["沒 trace id"]
style A1 fill:#ef4444,color:#fff
style A2 fill:#ef4444,color:#fff
style A3 fill:#ef4444,color:#fff
style A4 fill:#ef4444,color:#fff
style A5 fill:#ef4444,color:#fff
style A6 fill:#ef4444,color:#fff
style A7 fill:#ef4444,color:#fff
給 QA Lead 的 5 句
- 微服務的 bug 不在單一服務、在介面
- Contract test > Integration test 為主、Integration 為輔
- 沒 deprecation policy = 沒 versioning
- Retry 一條鏈只一個地方做
- Trace ID 不傳 = debug 不可能
最後
微服務 spec review 是 QA 在分散式系統的主場。單一服務的 spec review 用既有 Spec Review Checklist、跨服務用這份。導入 Pact + 強制 deprecation policy + 統一 timeout / retry — 三個月後 production incident 砍 80%。