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 句

  1. 微服務的 bug 不在單一服務、在介面
  2. Contract test > Integration test 為主、Integration 為輔
  3. 沒 deprecation policy = 沒 versioning
  4. Retry 一條鏈只一個地方做
  5. Trace ID 不傳 = debug 不可能

最後

微服務 spec review 是 QA 在分散式系統的主場。單一服務的 spec review 用既有 Spec Review Checklist、跨服務用這份。導入 Pact + 強制 deprecation policy + 統一 timeout / retry — 三個月後 production incident 砍 80%。