<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Error-Contract on Tarragon</title><link>https://tarrragon.github.io/blog/tags/error-contract/</link><description>Recent content in Error-Contract on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 04 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/error-contract/index.xml" rel="self" type="application/rss+xml"/><item><title>11.11 Status 與錯誤的雙向契約</title><link>https://tarrragon.github.io/blog/backend/11-api-design/error-bidirectional-contract/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/error-bidirectional-contract/</guid><description>&lt;p>status code 與錯誤回應是 provider 與 consumer 之間的合作契約、不是 provider 單方的輸出格式。兩端理論上是合作對象 —— provider 要 consumer 正確使用服務、consumer 要 provider 給出可判讀的行為指示 —— 但商業上常因地位不對等、由強勢一方片面從自己的需求設計、把成本外部化給對方：平台不給 debug 資訊、消費者只能猜；消費者盲目重試、provider 在過載時被自己的用戶打垮。本章立這份契約的雙向判準；每個設計決策的判別問題是「這是在解決問題、還是把成本推給對方」。&lt;/p>
&lt;p>status 語意的粗承諾在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/resource-modeling-operation-semantics/" data-link-title="11.3 資源建模與操作語意" data-link-desc="endpoint 該建模成資源還是動作、HTTP method 與 status 承諾了什麼、available actions 由誰計算 — 建模決策的判準">11.3&lt;/a>、錯誤格式設計在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4&lt;/a>、限流語意在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9&lt;/a> —— 本章不重述這三章的 producer 側設計、收的是它們共同缺的另一半：consumer 端拿到之後怎麼辦、以及兩端對彼此的期望怎麼寫進契約。&lt;/p>
&lt;h2 id="兩端各自期望什麼">兩端各自期望什麼&lt;/h2>
&lt;p>consumer 對錯誤回應的期望收斂成四件：&lt;strong>可判讀的行為指示&lt;/strong>（這個錯誤重試有沒有用、要等多久 —— 對應 11.4 的第一刀「可重試與終態」分類、11.9 的 Retry-After）；&lt;strong>可分支的機器碼&lt;/strong>（程式能走 switch、不用 parse 人類訊息 —— 對應 11.4 的 type/code 層）；&lt;strong>可自助的 debug 入口&lt;/strong>（error 帶 request-id 或 trace id、回報時引用它就能被定位）；&lt;strong>穩定性&lt;/strong>（錯誤格式與語意的變更跟正常回應一樣是 breaking change、對應 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/backward-compatibility-discipline/" data-link-title="11.6 向後相容的變更紀律" data-link-desc="哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律">11.6&lt;/a>）。&lt;/p>
&lt;p>provider 對 consumer 的期望同樣具體：守 retry 紀律（退避間隔加隨機抖動、有上限、對過載退讓）；快速 ack（多快看 vendor 明文、如 GitHub 的 10 秒、Slack 的 3 秒）、把慢邏輯移出回應路徑；用 event id 去重、不依賴投遞順序（webhook 場景、見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/styles/realtime/realtime-webhook-contract/" data-link-title="webhook 對外承諾：投遞保證不是預設、consumer 負責一半" data-link-desc="webhook 是盡力而為的事件推送不是可靠佇列：投遞保證逐 vendor 讀、可靠性責任分一半給 consumer">realtime 流派層&lt;/a>）；把機器分支寫在 type/code 上、不 parse message 文字 —— consumer 把人類可讀欄位當契約、之後 provider 改個錯字都變 breaking change（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hyrums-law/" data-link-title="Hyrum&amp;#39;s Law" data-link-desc="使用者夠多時、介面的一切可觀察行為都會被依賴 — 不管你承諾了什麼；契約設計要主動給機器可讀欄位、否則人類可讀欄位會被迫變成契約">Hyrum&amp;rsquo;s Law&lt;/a>（一切可觀察行為終將被依賴）的錯誤版、案例見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-aip193-error-content/" data-link-title="11.C75 AIP-193 錯誤內容規範：三層受眾與「不假設使用者懂內部實作」" data-link-desc="機器可讀的 (reason, domain) 契約、developer-facing message、LocalizedMessage 三層分工；message 穩定性規則反向揭露 Hyrum&amp;#39;s Law">11.C75&lt;/a> 的 message 穩定性條款）。&lt;/p>
&lt;p>這兩張清單合起來是本章的骨架：契約寫得好、兩張清單都成立；寫得偏、一邊的成本變成另一邊的日常。&lt;/p>
&lt;h2 id="成本外部化的判讀訊號">成本外部化的判讀訊號&lt;/h2>
&lt;p>單邊設計的產物有固定形態、兩個方向都有。provider 側轉嫁：業務失敗包 200（把「讀 body 才知道成敗」的解析成本推給 consumer、順便讓自己的錯誤率圖表失真、見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/resource-modeling-operation-semantics/" data-link-title="11.3 資源建模與操作語意" data-link-desc="endpoint 該建模成資源還是動作、HTTP method 與 status 承諾了什麼、available actions 由誰計算 — 建模決策的判準">11.3 判讀訊號&lt;/a>）；不給機器可讀 code（分支成本推給 consumer 去 parse message）；不給 request-id（debug 成本推給 consumer 與自己的 support 團隊）；用兩種 status 表達同一件事且不明文劃分時機（GitHub 超限回 403 或 429、consumer 分支邏輯雙倍、見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/ratelimit-github-primary-secondary/" data-link-title="11.C43 GitHub 雙層限流：primary / secondary 與 x-ratelimit 契約" data-link-desc="單一維度配額擋不住真實濫用、前標準時代 x- header 與 IETF 命名並存的遷移期現實">11.C43&lt;/a>、語意判準主寫在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9&lt;/a>）；完全不重試的 webhook（重試責任整包轉給 consumer 自建排程、見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/webhook-github-no-retry/" data-link-title="11.C61 GitHub webhooks：不自動重試的反向承諾" data-link-desc="at-least-once 不是所有 vendor 都給：GitHub 明文只試一次、失敗靠 consumer 自建排程補投；逼你讀 vendor 明文而非假設">11.C61&lt;/a>）。最後一項要再切一刀：GitHub 把不重試寫進文件、附補投 API 與投遞狀態查詢 —— 明文轉移是可規劃的契約條款、跟默默轉嫁（不明說、consumer 事後才發現）是兩回事；判讀的重點在「對方知不知道自己接了這筆成本」、不在成本移動本身。&lt;/p></description><content:encoded><![CDATA[<p>status code 與錯誤回應是 provider 與 consumer 之間的合作契約、不是 provider 單方的輸出格式。兩端理論上是合作對象 —— provider 要 consumer 正確使用服務、consumer 要 provider 給出可判讀的行為指示 —— 但商業上常因地位不對等、由強勢一方片面從自己的需求設計、把成本外部化給對方：平台不給 debug 資訊、消費者只能猜；消費者盲目重試、provider 在過載時被自己的用戶打垮。本章立這份契約的雙向判準；每個設計決策的判別問題是「這是在解決問題、還是把成本推給對方」。</p>
<p>status 語意的粗承諾在 <a href="/blog/backend/11-api-design/resource-modeling-operation-semantics/" data-link-title="11.3 資源建模與操作語意" data-link-desc="endpoint 該建模成資源還是動作、HTTP method 與 status 承諾了什麼、available actions 由誰計算 — 建模決策的判準">11.3</a>、錯誤格式設計在 <a href="/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4</a>、限流語意在 <a href="/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9</a> —— 本章不重述這三章的 producer 側設計、收的是它們共同缺的另一半：consumer 端拿到之後怎麼辦、以及兩端對彼此的期望怎麼寫進契約。</p>
<h2 id="兩端各自期望什麼">兩端各自期望什麼</h2>
<p>consumer 對錯誤回應的期望收斂成四件：<strong>可判讀的行為指示</strong>（這個錯誤重試有沒有用、要等多久 —— 對應 11.4 的第一刀「可重試與終態」分類、11.9 的 Retry-After）；<strong>可分支的機器碼</strong>（程式能走 switch、不用 parse 人類訊息 —— 對應 11.4 的 type/code 層）；<strong>可自助的 debug 入口</strong>（error 帶 request-id 或 trace id、回報時引用它就能被定位）；<strong>穩定性</strong>（錯誤格式與語意的變更跟正常回應一樣是 breaking change、對應 <a href="/blog/backend/11-api-design/backward-compatibility-discipline/" data-link-title="11.6 向後相容的變更紀律" data-link-desc="哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律">11.6</a>）。</p>
<p>provider 對 consumer 的期望同樣具體：守 retry 紀律（退避間隔加隨機抖動、有上限、對過載退讓）；快速 ack（多快看 vendor 明文、如 GitHub 的 10 秒、Slack 的 3 秒）、把慢邏輯移出回應路徑；用 event id 去重、不依賴投遞順序（webhook 場景、見 <a href="/blog/backend/11-api-design/styles/realtime/realtime-webhook-contract/" data-link-title="webhook 對外承諾：投遞保證不是預設、consumer 負責一半" data-link-desc="webhook 是盡力而為的事件推送不是可靠佇列：投遞保證逐 vendor 讀、可靠性責任分一半給 consumer">realtime 流派層</a>）；把機器分支寫在 type/code 上、不 parse message 文字 —— consumer 把人類可讀欄位當契約、之後 provider 改個錯字都變 breaking change（<a href="/blog/backend/knowledge-cards/hyrums-law/" data-link-title="Hyrum&#39;s Law" data-link-desc="使用者夠多時、介面的一切可觀察行為都會被依賴 — 不管你承諾了什麼；契約設計要主動給機器可讀欄位、否則人類可讀欄位會被迫變成契約">Hyrum&rsquo;s Law</a>（一切可觀察行為終將被依賴）的錯誤版、案例見 <a href="/blog/backend/11-api-design/cases/errorchain-aip193-error-content/" data-link-title="11.C75 AIP-193 錯誤內容規範：三層受眾與「不假設使用者懂內部實作」" data-link-desc="機器可讀的 (reason, domain) 契約、developer-facing message、LocalizedMessage 三層分工；message 穩定性規則反向揭露 Hyrum&#39;s Law">11.C75</a> 的 message 穩定性條款）。</p>
<p>這兩張清單合起來是本章的骨架：契約寫得好、兩張清單都成立；寫得偏、一邊的成本變成另一邊的日常。</p>
<h2 id="成本外部化的判讀訊號">成本外部化的判讀訊號</h2>
<p>單邊設計的產物有固定形態、兩個方向都有。provider 側轉嫁：業務失敗包 200（把「讀 body 才知道成敗」的解析成本推給 consumer、順便讓自己的錯誤率圖表失真、見 <a href="/blog/backend/11-api-design/resource-modeling-operation-semantics/" data-link-title="11.3 資源建模與操作語意" data-link-desc="endpoint 該建模成資源還是動作、HTTP method 與 status 承諾了什麼、available actions 由誰計算 — 建模決策的判準">11.3 判讀訊號</a>）；不給機器可讀 code（分支成本推給 consumer 去 parse message）；不給 request-id（debug 成本推給 consumer 與自己的 support 團隊）；用兩種 status 表達同一件事且不明文劃分時機（GitHub 超限回 403 或 429、consumer 分支邏輯雙倍、見 <a href="/blog/backend/11-api-design/cases/ratelimit-github-primary-secondary/" data-link-title="11.C43 GitHub 雙層限流：primary / secondary 與 x-ratelimit 契約" data-link-desc="單一維度配額擋不住真實濫用、前標準時代 x- header 與 IETF 命名並存的遷移期現實">11.C43</a>、語意判準主寫在 <a href="/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9</a>）；完全不重試的 webhook（重試責任整包轉給 consumer 自建排程、見 <a href="/blog/backend/11-api-design/cases/webhook-github-no-retry/" data-link-title="11.C61 GitHub webhooks：不自動重試的反向承諾" data-link-desc="at-least-once 不是所有 vendor 都給：GitHub 明文只試一次、失敗靠 consumer 自建排程補投；逼你讀 vendor 明文而非假設">11.C61</a>）。最後一項要再切一刀：GitHub 把不重試寫進文件、附補投 API 與投遞狀態查詢 —— 明文轉移是可規劃的契約條款、跟默默轉嫁（不明說、consumer 事後才發現）是兩回事；判讀的重點在「對方知不知道自己接了這筆成本」、不在成本移動本身。</p>
<p>consumer 側轉嫁：盲目 retry 把恢復成本推回 provider —— 失敗源於過載時、retry 是持續攻擊（AWS 內部元件把錯誤率推到 55% 的實例、見 <a href="/blog/backend/11-api-design/cases/retry-dynamodb-2015-storm/" data-link-title="11.C70 AWS DynamoDB 2015 事故：內部元件的 retry 自保把錯誤率推到 55%（反例）" data-link-desc="反例：metadata 服務過載後 storage server 逾時自我下線再重試、風暴成形後系統不自癒、要人工暫停請求才能喘息；事後修正同時動兩端">11.C70</a>；retry 行為常繼承自 SDK 預設而非顯式選擇、審自己依賴堆疊的預設值也是 consumer 的義務）；多層各自 retry、三層各三次在底層疊成 64 次嘗試（見 <a href="/blog/backend/11-api-design/cases/retry-sre-book-cascading-failures/" data-link-title="11.C69 Google SRE Book：retry 放大與跨層疊乘、per-request 上限與 retry budget" data-link-desc="retry 放大讓有效工作遞減；三層各 retry 3 次在底層變 64 次；建議 per-request 上限 &#43; server-wide retry budget、provider 要用不同 code 分開可重試與不可重試">11.C69</a>）；parse message 文字做分支、把自己的穩定性押在 provider 不改字上。</p>
<p>判讀方法：看到任何一項、先問成本被推到哪一端、再回對應的深度文章找修法。</p>
<h2 id="深度議題分流">深度議題分流</h2>
<p>雙向契約的複雜度集中在幾個議題、各有一篇深度文章：</p>
<p><strong>status 裝不下的東西</strong>。單一 status 有三種表達力邊界：裝不下多個結果（部分成功）、裝不下時間軸（202 之後才失敗）、裝不下不確定性（504 分不出「沒送到」還是「執行了」）。兩條處理路線 —— 把狀態表下放 body、或收窄語意保持單一 status 恆為真 —— 在 <a href="/blog/backend/11-api-design/status-expressiveness-boundary/" data-link-title="Status 裝不下的東西：部分成功、延遲失敗、gateway 歧義" data-link-desc="單一 status 表達不了部分成功、延遲失敗與 gateway 歧義時怎麼辦：把狀態下放 body 讓中介層變盲、或收窄語意保持 status 恆為真">status 表達力邊界</a> 攤開。</p>
<p><strong>收到錯誤之後重不重試</strong>。這是 consumer 最頻繁的決策、也是雙向責任最典型的場景：單一請求層看 status 加冪等合判、集體層要 backoff 加 jitter 防同步波、架構層要決定 retry 放哪一層、配 retry budget 與 circuit breaker。完整判準在 <a href="/blog/backend/11-api-design/consumer-retry-decision/" data-link-title="接收方的重試決策：從單一請求到 retry 風暴" data-link-desc="收到錯誤之後重不重試：從單請求的 status 加冪等合判、集體的去同步責任、到 retry 預算與斷路閘門">接收方的重試決策</a>。</p>
<p><strong>錯誤跨服務怎麼傳</strong>。A 呼叫 B、B 呼叫 C、C 掛了 —— B 同時是 consumer 跟 provider、要決定透傳還是轉譯、以及錯誤細節暴露多少（機器可讀 vs 攻擊偵察面的張力）。在 <a href="/blog/backend/11-api-design/error-propagation-trust-boundary/" data-link-title="錯誤傳播與信任邊界：中間服務的雙重身分" data-link-desc="錯誤跨服務傳遞時誰該轉譯、收到的錯誤能信多少、對外暴露多少細節 — 服務鏈上每一跳同時是 consumer 與 provider 的責任判準">錯誤傳播與信任邊界</a>。</p>
<p><strong>收到錯誤之後怎麼溝通</strong>。consumer 拿 request-id 或 trace id 回報、provider 承諾用它定位 —— 這條回饋迴路是雙向 debug 契約、也是地位不對等最常見的缺口（不給 id、consumer 只能用「大概幾點、大概什麼操作」描述問題）。在 <a href="/blog/backend/11-api-design/error-feedback-loop/" data-link-title="錯誤回報的回饋迴路：request-id、trace 與呈現回報分工" data-link-desc="consumer 收到錯誤之後怎麼跟 provider 溝通：error 要帶什麼定位鉤子、同一個錯誤怎麼分別投影給使用者與回報、持續錯誤什麼時候該升級">錯誤回報的回饋迴路</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>錯誤格式的 producer 側設計：<a href="/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4 錯誤模型設計</a></li>
<li>status 承諾的地基：<a href="/blog/backend/11-api-design/resource-modeling-operation-semantics/" data-link-title="11.3 資源建模與操作語意" data-link-desc="endpoint 該建模成資源還是動作、HTTP method 與 status 承諾了什麼、available actions 由誰計算 — 建模決策的判準">11.3 資源建模與操作語意</a></li>
<li>429 與配額語意：<a href="/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9 對外流量語意</a></li>
<li>重送安全的冪等機制：<a href="/blog/backend/11-api-design/api-idempotency-design/" data-link-title="11.8 API 層冪等設計" data-link-desc="idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況">11.8 API 層冪等設計</a></li>
<li>同一批診斷欄位的觀測動機：<a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19 Debuggability by Design</a></li>
<li>案例原文：<a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a></li>
</ul>
]]></content:encoded></item><item><title>Status 裝不下的東西：部分成功、延遲失敗、gateway 歧義</title><link>https://tarrragon.github.io/blog/backend/11-api-design/status-expressiveness-boundary/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/status-expressiveness-boundary/</guid><description>&lt;p>status code 是整條 HTTP 鏈上被最多角色消費的一個欄位：consumer 的分支邏輯、中介層的 retry 與快取、監控的錯誤率圖表、全部只看這一格。它的表達力邊界因此是契約設計的硬約束 —— 有三種情況、一個 status 放不下事實：多個獨立結果（部分成功）、跨越時間的結果（先接受後失敗）、無法確定的結果（gateway 分不出上游做了沒）。本文攤開三種邊界、以及每種邊界下兩端各要負責什麼。本文承接 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約&lt;/a>。&lt;/p>
&lt;h2 id="裝不下多個結果部分成功的兩條路線">裝不下多個結果：部分成功的兩條路線&lt;/h2>
&lt;p>批次操作 5 筆裡 3 成功 2 失敗、一個 status 表達不了。規範與大廠給了方向相反的兩條路線、對照著讀最清楚。&lt;/p>
&lt;p>WebDAV 的 207 Multi-Status 是「下放」路線：頂層回 207、每個資源的真實狀態放進 body 的 multistatus 結構、規範明文接收方「needs to consult the contents of the multistatus response body」、207 可以同時用在全成功、部分成功、全失敗（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/status-207-multistatus-rfc4918/" data-link-title="11.C64 RFC 4918 207 Multi-Status：status line 降格為「請讀 body」" data-link-desc="一個 status code 裝不下批次結果：WebDAV 把每資源狀態下放到 body、解析責任隨之轉給 consumer">11.C64&lt;/a>）。這條路線換到表達力、代價由 consumer 端整條鏈承擔：generic client 與中介層（retry、快取、監控）只看 status line、207 對它們一律是成功 —— 部分失敗只有讀得懂 body schema 的 client 看得到、監控圖表上這批半失敗的請求全是綠的。業界更常見的下放形態其實是 200 加 per-item errors（Elasticsearch 的 bulk API、GraphQL 的 errors 欄位都是這條路）：中介層盲化問題與 207 同構、而且連 207 那個「非常規 status」的警示訊號都沒有。&lt;/p>
&lt;p>Google AIP 是「收窄」路線、而且立場寫得很硬：AIP-193 明文「APIs should not support partial errors」—— 部分錯誤把錯誤碼搬進 response body、consumer 就得寫專用錯誤處理、通用機制全部失效（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/status-google-aip-partial-success/" data-link-title="11.C65 Google AIP 部分成功立場：同步必原子、非同步才准部分成功且要顯式 opt-in" data-link-desc="AIP-193 明文「不該支援 partial errors」、批次三部曲給出原子性階梯與 LRO 出口：部分成功要 client 顯式同意">11.C65&lt;/a>）。批次方法的配套規則是一條原子性階梯：同步批次必須原子（全成或全敗、讓單一 status 恆為真；唯讀批次更直接禁止部分成功）；寫入批次要部分成功、必須升級成非同步 operation、失敗明細結構化進 metadata 的 &lt;code>failed_requests&lt;/code> map、且 request 要帶 &lt;code>return_partial_success&lt;/code> 讓 consumer 顯式 opt-in。&lt;/p>
&lt;p>兩條路線的差異正是雙向契約的分野：207 把解析責任&lt;strong>默默&lt;/strong>推給 consumer（收到的人自己發現要讀 body）；AIP 把同一份責任變成&lt;strong>顯式同意&lt;/strong>（consumer 用 opt-in flag 聲明「我會處理部分失敗」、provider 才回部分成功）。設計判準由此而來 —— 部分成功的設計題是「consumer 有沒有明知道自己要處理它」、能不能做反而其次；讓中介層誤判的表達方式（200 或 207 包部分失敗、但消費端沒有 opt-in）是把成本外部化的形態。&lt;/p>
&lt;h2 id="裝不下時間軸202-之後才失敗">裝不下時間軸：202 之後才失敗&lt;/h2>
&lt;p>202 Accepted 的規範定位是刻意不承諾。RFC 9110 原文：「The 202 response is intentionally noncommittal」、且「There is no facility in HTTP for re-sending a status code from an asynchronous operation」（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/status-202-noncommittal-rfc9110/" data-link-title="11.C66 RFC 9110 202 Accepted：接受不等於承諾、HTTP 沒有回傳非同步結果的機制" data-link-desc="202 是規範明文的 intentionally noncommittal：一旦回了 202、協定層不再有管道通知最終失敗、通知責任移轉到應用層">11.C66&lt;/a>）—— 一旦回了 202、HTTP 協定不再提供任何管道通知最終失敗。status 只描述「收到當下」、描述不了「之後會不會成」。&lt;/p>
&lt;p>責任移轉因此是 202 的內建性質、兩端都要有對應動作。provider 端：202 的回應要指向一個 status monitor（規範用「ought to」、實務上是 Operation resource —— 可輪詢的長時操作資源、設計見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/longrun-google-aip151/" data-link-title="11.C44 Google AIP-151：長時操作實體化成 Operation resource" data-link-desc="202 &amp;#43; 輪詢模式的系統化版本：回應型別先宣告、client 統一寫一套 polling、operation 生命週期明訂">11.C44 AIP-151&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/collection-interface-design/" data-link-title="11.7 集合介面設計" data-link-desc="分頁方案的承諾差異、批次操作的部分失敗語意、超過請求逾時的長時操作怎麼回 — 集合與長時操作的介面判準">11.7 的長時操作段&lt;/a>）—— 只回 202 不給查詢入口、等於把「結果去哪了」變成 consumer 的問題。consumer 端：把 202 當終局成功、最終失敗就靜默消失 —— 拿到 202 的正確做法是記下 operation 入口、把「確認終局」排進自己的流程。同一個時間軸問題在 webhook 方向更隱蔽：先回 2xx ack、背景處理才失敗 —— ack 的是「收到」、不是「處理成功」、對帳兜底因此是 consumer 的常備件（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/styles/realtime/realtime-webhook-contract/" data-link-title="webhook 對外承諾：投遞保證不是預設、consumer 負責一半" data-link-desc="webhook 是盡力而為的事件推送不是可靠佇列：投遞保證逐 vendor 讀、可靠性責任分一半給 consumer">webhook 對外承諾&lt;/a>）。&lt;/p></description><content:encoded><![CDATA[<p>status code 是整條 HTTP 鏈上被最多角色消費的一個欄位：consumer 的分支邏輯、中介層的 retry 與快取、監控的錯誤率圖表、全部只看這一格。它的表達力邊界因此是契約設計的硬約束 —— 有三種情況、一個 status 放不下事實：多個獨立結果（部分成功）、跨越時間的結果（先接受後失敗）、無法確定的結果（gateway 分不出上游做了沒）。本文攤開三種邊界、以及每種邊界下兩端各要負責什麼。本文承接 <a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約</a>。</p>
<h2 id="裝不下多個結果部分成功的兩條路線">裝不下多個結果：部分成功的兩條路線</h2>
<p>批次操作 5 筆裡 3 成功 2 失敗、一個 status 表達不了。規範與大廠給了方向相反的兩條路線、對照著讀最清楚。</p>
<p>WebDAV 的 207 Multi-Status 是「下放」路線：頂層回 207、每個資源的真實狀態放進 body 的 multistatus 結構、規範明文接收方「needs to consult the contents of the multistatus response body」、207 可以同時用在全成功、部分成功、全失敗（見 <a href="/blog/backend/11-api-design/cases/status-207-multistatus-rfc4918/" data-link-title="11.C64 RFC 4918 207 Multi-Status：status line 降格為「請讀 body」" data-link-desc="一個 status code 裝不下批次結果：WebDAV 把每資源狀態下放到 body、解析責任隨之轉給 consumer">11.C64</a>）。這條路線換到表達力、代價由 consumer 端整條鏈承擔：generic client 與中介層（retry、快取、監控）只看 status line、207 對它們一律是成功 —— 部分失敗只有讀得懂 body schema 的 client 看得到、監控圖表上這批半失敗的請求全是綠的。業界更常見的下放形態其實是 200 加 per-item errors（Elasticsearch 的 bulk API、GraphQL 的 errors 欄位都是這條路）：中介層盲化問題與 207 同構、而且連 207 那個「非常規 status」的警示訊號都沒有。</p>
<p>Google AIP 是「收窄」路線、而且立場寫得很硬：AIP-193 明文「APIs should not support partial errors」—— 部分錯誤把錯誤碼搬進 response body、consumer 就得寫專用錯誤處理、通用機制全部失效（見 <a href="/blog/backend/11-api-design/cases/status-google-aip-partial-success/" data-link-title="11.C65 Google AIP 部分成功立場：同步必原子、非同步才准部分成功且要顯式 opt-in" data-link-desc="AIP-193 明文「不該支援 partial errors」、批次三部曲給出原子性階梯與 LRO 出口：部分成功要 client 顯式同意">11.C65</a>）。批次方法的配套規則是一條原子性階梯：同步批次必須原子（全成或全敗、讓單一 status 恆為真；唯讀批次更直接禁止部分成功）；寫入批次要部分成功、必須升級成非同步 operation、失敗明細結構化進 metadata 的 <code>failed_requests</code> map、且 request 要帶 <code>return_partial_success</code> 讓 consumer 顯式 opt-in。</p>
<p>兩條路線的差異正是雙向契約的分野：207 把解析責任<strong>默默</strong>推給 consumer（收到的人自己發現要讀 body）；AIP 把同一份責任變成<strong>顯式同意</strong>（consumer 用 opt-in flag 聲明「我會處理部分失敗」、provider 才回部分成功）。設計判準由此而來 —— 部分成功的設計題是「consumer 有沒有明知道自己要處理它」、能不能做反而其次；讓中介層誤判的表達方式（200 或 207 包部分失敗、但消費端沒有 opt-in）是把成本外部化的形態。</p>
<h2 id="裝不下時間軸202-之後才失敗">裝不下時間軸：202 之後才失敗</h2>
<p>202 Accepted 的規範定位是刻意不承諾。RFC 9110 原文：「The 202 response is intentionally noncommittal」、且「There is no facility in HTTP for re-sending a status code from an asynchronous operation」（見 <a href="/blog/backend/11-api-design/cases/status-202-noncommittal-rfc9110/" data-link-title="11.C66 RFC 9110 202 Accepted：接受不等於承諾、HTTP 沒有回傳非同步結果的機制" data-link-desc="202 是規範明文的 intentionally noncommittal：一旦回了 202、協定層不再有管道通知最終失敗、通知責任移轉到應用層">11.C66</a>）—— 一旦回了 202、HTTP 協定不再提供任何管道通知最終失敗。status 只描述「收到當下」、描述不了「之後會不會成」。</p>
<p>責任移轉因此是 202 的內建性質、兩端都要有對應動作。provider 端：202 的回應要指向一個 status monitor（規範用「ought to」、實務上是 Operation resource —— 可輪詢的長時操作資源、設計見 <a href="/blog/backend/11-api-design/cases/longrun-google-aip151/" data-link-title="11.C44 Google AIP-151：長時操作實體化成 Operation resource" data-link-desc="202 &#43; 輪詢模式的系統化版本：回應型別先宣告、client 統一寫一套 polling、operation 生命週期明訂">11.C44 AIP-151</a> 與 <a href="/blog/backend/11-api-design/collection-interface-design/" data-link-title="11.7 集合介面設計" data-link-desc="分頁方案的承諾差異、批次操作的部分失敗語意、超過請求逾時的長時操作怎麼回 — 集合與長時操作的介面判準">11.7 的長時操作段</a>）—— 只回 202 不給查詢入口、等於把「結果去哪了」變成 consumer 的問題。consumer 端：把 202 當終局成功、最終失敗就靜默消失 —— 拿到 202 的正確做法是記下 operation 入口、把「確認終局」排進自己的流程。同一個時間軸問題在 webhook 方向更隱蔽：先回 2xx ack、背景處理才失敗 —— ack 的是「收到」、不是「處理成功」、對帳兜底因此是 consumer 的常備件（見 <a href="/blog/backend/11-api-design/styles/realtime/realtime-webhook-contract/" data-link-title="webhook 對外承諾：投遞保證不是預設、consumer 負責一半" data-link-desc="webhook 是盡力而為的事件推送不是可靠佇列：投遞保證逐 vendor 讀、可靠性責任分一半給 consumer">webhook 對外承諾</a>）。</p>
<h2 id="裝不下不確定性502504-的-retry-歧義">裝不下不確定性：502/504 的 retry 歧義</h2>
<p>RFC 9110 對 502 與 504 的定義只描述 gateway 自己的觀察：收到無效回應（502）、沒收到及時回應（504）—— 規範沒有任何欄位區分「上游根本沒收到請求」與「上游執行了、只是回應沒回來」（見 <a href="/blog/backend/11-api-design/cases/status-502-504-gateway-ambiguity/" data-link-title="11.C67 RFC 9110 502/504：gateway 只回報自己的觀察、不回報上游的執行狀態" data-link-desc="502/504 定義只說 gateway 沒收到有效或及時回應、沒有欄位區分「請求沒送到」與「執行了但回應丟了」— retry 安全性相反的兩種情況拿到同一個 code">11.C67</a>）。</p>
<p>這個缺口的工程後果（此為從定義出發的推導、非 spec 明文）：connect timeout（請求沒送到、重送安全）跟 read timeout（請求已執行、重送非冪等操作會重複執行）在 consumer 端拿到同一個 504、而兩者的 retry 安全性相反。status 在這裡不是裝不下多個結果、是裝不下「連 gateway 自己都不知道」的不確定性 —— 補強手段全在 status 之外：操作設計成冪等、或帶 <a href="/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">idempotency key</a> 讓重送安全（<a href="/blog/backend/11-api-design/api-idempotency-design/" data-link-title="11.8 API 層冪等設計" data-link-desc="idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況">11.8</a> 主寫）、上游做去重。consumer 收到 502/504 的判讀規則因此很短：不確定上游做了沒、就當作做了 —— 除非操作冪等或帶了 key、否則重送前先查。</p>
<h2 id="三種邊界的共同判準">三種邊界的共同判準</h2>
<p>三種邊界指向同一條設計原則：status 是給整條鏈看的最低契約、它裝不下的資訊要嘛收窄語意讓它恆為真（原子批次）、要嘛在 status 之外建立顯式的補充通道（operation resource、opt-in 的部分失敗結構、idempotency key）—— 而不是把資訊藏進只有一方讀得懂的地方、讓另一端與中介層在不知情下做錯決策。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>雙向契約的框架：<a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 Status 與錯誤的雙向契約</a></li>
<li>拿到模糊 status 之後的重試合判：<a href="/blog/backend/11-api-design/consumer-retry-decision/" data-link-title="接收方的重試決策：從單一請求到 retry 風暴" data-link-desc="收到錯誤之後重不重試：從單請求的 status 加冪等合判、集體的去同步責任、到 retry 預算與斷路閘門">接收方的重試決策</a></li>
<li>錯誤細節住在哪個容器、以及那個選擇讓誰讀不到錯誤：<a href="/blog/backend/11-api-design/error-format-debate/" data-link-title="錯誤格式之爭：status 的真實性決定誰讀得到錯誤、容器決定誰讀得到細節" data-link-desc="選錯誤格式時各派的分歧與代價：錯誤內容跟 transport status 的關係、它讓哪些角色讀得到錯誤、以及演化條款與命名空間由誰提供">錯誤格式之爭</a>（本文問的是 status 這一格裝不裝得下事實，該文問的是細節住哪裡）</li>
<li>重送安全的機制：<a href="/blog/backend/11-api-design/api-idempotency-design/" data-link-title="11.8 API 層冪等設計" data-link-desc="idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況">11.8 API 層冪等設計</a></li>
<li>長時操作的查詢入口：<a href="/blog/backend/11-api-design/collection-interface-design/" data-link-title="11.7 集合介面設計" data-link-desc="分頁方案的承諾差異、批次操作的部分失敗語意、超過請求逾時的長時操作怎麼回 — 集合與長時操作的介面判準">11.7 集合介面設計</a></li>
<li>案例原文：<a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a></li>
</ul>
]]></content:encoded></item><item><title>接收方的重試決策：從單一請求到 retry 風暴</title><link>https://tarrragon.github.io/blog/backend/11-api-design/consumer-retry-decision/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/consumer-retry-decision/</guid><description>&lt;p>重試是 consumer 收到錯誤後最頻繁的決策、也是雙向契約裡責任交纏最深的一條：retry 對 consumer 是自保（提高單一請求的表觀成功率）、對 provider 是額外負載 —— 失敗稀少時這筆交換成立、失敗源於過載時、同一個行為變成持續攻擊。這個決策因此分三層、每層的判準不同：單一請求層問「這個錯誤重送安全嗎」、集體層問「大家一起重送會發生什麼」、架構層問「重試這件事該由誰做、配多少預算」。三層的上層框架 —— 兩端期望與成本外部化 —— 在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約&lt;/a>。&lt;/p>
&lt;h2 id="單一請求層statusmethod冪等的合判">單一請求層：status、method、冪等的合判&lt;/h2>
&lt;p>「該不該重試」是三個輸入的合判、status 只是其中之一。status 給第一刀：4xx 終態停止重試、5xx 與 429 可重試（分類判準見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4&lt;/a>、429 的等待語意見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9&lt;/a>）。method 與冪等給第二刀：可重試的 status 不等於重送安全 —— GET 與 PUT 有冪等承諾、直接重送；POST 沒有、重送可能重複執行、要嘛操作帶 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">idempotency key&lt;/a>（&lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/api-idempotency-design/" data-link-title="11.8 API 層冪等設計" data-link-desc="idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況">11.8&lt;/a>）、要嘛先查再送。第三刀是不確定性：502/504 分不出上游「沒收到」還是「執行了」、兩種情況 retry 安全性相反（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/status-502-504-gateway-ambiguity/" data-link-title="11.C67 RFC 9110 502/504：gateway 只回報自己的觀察、不回報上游的執行狀態" data-link-desc="502/504 定義只說 gateway 沒收到有效或及時回應、沒有欄位區分「請求沒送到」與「執行了但回應丟了」— retry 安全性相反的兩種情況拿到同一個 code">11.C67&lt;/a>）—— 判讀規則是「不確定就當作做了」、非冪等又沒帶 key 的操作、重送前先查狀態。&lt;/p>
&lt;p>這一層的 provider 義務對應存在：用不同的 code 把可重試與不可重試分開（Google SRE Book 的明文建議、見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-sre-book-cascading-failures/" data-link-title="11.C69 Google SRE Book：retry 放大與跨層疊乘、per-request 上限與 retry budget" data-link-desc="retry 放大讓有效工作遞減；三層各 retry 3 次在底層變 64 次；建議 per-request 上限 &amp;#43; server-wide retry budget、provider 要用不同 code 分開可重試與不可重試">11.C69&lt;/a>）、Retry-After 說到做到（見 11.9）。provider 標示含糊、consumer 的合判就從第一刀開始就錯。&lt;/p>
&lt;h2 id="集體層各自理性集體災難">集體層：各自理性、集體災難&lt;/h2>
&lt;p>單一 consumer 的合理重試、乘上所有 consumer 就變質。兩個機制疊加：第一是 retry 放大 —— 100 QPS 的失敗、每個都重試一次就變 200 QPS、再放大成 300 QPS、「fewer and fewer requests are able to succeed on their first attempt」（SRE Book 原文、見 C69）；第二是同步波 —— 所有 client 用相同的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">exponential backoff&lt;/a>、退避後會在同一時刻一起回來、每一波都是對 provider 的同步衝擊。&lt;/p>
&lt;p>去同步是 consumer 的集體契約責任。Marc Brooker 的實測給了量化根據（模擬情境是 OCC（樂觀並發控制）寫入競爭、非 HTTP retry、結論可遷移）：N 個 client 競爭時總工作量隨 N² 成長、無 jitter 的純 exponential backoff 是「the clear loser」、100 個競爭 client 下加 jitter 讓呼叫量減半以上、Full Jitter（&lt;code>sleep = random(0, min(cap, base * 2^attempt))&lt;/code>）總工作量最少（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-brooker-backoff-jitter/" data-link-title="11.C68 Exponential Backoff And Jitter：無 jitter 的退避是明確輸家" data-link-desc="N 個 client 同時競爭時總工作量隨 N² 成長；三種 jitter 公式實測、Full Jitter 總工作量最少 — consumer 之間的隱性同步要主動打散">11.C68&lt;/a>）。判準很直接：backoff 解決「等多久」、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter&lt;/a> 解決「別一起回來」—— 兩者都是 consumer 對 provider 的義務、不是可選優化。&lt;/p></description><content:encoded><![CDATA[<p>重試是 consumer 收到錯誤後最頻繁的決策、也是雙向契約裡責任交纏最深的一條：retry 對 consumer 是自保（提高單一請求的表觀成功率）、對 provider 是額外負載 —— 失敗稀少時這筆交換成立、失敗源於過載時、同一個行為變成持續攻擊。這個決策因此分三層、每層的判準不同：單一請求層問「這個錯誤重送安全嗎」、集體層問「大家一起重送會發生什麼」、架構層問「重試這件事該由誰做、配多少預算」。三層的上層框架 —— 兩端期望與成本外部化 —— 在 <a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約</a>。</p>
<h2 id="單一請求層statusmethod冪等的合判">單一請求層：status、method、冪等的合判</h2>
<p>「該不該重試」是三個輸入的合判、status 只是其中之一。status 給第一刀：4xx 終態停止重試、5xx 與 429 可重試（分類判準見 <a href="/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4</a>、429 的等待語意見 <a href="/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9</a>）。method 與冪等給第二刀：可重試的 status 不等於重送安全 —— GET 與 PUT 有冪等承諾、直接重送；POST 沒有、重送可能重複執行、要嘛操作帶 <a href="/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">idempotency key</a>（<a href="/blog/backend/11-api-design/api-idempotency-design/" data-link-title="11.8 API 層冪等設計" data-link-desc="idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況">11.8</a>）、要嘛先查再送。第三刀是不確定性：502/504 分不出上游「沒收到」還是「執行了」、兩種情況 retry 安全性相反（見 <a href="/blog/backend/11-api-design/cases/status-502-504-gateway-ambiguity/" data-link-title="11.C67 RFC 9110 502/504：gateway 只回報自己的觀察、不回報上游的執行狀態" data-link-desc="502/504 定義只說 gateway 沒收到有效或及時回應、沒有欄位區分「請求沒送到」與「執行了但回應丟了」— retry 安全性相反的兩種情況拿到同一個 code">11.C67</a>）—— 判讀規則是「不確定就當作做了」、非冪等又沒帶 key 的操作、重送前先查狀態。</p>
<p>這一層的 provider 義務對應存在：用不同的 code 把可重試與不可重試分開（Google SRE Book 的明文建議、見 <a href="/blog/backend/11-api-design/cases/retry-sre-book-cascading-failures/" data-link-title="11.C69 Google SRE Book：retry 放大與跨層疊乘、per-request 上限與 retry budget" data-link-desc="retry 放大讓有效工作遞減；三層各 retry 3 次在底層變 64 次；建議 per-request 上限 &#43; server-wide retry budget、provider 要用不同 code 分開可重試與不可重試">11.C69</a>）、Retry-After 說到做到（見 11.9）。provider 標示含糊、consumer 的合判就從第一刀開始就錯。</p>
<h2 id="集體層各自理性集體災難">集體層：各自理性、集體災難</h2>
<p>單一 consumer 的合理重試、乘上所有 consumer 就變質。兩個機制疊加：第一是 retry 放大 —— 100 QPS 的失敗、每個都重試一次就變 200 QPS、再放大成 300 QPS、「fewer and fewer requests are able to succeed on their first attempt」（SRE Book 原文、見 C69）；第二是同步波 —— 所有 client 用相同的 <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">exponential backoff</a>、退避後會在同一時刻一起回來、每一波都是對 provider 的同步衝擊。</p>
<p>去同步是 consumer 的集體契約責任。Marc Brooker 的實測給了量化根據（模擬情境是 OCC（樂觀並發控制）寫入競爭、非 HTTP retry、結論可遷移）：N 個 client 競爭時總工作量隨 N² 成長、無 jitter 的純 exponential backoff 是「the clear loser」、100 個競爭 client 下加 jitter 讓呼叫量減半以上、Full Jitter（<code>sleep = random(0, min(cap, base * 2^attempt))</code>）總工作量最少（見 <a href="/blog/backend/11-api-design/cases/retry-brooker-backoff-jitter/" data-link-title="11.C68 Exponential Backoff And Jitter：無 jitter 的退避是明確輸家" data-link-desc="N 個 client 同時競爭時總工作量隨 N² 成長；三種 jitter 公式實測、Full Jitter 總工作量最少 — consumer 之間的隱性同步要主動打散">11.C68</a>）。判準很直接：backoff 解決「等多久」、<a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">jitter</a> 解決「別一起回來」—— 兩者都是 consumer 對 provider 的義務、不是可選優化。</p>
<p>放大失控的終點是 <a href="/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">retry 風暴</a>。AWS 官方定義：「the network can quickly become saturated with new and retried requests… This can result in a retry storm」（見 <a href="/blog/backend/11-api-design/cases/retry-aws-guidance-budget/" data-link-title="11.C72 AWS retry 指南：retry storm 的官方定義與分層限制" data-link-desc="Well-Architected 逐字定義 retry storm、建議低層服務 retry 上限 0-1 次、把 retry 委派給上層；Builders&#39; Library 的 token bucket 路線">11.C72</a>）。代表案例是 DynamoDB 2015 事故：metadata 服務過載後、逾時的 storage server 自我下線再重試、錯誤率推到 55%、且風暴成形後系統不自癒 —— 復原靠人工暫停請求讓 provider 喘息（見 <a href="/blog/backend/11-api-design/cases/retry-dynamodb-2015-storm/" data-link-title="11.C70 AWS DynamoDB 2015 事故：內部元件的 retry 自保把錯誤率推到 55%（反例）" data-link-desc="反例：metadata 服務過載後 storage server 逾時自我下線再重試、風暴成形後系統不自癒、要人工暫停請求才能喘息；事後修正同時動兩端">11.C70</a>）。這個案例的 consumer 是 AWS 自己的內部元件：retry 變攻擊是任何 caller 的結構性行為、不是外部用戶不守規矩。</p>
<h2 id="架構層retry-放哪一層配多少預算">架構層：retry 放哪一層、配多少預算</h2>
<p>多層服務各自 retry 會疊乘：三層各重試 3 次、最底層收到 64 次嘗試（SRE Book、見 C69）。盤點層數時要把 infra 層的隱形 retry 算進去 —— service mesh 的預設 retry policy、SDK 內建的重試（AWS SDK 預設就會重試）都是最常被漏算的一層。retry 因此是要在架構層分配的預算、不是每層的預設行為 —— AWS 的分層建議：低層服務 retry 上限 0 到 1 次、把重試委派給上層（見 C72）、收斂的方向通常是最接近業務語意的外層（它才知道這個操作值不值得再試）；SRE Book 的程序級預算：per-request 上限之外、再設 server-wide <a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget</a>（例如每程序每分鐘 60 次）—— 預算耗盡就不再重試、把「retry 是否過量」從逐請求的局部判斷變成程序級的資源帳。這一層還有一個更上游的預算是剩餘時間：<a href="/blog/backend/knowledge-cards/deadline/" data-link-title="Deadline" data-link-desc="說明整體操作的截止時間如何沿著服務邊界傳遞">deadline</a> 傳播下、剩的時間不夠跑完一次重試、retry 是純浪費 —— 重不重試之前先看還剩多久；hedged request、adaptive retry 這類進階形態同屬這層的預算分配問題、本文不展開。</p>
<p><a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker</a> 是這一層的閘門、也是 retry 敘事的另一面。Slack 2021 事故給了平衡的實例：網路層恢復後、「plus retries and circuit breaking — got us back to serving」—— retry 加斷路器正是把系統拉回服務狀態的工具（見 <a href="/blog/backend/11-api-design/cases/retry-slack-2021-recovery/" data-link-title="11.C71 Slack 2021-01-04 事故：復原期 retry 加 circuit breaking 是藥方" data-link-desc="retry 的反向平衡：網路恢復後正是 retry 與 circuit breaking 讓系統爬回服務狀態 — retry 是否有害取決於 provider 處於過載中還是恢復中">11.C71</a>）。判讀：consumer 的 retry 是否有害、取決於 provider 當下在「過載中」還是「恢復中」、而 consumer 無法直接觀測這件事 —— circuit breaker 用本地錯誤率推斷代替猜測：斷路時擋住無效重試保護對方、半開時少量探測驗證恢復、恢復後 retry 轉為復原工具。這兩層的 provider 鏡像義務是給出可推斷的訊號：過載時回明確的 429 加等待時間（而非含糊的 5xx、見 11.9）、提供 health endpoint 或 status page 讓斷路器的推斷有依據 —— consumer 的預算與閘門、要有 provider 的訊號才調得準。</p>
<h2 id="責任分配表">責任分配表</h2>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>consumer 的義務</th>
          <th>provider 的對應義務</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一請求</td>
          <td>status 加冪等合判、不確定就先查</td>
          <td>code 分開可重試與不可重試、Retry-After 說到做到</td>
      </tr>
      <tr>
          <td>集體</td>
          <td>backoff 加 jitter、per-request 上限</td>
          <td>過載時給明確的退讓訊號（429 加等待時間）</td>
      </tr>
      <tr>
          <td>架構</td>
          <td>retry 收斂到一層、配 retry budget、circuit breaker</td>
          <td>提供健康訊號讓斷路器有依據</td>
      </tr>
  </tbody>
</table>
<p>表是索引、每格的成立條件在上文各段。三層合起來的判讀是：重試的每一層都是雙向的 —— consumer 單方面努力擋不住 provider 標示含糊、provider 單方面標清楚也擋不住 consumer 無預算地重送。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>雙向契約的框架：<a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 Status 與錯誤的雙向契約</a></li>
<li>重送安全的機制設計：<a href="/blog/backend/11-api-design/api-idempotency-design/" data-link-title="11.8 API 層冪等設計" data-link-desc="idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況">11.8 API 層冪等設計</a></li>
<li>429 與退讓訊號：<a href="/blog/backend/11-api-design/external-traffic-semantics/" data-link-title="11.9 對外流量語意" data-link-desc="rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計">11.9 對外流量語意</a></li>
<li>服務端的過載防護：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 可靠性</a>、<a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker 知識卡</a>、<a href="/blog/backend/knowledge-cards/retry-budget/" data-link-title="Retry Budget" data-link-desc="說明重試次數如何受整體容量與錯誤預算限制">retry budget 知識卡</a></li>
<li>帶了 key 之後重送拿到什麼，各家條款不同：<a href="/blog/backend/11-api-design/idempotency-key-standardization-debate/" data-link-title="Idempotency key 標準化之爭：標準統一得了揭露的形狀、統一不了業務綁定的值" data-link-desc="整合或自建冪等機制時各家條款的實質差異：replay 回首次快照還是最新狀態、保存期是否明文、同 key 並發怎麼處理">Idempotency key 標準化之爭</a>（replay 回首次快照還是最新狀態、認不認得出是重放、保存期多長，直接決定重試窗口能設多寬）</li>
<li>案例原文：<a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a></li>
</ul>
]]></content:encoded></item><item><title>錯誤傳播與信任邊界：中間服務的雙重身分</title><link>https://tarrragon.github.io/blog/backend/11-api-design/error-propagation-trust-boundary/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/error-propagation-trust-boundary/</guid><description>&lt;p>服務鏈上的錯誤處理有一個單服務視角看不到的結構：A 呼叫 B、B 呼叫 C、C 掛了 —— B 對 C 是 consumer、對 A 是 provider、&lt;strong>同一個服務在同一次失敗裡承擔兩份契約責任&lt;/strong>。C 的錯誤怎麼變成 B 回給 A 的錯誤、是每個中間服務都要回答的設計題；答錯的形態是把上游錯誤原樣倒給下游（洩漏 + 語意錯位）、或把一切包成不可判讀的 500（資訊消滅）。三個判準依序展開：錯誤的哪一層能傳多遠、收到的錯誤能信多少、對外暴露多少。本文掛在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約&lt;/a>；跨服務轉譯沒有單一規範明文、本文的責任推導從各規範的單跳條款出發、逐處標明。&lt;/p>
&lt;h2 id="錯誤契約有保證層與選配層傳播能力不同">錯誤契約有保證層與選配層、傳播能力不同&lt;/h2>
&lt;p>gRPC 的兩層錯誤模型把這件事講得最清楚：標準模型（status code 加 optional message）是所有語言 client 都拿得到的保證層；richer error model（&lt;code>google.rpc.Status&lt;/code> 帶結構化 detail）是選配層 —— 官方自列它的三個傳播風險：語言支援不全、payload 撞 header 上限、以及最關鍵的一條：detail 走 trailing metadata（trailer 在回應串流結束後才送出、多數中介層只讀開頭的 header）、&lt;strong>proxy 與 logger 看不到&lt;/strong>（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-grpc-two-layer-model/" data-link-title="11.C73 gRPC 兩層錯誤模型：status code 是保證層、richer detail 是選配層" data-link-desc="標準模型全語言保證、richer error model 走 trailing metadata — proxy 與 logger 看不到、中間節點對錯誤細節是盲的">11.C73&lt;/a>）。&lt;/p>
&lt;p>工程含義：錯誤資訊的可見範圍分層 —— status code 全鏈可見（每一跳、每個中介層都讀得到）、結構化 detail 只有端點可見（中間節點對它是盲的）。設計錯誤契約時要按這個分層放資訊：中介層要用的（可不可重試、要不要熔斷）必須放在全鏈可見層、放進 detail 就等於對整條鏈的基礎設施隱形。HTTP 系的對應是 status 全鏈可見、body 端到端 —— 同構的分層、同樣的設計判準。&lt;/p>
&lt;h2 id="收到的錯誤能信多少產生者歧義">收到的錯誤能信多少：產生者歧義&lt;/h2>
&lt;p>中間服務轉譯錯誤前、先要判斷收到的錯誤是誰說的。gRPC 的 status codes 文件給了一手根據：17 個 code 裡只有 7 個（INVALID_ARGUMENT、NOT_FOUND、ALREADY_EXISTS、FAILED_PRECONDITION、ABORTED、OUT_OF_RANGE、DATA_LOSS）保證來自 server 應用邏輯、library 從不自產；UNAVAILABLE、DEADLINE_EXCEEDED、INTERNAL 則可能是中間 channel 或 library 產生 —— consumer 單看 code 分不出來（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-grpc-code-producer-ambiguity/" data-link-title="11.C74 gRPC status code 產生者歧義：收到的 code 不一定來自 server 應用層" data-link-desc="17 個 code 裡只有 7 個保證來自 user code；UNAVAILABLE / INTERNAL 可能是 library 或 channel 自產 — consumer 對錯誤來源的信任判讀有一手依據">11.C74&lt;/a>）。&lt;/p>
&lt;p>這對轉譯的含義（推導、標明）：錯誤的可信度不均質。收到「保證來自應用」的 code、語意可以直接轉譯（NOT_FOUND 就是資源不在）；收到產生者不明的 code、轉譯往「暫時性」收斂 —— 回自己的 UNAVAILABLE 或 502 並標可重試、不映射成上游的業務錯誤；要保留產生者線索、放進結構化 detail 而非原樣透傳。它可能只是網路層的一次抖動、不代表上游的業務判斷。中間服務原樣透傳 UNKNOWN 或 INTERNAL、等於把「產生者是誰」的資訊消滅掉、下游拿到的錯誤比你拿到的更不可判讀 —— 資訊只會在鏈上遞減、不會自己恢復。&lt;/p>
&lt;h2 id="轉譯的責任對上游是誰的錯對下游要換語意">轉譯的責任：對上游是誰的錯、對下游要換語意&lt;/h2>
&lt;p>中間服務回給下游的錯誤、語意主詞要換。C 掛了、對 B 是「我的依賴壞了」；但 B 回給 A 的錯誤要回答的是 A 的問題 ——「我的請求怎麼了」。判準（推導）：上游的 5xx 到你這裡、對下游是你的 502/503（你的服務暫時無法完成、可重試）、不是把 C 的 500 連 body 一起倒出去；上游的 4xx 要分辨 —— 是你組請求組錯了（你的 bug、對下游是你的 500）、還是下游的輸入真的非法（對下游還是 4xx、但錯誤內容要換成下游看得懂的欄位名與語彙）。原樣透傳最誘人的時刻是趕工期、它把三種成本外部化：下游拿到不知所云的錯誤（解析成本）、上游的內部細節穿透兩層信任邊界（安全成本）、除錯時分不清錯誤源頭（定位成本）。把轉譯責任制度化的常見做法是收斂到專職層 —— BFF 或 gateway 統一做錯誤轉譯、個別服務只處理自己直接依賴的錯誤（gateway 的執行面屬 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台&lt;/a>）。&lt;/p></description><content:encoded><![CDATA[<p>服務鏈上的錯誤處理有一個單服務視角看不到的結構：A 呼叫 B、B 呼叫 C、C 掛了 —— B 對 C 是 consumer、對 A 是 provider、<strong>同一個服務在同一次失敗裡承擔兩份契約責任</strong>。C 的錯誤怎麼變成 B 回給 A 的錯誤、是每個中間服務都要回答的設計題；答錯的形態是把上游錯誤原樣倒給下游（洩漏 + 語意錯位）、或把一切包成不可判讀的 500（資訊消滅）。三個判準依序展開：錯誤的哪一層能傳多遠、收到的錯誤能信多少、對外暴露多少。本文掛在 <a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約</a>；跨服務轉譯沒有單一規範明文、本文的責任推導從各規範的單跳條款出發、逐處標明。</p>
<h2 id="錯誤契約有保證層與選配層傳播能力不同">錯誤契約有保證層與選配層、傳播能力不同</h2>
<p>gRPC 的兩層錯誤模型把這件事講得最清楚：標準模型（status code 加 optional message）是所有語言 client 都拿得到的保證層；richer error model（<code>google.rpc.Status</code> 帶結構化 detail）是選配層 —— 官方自列它的三個傳播風險：語言支援不全、payload 撞 header 上限、以及最關鍵的一條：detail 走 trailing metadata（trailer 在回應串流結束後才送出、多數中介層只讀開頭的 header）、<strong>proxy 與 logger 看不到</strong>（見 <a href="/blog/backend/11-api-design/cases/errorchain-grpc-two-layer-model/" data-link-title="11.C73 gRPC 兩層錯誤模型：status code 是保證層、richer detail 是選配層" data-link-desc="標準模型全語言保證、richer error model 走 trailing metadata — proxy 與 logger 看不到、中間節點對錯誤細節是盲的">11.C73</a>）。</p>
<p>工程含義：錯誤資訊的可見範圍分層 —— status code 全鏈可見（每一跳、每個中介層都讀得到）、結構化 detail 只有端點可見（中間節點對它是盲的）。設計錯誤契約時要按這個分層放資訊：中介層要用的（可不可重試、要不要熔斷）必須放在全鏈可見層、放進 detail 就等於對整條鏈的基礎設施隱形。HTTP 系的對應是 status 全鏈可見、body 端到端 —— 同構的分層、同樣的設計判準。</p>
<h2 id="收到的錯誤能信多少產生者歧義">收到的錯誤能信多少：產生者歧義</h2>
<p>中間服務轉譯錯誤前、先要判斷收到的錯誤是誰說的。gRPC 的 status codes 文件給了一手根據：17 個 code 裡只有 7 個（INVALID_ARGUMENT、NOT_FOUND、ALREADY_EXISTS、FAILED_PRECONDITION、ABORTED、OUT_OF_RANGE、DATA_LOSS）保證來自 server 應用邏輯、library 從不自產；UNAVAILABLE、DEADLINE_EXCEEDED、INTERNAL 則可能是中間 channel 或 library 產生 —— consumer 單看 code 分不出來（見 <a href="/blog/backend/11-api-design/cases/errorchain-grpc-code-producer-ambiguity/" data-link-title="11.C74 gRPC status code 產生者歧義：收到的 code 不一定來自 server 應用層" data-link-desc="17 個 code 裡只有 7 個保證來自 user code；UNAVAILABLE / INTERNAL 可能是 library 或 channel 自產 — consumer 對錯誤來源的信任判讀有一手依據">11.C74</a>）。</p>
<p>這對轉譯的含義（推導、標明）：錯誤的可信度不均質。收到「保證來自應用」的 code、語意可以直接轉譯（NOT_FOUND 就是資源不在）；收到產生者不明的 code、轉譯往「暫時性」收斂 —— 回自己的 UNAVAILABLE 或 502 並標可重試、不映射成上游的業務錯誤；要保留產生者線索、放進結構化 detail 而非原樣透傳。它可能只是網路層的一次抖動、不代表上游的業務判斷。中間服務原樣透傳 UNKNOWN 或 INTERNAL、等於把「產生者是誰」的資訊消滅掉、下游拿到的錯誤比你拿到的更不可判讀 —— 資訊只會在鏈上遞減、不會自己恢復。</p>
<h2 id="轉譯的責任對上游是誰的錯對下游要換語意">轉譯的責任：對上游是誰的錯、對下游要換語意</h2>
<p>中間服務回給下游的錯誤、語意主詞要換。C 掛了、對 B 是「我的依賴壞了」；但 B 回給 A 的錯誤要回答的是 A 的問題 ——「我的請求怎麼了」。判準（推導）：上游的 5xx 到你這裡、對下游是你的 502/503（你的服務暫時無法完成、可重試）、不是把 C 的 500 連 body 一起倒出去；上游的 4xx 要分辨 —— 是你組請求組錯了（你的 bug、對下游是你的 500）、還是下游的輸入真的非法（對下游還是 4xx、但錯誤內容要換成下游看得懂的欄位名與語彙）。原樣透傳最誘人的時刻是趕工期、它把三種成本外部化：下游拿到不知所云的錯誤（解析成本）、上游的內部細節穿透兩層信任邊界（安全成本）、除錯時分不清錯誤源頭（定位成本）。把轉譯責任制度化的常見做法是收斂到專職層 —— BFF 或 gateway 統一做錯誤轉譯、個別服務只處理自己直接依賴的錯誤（gateway 的執行面屬 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台</a>）。</p>
<h2 id="暴露多少機器可讀與偵察面的對撞">暴露多少：機器可讀與偵察面的對撞</h2>
<p>錯誤內容該多詳細、有兩股方向相反的一手規範、對撞出中間路線。安全端要求少暴露：OWASP 的規則是非預期錯誤回 generic response、細節只留 server side log —— stack trace 洩漏框架版本、SQL error 幫攻擊者找 injection point、錯誤訊息是攻擊者的偵察面（見 <a href="/blog/backend/11-api-design/cases/errorchain-owasp-error-handling/" data-link-title="11.C77 OWASP error handling：錯誤訊息是攻擊者的偵察面" data-link-desc="非預期錯誤回 generic response、細節只留 server side log — provider 少暴露的安全端論證、跟 AIP-193 的機器可讀路線形成張力">11.C77</a>、攻擊面思路同 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 安全</a>）。可用性端要求夠機器可讀：全 generic 的錯誤讓 consumer 完全無法自助、每個錯誤都變成 support ticket。</p>
<p>Google 的 API 設計規範 AIP-193 用三層受眾設計走出中間路線（見 <a href="/blog/backend/11-api-design/cases/errorchain-aip193-error-content/" data-link-title="11.C75 AIP-193 錯誤內容規範：三層受眾與「不假設使用者懂內部實作」" data-link-desc="機器可讀的 (reason, domain) 契約、developer-facing message、LocalizedMessage 三層分工；message 穩定性規則反向揭露 Hyrum&#39;s Law">11.C75</a>）：機器層給 <code>ErrorInfo</code> 的 (reason, domain)、可程式化分支的穩定識別符 —— 但「error messages must not assume that the user will know anything about its underlying implementation」、識別符用 consumer 的語彙命名、不洩內部結構；開發者層給 message、人類可讀的 debug 訊息、可以變動、定位上不可當 API；使用者層給 LocalizedMessage —— 呈現給終端使用者的文案。三層各給對的受眾、既不是 generic 到無法自助、也不是把內部狀態倒出來。附帶一條反向條款：舊 API 沒給機器可讀欄位的、AIP 要求 message 內容必須穩定 —— consumer 已經在 parse 它、改字就是 breaking change；不給機器層、人類層就會被迫變成契約。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>雙向契約的框架：<a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 Status 與錯誤的雙向契約</a></li>
<li>錯誤格式的 producer 側設計：<a href="/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4 錯誤模型設計</a></li>
<li>回報時的定位鉤子：<a href="/blog/backend/11-api-design/error-feedback-loop/" data-link-title="錯誤回報的回饋迴路：request-id、trace 與呈現回報分工" data-link-desc="consumer 收到錯誤之後怎麼跟 provider 溝通：error 要帶什麼定位鉤子、同一個錯誤怎麼分別投影給使用者與回報、持續錯誤什麼時候該升級">錯誤回報的回饋迴路</a></li>
<li>錯誤訊息的攻擊面：<a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 安全與資料保護</a></li>
<li>dependency call 的診斷欄位（upstream、response class、circuit state）：<a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19 Debuggability by Design</a></li>
<li>案例原文：<a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a></li>
</ul>
]]></content:encoded></item><item><title>錯誤回報的回饋迴路：request-id、trace 與呈現回報分工</title><link>https://tarrragon.github.io/blog/backend/11-api-design/error-feedback-loop/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/error-feedback-loop/</guid><description>&lt;p>錯誤處理的最後一段是溝通：consumer 收到錯誤、自己處理不了、要回頭問 provider ——「幾點幾分、我呼叫你的什麼 API、拿到什麼錯」。這段對話的品質完全由契約決定：error 帶了可定位的識別符、一句「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request-id&lt;/a> 是 X」就能讓 provider 直接調出該次請求的全鏈紀錄；沒帶、consumer 只能用時間與操作描述、provider 在 log 海裡撈 —— debug 成本被推給兩端的人力。不給定位鉤子的成因要分兩種：沒人要求過是優先序問題、提了常能補上；要求了也不修、才是地位不對等的形態 —— 平台省一個欄位、每個 consumer 每次排錯多付幾小時。這種不對等的整體判讀框架在 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約&lt;/a>。&lt;/p>
&lt;h2 id="定位鉤子request-id-與-trace-的契約">定位鉤子：request-id 與 trace 的契約&lt;/h2>
&lt;p>回饋迴路的最小契約是每個錯誤回應帶一個唯一識別符。成熟先例都這麼做：Stripe 在錯誤物件附 &lt;code>request_log_url&lt;/code>（直達該次請求的 dashboard 紀錄、見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/error-stripe-error-object/" data-link-title="11.C36 Stripe 錯誤物件：type / code / param 三層分離" data-link-desc="路由層、分支層、UI 層做成正交欄位；冪等衝突列 first-class 錯誤型別；標準前自成一格的對照組">11.C36&lt;/a>）、GitHub 的 webhook 每次投遞帶 &lt;code>X-GitHub-Delivery&lt;/code> GUID（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/webhook-github-no-retry/" data-link-title="11.C61 GitHub webhooks：不自動重試的反向承諾" data-link-desc="at-least-once 不是所有 vendor 都給：GitHub 明文只試一次、失敗靠 consumer 自建排程補投；逼你讀 vendor 明文而非假設">11.C61&lt;/a>）、RFC 9457 的 &lt;code>instance&lt;/code> 欄位（識別該次 problem occurrence 的 URI）可承擔類似角色（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/error-rfc9457-problem-details/" data-link-title="11.C35 RFC 9457：problem&amp;#43;json 標準化錯誤格式" data-link-desc="type 用 URI 外部化錯誤命名空間、client 必須忽略未知欄位的演化條款、IANA registry 補 7807 碎片化">11.C35&lt;/a>）。契約的兩半：provider 承諾這個 id 在自己的 log 與 trace 系統裡查得到、且保留得比 consumer 的排錯周期長（多久、寫進文件）；consumer 的義務是把它記進自己的錯誤 log —— 收到錯誤時丟棄 id、回報時就退回「大概幾點」。&lt;/p>
&lt;p>id 能定位「單跳」、trace 才能定位「全鏈」。一個請求跨五個服務、provider 的第一層 log 只能看到自己這一跳 —— 要從 consumer 回報的識別符追到深處哪個服務出錯、靠的是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 的傳播義務：W3C Trace Context 規定收到 &lt;code>traceparent&lt;/code> header 的服務 MUST 往 outgoing request 傳、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace-id&lt;/a> 全鏈不變（見 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/trace-w3c-trace-context/" data-link-title="11.C76 W3C Trace Context：traceparent 的傳播義務與 security boundary 重開機制" data-link-desc="跨 vendor trace 關聯的標準鉤子：每一跳 MUST 傳播、security boundary 可 restart trace、無效 id MUST ignore — 信任邊界寫進規範">11.C76&lt;/a>）。這條 MUST 是回饋迴路的規範地基 —— 任何一跳斷掉傳播、consumer 手上的 id 就只能追到斷點。同一份規範也內建信任邊界：security boundary 可以 restart trace（provider 不必信外部給的 trace-id）、無效 id MUST ignore —— 對外的 API 通常回自己生成的 request-id 給 consumer、內部用 trace-id 關聯、兩者在 gateway 對接（此對接模式為常見實務、非規範明文）。trace 系統本身的建置屬 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性&lt;/a>（主流實作生態是 OpenTelemetry）、本文只收契約面：id 要不要給、給了承諾什麼。&lt;/p></description><content:encoded><![CDATA[<p>錯誤處理的最後一段是溝通：consumer 收到錯誤、自己處理不了、要回頭問 provider ——「幾點幾分、我呼叫你的什麼 API、拿到什麼錯」。這段對話的品質完全由契約決定：error 帶了可定位的識別符、一句「<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request-id</a> 是 X」就能讓 provider 直接調出該次請求的全鏈紀錄；沒帶、consumer 只能用時間與操作描述、provider 在 log 海裡撈 —— debug 成本被推給兩端的人力。不給定位鉤子的成因要分兩種：沒人要求過是優先序問題、提了常能補上；要求了也不修、才是地位不對等的形態 —— 平台省一個欄位、每個 consumer 每次排錯多付幾小時。這種不對等的整體判讀框架在 <a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 雙向契約</a>。</p>
<h2 id="定位鉤子request-id-與-trace-的契約">定位鉤子：request-id 與 trace 的契約</h2>
<p>回饋迴路的最小契約是每個錯誤回應帶一個唯一識別符。成熟先例都這麼做：Stripe 在錯誤物件附 <code>request_log_url</code>（直達該次請求的 dashboard 紀錄、見 <a href="/blog/backend/11-api-design/cases/error-stripe-error-object/" data-link-title="11.C36 Stripe 錯誤物件：type / code / param 三層分離" data-link-desc="路由層、分支層、UI 層做成正交欄位；冪等衝突列 first-class 錯誤型別；標準前自成一格的對照組">11.C36</a>）、GitHub 的 webhook 每次投遞帶 <code>X-GitHub-Delivery</code> GUID（見 <a href="/blog/backend/11-api-design/cases/webhook-github-no-retry/" data-link-title="11.C61 GitHub webhooks：不自動重試的反向承諾" data-link-desc="at-least-once 不是所有 vendor 都給：GitHub 明文只試一次、失敗靠 consumer 自建排程補投；逼你讀 vendor 明文而非假設">11.C61</a>）、RFC 9457 的 <code>instance</code> 欄位（識別該次 problem occurrence 的 URI）可承擔類似角色（見 <a href="/blog/backend/11-api-design/cases/error-rfc9457-problem-details/" data-link-title="11.C35 RFC 9457：problem&#43;json 標準化錯誤格式" data-link-desc="type 用 URI 外部化錯誤命名空間、client 必須忽略未知欄位的演化條款、IANA registry 補 7807 碎片化">11.C35</a>）。契約的兩半：provider 承諾這個 id 在自己的 log 與 trace 系統裡查得到、且保留得比 consumer 的排錯周期長（多久、寫進文件）；consumer 的義務是把它記進自己的錯誤 log —— 收到錯誤時丟棄 id、回報時就退回「大概幾點」。</p>
<p>id 能定位「單跳」、trace 才能定位「全鏈」。一個請求跨五個服務、provider 的第一層 log 只能看到自己這一跳 —— 要從 consumer 回報的識別符追到深處哪個服務出錯、靠的是 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 的傳播義務：W3C Trace Context 規定收到 <code>traceparent</code> header 的服務 MUST 往 outgoing request 傳、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace-id</a> 全鏈不變（見 <a href="/blog/backend/11-api-design/cases/trace-w3c-trace-context/" data-link-title="11.C76 W3C Trace Context：traceparent 的傳播義務與 security boundary 重開機制" data-link-desc="跨 vendor trace 關聯的標準鉤子：每一跳 MUST 傳播、security boundary 可 restart trace、無效 id MUST ignore — 信任邊界寫進規範">11.C76</a>）。這條 MUST 是回饋迴路的規範地基 —— 任何一跳斷掉傳播、consumer 手上的 id 就只能追到斷點。同一份規範也內建信任邊界：security boundary 可以 restart trace（provider 不必信外部給的 trace-id）、無效 id MUST ignore —— 對外的 API 通常回自己生成的 request-id 給 consumer、內部用 trace-id 關聯、兩者在 gateway 對接（此對接模式為常見實務、非規範明文）。trace 系統本身的建置屬 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a>（主流實作生態是 OpenTelemetry）、本文只收契約面：id 要不要給、給了承諾什麼。</p>
<h2 id="同一個錯誤兩種投影呈現與回報">同一個錯誤、兩種投影：呈現與回報</h2>
<p>錯誤內容要分受眾投影、而且兩種投影的組成幾乎相反。給終端使用者呈現的：友善、可行動、不含技術細節（AIP-193 的 <code>LocalizedMessage</code> 層、見 <a href="/blog/backend/11-api-design/cases/errorchain-aip193-error-content/" data-link-title="11.C75 AIP-193 錯誤內容規範：三層受眾與「不假設使用者懂內部實作」" data-link-desc="機器可讀的 (reason, domain) 契約、developer-facing message、LocalizedMessage 三層分工；message 穩定性規則反向揭露 Hyrum&#39;s Law">11.C75</a>）—— 使用者不需要知道是哪個服務的哪類錯誤、需要知道「現在能做什麼」。回報給 provider 的：機器碼（type/code 或 reason/domain）、request-id 或 trace-id、時間戳 —— 全是使用者不需要、定位卻缺一不可的欄位。</p>
<p>實務上最常見的組合是「generic 訊息加識別符」：畫面上顯示友善訊息與一個錯誤編號、使用者回報時唸出編號即可。要標明的邊界：OWASP 的 error handling 指南只要求 generic response 加 server-side log、沒有規範「回傳識別符給使用者」這一段（見 <a href="/blog/backend/11-api-design/cases/errorchain-owasp-error-handling/" data-link-title="11.C77 OWASP error handling：錯誤訊息是攻擊者的偵察面" data-link-desc="非預期錯誤回 generic response、細節只留 server side log — provider 少暴露的安全端論證、跟 AIP-193 的機器可讀路線形成張力">11.C77</a>）—— 這個組合是業界常見實務、識別符部分的規範根據是 Trace Context 與各 vendor 的 request-id 慣例、不是 OWASP。consumer 端的落地判準：錯誤呈現層跟錯誤上報層分開寫 —— 呈現層消費 LocalizedMessage 類欄位、上報層把完整 error 物件（含 id）送進自己的 log 與監控、兩層各取所需、不互相污染。</p>
<h2 id="什麼時候該升級偶發與持續的判讀">什麼時候該升級：偶發與持續的判讀</h2>
<p>回報值不值得、看錯誤的節奏。consumer 端要能區分三種：偶發（單一請求失敗、retry 成功：分散式系統的日常、記 log 不動作）；持續（同一類錯誤連續出現、retry 無效、circuit breaker 開始跳：該檢查是自己的用法錯還是對方壞了）；異常放大（錯誤率突然跳升 —— 對照 provider 的 status page、確認是不是對方的事故）。這條判讀線的量化工具（錯誤率、<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a>、告警閾值）屬 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性</a> 的範圍、契約面的要求只有一條：provider 要有一個 consumer 查得到的健康狀態出口（<a href="/blog/backend/knowledge-cards/status-page/" data-link-title="Status Page" data-link-desc="說明事故期間對外狀態頁如何承接可用性承諾">status page</a> 或 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health endpoint</a>）—— 沒有它、每個 consumer 在事故時都會打 support 問「是不是你們壞了」、支援量在最糟的時刻放大。</p>
<p>provider 側的鏡像責任：把 consumer 的回報當訊號源。同一個 request-id 被多個 consumer 回報、比監控告警更早指出問題；回報的摩擦越低（id 好找、回報入口明確）、這個訊號源越有效 —— 讓 consumer 好回報、是 provider 給自己買的免費監控。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>雙向契約的框架：<a href="/blog/backend/11-api-design/error-bidirectional-contract/" data-link-title="11.11 Status 與錯誤的雙向契約" data-link-desc="status 與錯誤是兩端的合作契約：provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方">11.11 Status 與錯誤的雙向契約</a></li>
<li>錯誤內容的受眾分層：<a href="/blog/backend/11-api-design/error-propagation-trust-boundary/" data-link-title="錯誤傳播與信任邊界：中間服務的雙重身分" data-link-desc="錯誤跨服務傳遞時誰該轉譯、收到的錯誤能信多少、對外暴露多少細節 — 服務鏈上每一跳同時是 consumer 與 provider 的責任判準">錯誤傳播與信任邊界</a></li>
<li>trace 傳播的機制面：<a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing 與 context link</a></li>
<li>error rate 與 SLO 的訊號設計：<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI 量測與 SLO 訊號設計</a></li>
<li>診斷欄位的觀測動機（本篇契約欄位的另一半）：<a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19 Debuggability by Design</a></li>
<li>錯誤格式的欄位設計：<a href="/blog/backend/11-api-design/error-model-design/" data-link-title="11.4 錯誤模型設計" data-link-desc="錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準">11.4 錯誤模型設計</a></li>
<li>案例原文：<a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a></li>
</ul>
]]></content:encoded></item><item><title>11.C64 RFC 4918 207 Multi-Status：status line 降格為「請讀 body」</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-207-multistatus-rfc4918/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-207-multistatus-rfc4918/</guid><description>&lt;p>這個案例的核心責任是提供「status 表達力不足」的規範層正面承認：207 的設計本身就是承認單一 status 裝不下多個獨立結果。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>RFC 4918 §11.1 定義：「The 207 (Multi-Status) status code provides status for multiple independent operations」。§13 明文：頂層雖回 207、「the recipient needs to consult the contents of the multistatus response body for further information about the success or failure of the method execution. The response MAY be used in success, partial success and also in failure situations.」body 是 XML &lt;code>multistatus&lt;/code> root、每個 &lt;code>response&lt;/code> 元素帶各自資源的 status。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>207 是規範層對 status line 表達力不足的正面承認 —— 頂層 status 降格為「請去讀 body」的訊號、真正的成功失敗判定移進 payload。兩端張力落在 consumer：generic HTTP client 與中介層（retry、cache、監控）只看 status line、207 對它們一律是「成功」、部分失敗只有讀得懂 body schema 的 client 才看得到。provider 換到了表達力、代價是把解析責任整包轉給 consumer —— 跟 Google AIP 的反向立場（見 C65）形成兩條路線的正面對照。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 status 表達力邊界章「部分成功」段（規範層先例、與 C65 對照）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.rfc-editor.org/rfc/rfc4918.html">HTTP Extensions for WebDAV（RFC 4918）&lt;/a> — 一手 IETF spec、Proposed Standard。已 WebFetch 驗證、逐字引文另以 rfc-editor 官方 .txt 取回核對。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>207 是 WebDAV 擴充 status、不在 RFC 9110 核心語意內 —— 一般 REST API 借用 207 等於引入 WebDAV 語意、引用時標明脈絡。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「status 表達力不足」的規範層正面承認：207 的設計本身就是承認單一 status 裝不下多個獨立結果。</p>
<h2 id="觀察">觀察</h2>
<p>RFC 4918 §11.1 定義：「The 207 (Multi-Status) status code provides status for multiple independent operations」。§13 明文：頂層雖回 207、「the recipient needs to consult the contents of the multistatus response body for further information about the success or failure of the method execution. The response MAY be used in success, partial success and also in failure situations.」body 是 XML <code>multistatus</code> root、每個 <code>response</code> 元素帶各自資源的 status。</p>
<h2 id="判讀">判讀</h2>
<p>207 是規範層對 status line 表達力不足的正面承認 —— 頂層 status 降格為「請去讀 body」的訊號、真正的成功失敗判定移進 payload。兩端張力落在 consumer：generic HTTP client 與中介層（retry、cache、監控）只看 status line、207 對它們一律是「成功」、部分失敗只有讀得懂 body schema 的 client 才看得到。provider 換到了表達力、代價是把解析責任整包轉給 consumer —— 跟 Google AIP 的反向立場（見 C65）形成兩條路線的正面對照。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 status 表達力邊界章「部分成功」段（規範層先例、與 C65 對照）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc4918.html">HTTP Extensions for WebDAV（RFC 4918）</a> — 一手 IETF spec、Proposed Standard。已 WebFetch 驗證、逐字引文另以 rfc-editor 官方 .txt 取回核對。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>207 是 WebDAV 擴充 status、不在 RFC 9110 核心語意內 —— 一般 REST API 借用 207 等於引入 WebDAV 語意、引用時標明脈絡。</p>
]]></content:encoded></item><item><title>11.C65 Google AIP 部分成功立場：同步必原子、非同步才准部分成功且要顯式 opt-in</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-google-aip-partial-success/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-google-aip-partial-success/</guid><description>&lt;p>這個案例的核心責任是提供部分成功的大廠設計立場：跟 207（C64）相反的路線、以及原子性階梯的具體切分規則。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AIP-193（Errors、Approved）專節明文：「APIs &lt;strong>should not&lt;/strong> support partial errors. Partial errors add significant complexity for users, because they usually sidestep the use of error codes, or move those error codes into the response message, where the user must write specialized error handling logic to address the problem.」出口是 long-running operations：「Methods that require partial errors &lt;strong>should&lt;/strong> use long-running operations, and the method should put partial failure information in the metadata message.」&lt;/p>
&lt;p>批次三部曲同構：AIP-231（Batch Get）「The operation &lt;strong>must&lt;/strong> be atomic: it must fail for all resources or succeed for all resources (no partial success)」；AIP-233（Batch Create）與 AIP-234（Batch Update）規定同步批次必須原子、非同步批次才可支援部分成功 —— 且支援時 metadata 必須含 &lt;code>map&amp;lt;int32, google.rpc.Status&amp;gt; failed_requests&lt;/code>、request 要有 &lt;code>bool return_partial_success&lt;/code> 欄位讓 client 顯式 opt-in。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這組規則把 status 表達力邊界轉成一條設計階梯：同步呼叫只有一個 status 可回、就把語意收窄到原子（讓單一 status 恆為真）；要部分成功、必須升級到非同步 operation、失敗明細結構化進 metadata、且 client 用 opt-in flag 顯式聲明「我會處理部分失敗」。對照 207 的被動解析（consumer 被迫讀 body）、AIP 把契約變成雙向顯式同意 —— 通知責任的分配寫進了介面形狀。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 status 表達力邊界章「部分成功」段（與 C64 對照的反向立場、原子性階梯）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://google.aip.dev/193">AIP-193 Errors&lt;/a> — Approved。已 WebFetch 驗證。&lt;/li>
&lt;li>&lt;a href="https://google.aip.dev/231">AIP-231 Batch methods: Get&lt;/a>、&lt;a href="https://google.aip.dev/233">AIP-233 Batch methods: Create&lt;/a>、&lt;a href="https://google.aip.dev/234">AIP-234 Batch methods: Update&lt;/a> — 均 Approved。已 WebFetch 驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>AIP 對 gRPC / protobuf 生態（&lt;code>google.rpc.Status&lt;/code>）有預設、引到一般 REST 情境要說明可轉譯性。AIP-231 是唯讀批次所以直接禁止部分成功、233/234 是寫入批次才有非同步分支 —— 引用時不可把三者混寫成同一條規則。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供部分成功的大廠設計立場：跟 207（C64）相反的路線、以及原子性階梯的具體切分規則。</p>
<h2 id="觀察">觀察</h2>
<p>AIP-193（Errors、Approved）專節明文：「APIs <strong>should not</strong> support partial errors. Partial errors add significant complexity for users, because they usually sidestep the use of error codes, or move those error codes into the response message, where the user must write specialized error handling logic to address the problem.」出口是 long-running operations：「Methods that require partial errors <strong>should</strong> use long-running operations, and the method should put partial failure information in the metadata message.」</p>
<p>批次三部曲同構：AIP-231（Batch Get）「The operation <strong>must</strong> be atomic: it must fail for all resources or succeed for all resources (no partial success)」；AIP-233（Batch Create）與 AIP-234（Batch Update）規定同步批次必須原子、非同步批次才可支援部分成功 —— 且支援時 metadata 必須含 <code>map&lt;int32, google.rpc.Status&gt; failed_requests</code>、request 要有 <code>bool return_partial_success</code> 欄位讓 client 顯式 opt-in。</p>
<h2 id="判讀">判讀</h2>
<p>這組規則把 status 表達力邊界轉成一條設計階梯：同步呼叫只有一個 status 可回、就把語意收窄到原子（讓單一 status 恆為真）；要部分成功、必須升級到非同步 operation、失敗明細結構化進 metadata、且 client 用 opt-in flag 顯式聲明「我會處理部分失敗」。對照 207 的被動解析（consumer 被迫讀 body）、AIP 把契約變成雙向顯式同意 —— 通知責任的分配寫進了介面形狀。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 status 表達力邊界章「部分成功」段（與 C64 對照的反向立場、原子性階梯）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://google.aip.dev/193">AIP-193 Errors</a> — Approved。已 WebFetch 驗證。</li>
<li><a href="https://google.aip.dev/231">AIP-231 Batch methods: Get</a>、<a href="https://google.aip.dev/233">AIP-233 Batch methods: Create</a>、<a href="https://google.aip.dev/234">AIP-234 Batch methods: Update</a> — 均 Approved。已 WebFetch 驗證。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>AIP 對 gRPC / protobuf 生態（<code>google.rpc.Status</code>）有預設、引到一般 REST 情境要說明可轉譯性。AIP-231 是唯讀批次所以直接禁止部分成功、233/234 是寫入批次才有非同步分支 —— 引用時不可把三者混寫成同一條規則。</p>
]]></content:encoded></item><item><title>11.C66 RFC 9110 202 Accepted：接受不等於承諾、HTTP 沒有回傳非同步結果的機制</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-202-noncommittal-rfc9110/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-202-noncommittal-rfc9110/</guid><description>&lt;p>這個案例的核心責任是提供「先接受、後失敗」模式的規範根據：202 的責任移轉是 HTTP 明文設計。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>RFC 9110 §15.3.3（Internet Standard）原文：「The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place. There is no facility in HTTP for re-sending a status code from an asynchronous operation.」以及「The 202 response is intentionally noncommittal.」規範同時建議 202 的回應內容「ought to describe the request&amp;rsquo;s current status and point to (or embed) a status monitor」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「intentionally noncommittal」加「no facility for re-sending a status code」合起來是規範層的責任移轉聲明：一旦回了 202、HTTP 協定本身不再提供任何管道通知最終失敗 —— 通知責任落到應用層（status monitor、polling endpoint、callback）、且規範只用「ought to」要求 provider 提供、consumer 得主動來查。consumer 把 202 當終局成功、最終失敗就靜默消失。這是 status 表達力的時間軸邊界：status 只描述「收到當下」、描述不了「之後會不會成」。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 status 表達力邊界章「非同步 / 延遲失敗」段（與 C65 的 LRO 出口、C44 AIP-151 Operation 相互印證）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.3">HTTP Semantics §15.3.3（RFC 9110）&lt;/a> — 一手 IETF spec、Internet Standard（STD 97）。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>RFC 9110 全文超出 WebFetch 摘要視窗、逐字原文以 rfc-editor 官方 .txt 直接取回抽段核對 —— 同源同權威、驗證工具為 curl 非 WebFetch。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「先接受、後失敗」模式的規範根據：202 的責任移轉是 HTTP 明文設計。</p>
<h2 id="觀察">觀察</h2>
<p>RFC 9110 §15.3.3（Internet Standard）原文：「The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place. There is no facility in HTTP for re-sending a status code from an asynchronous operation.」以及「The 202 response is intentionally noncommittal.」規範同時建議 202 的回應內容「ought to describe the request&rsquo;s current status and point to (or embed) a status monitor」。</p>
<h2 id="判讀">判讀</h2>
<p>「intentionally noncommittal」加「no facility for re-sending a status code」合起來是規範層的責任移轉聲明：一旦回了 202、HTTP 協定本身不再提供任何管道通知最終失敗 —— 通知責任落到應用層（status monitor、polling endpoint、callback）、且規範只用「ought to」要求 provider 提供、consumer 得主動來查。consumer 把 202 當終局成功、最終失敗就靜默消失。這是 status 表達力的時間軸邊界：status 只描述「收到當下」、描述不了「之後會不會成」。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 status 表達力邊界章「非同步 / 延遲失敗」段（與 C65 的 LRO 出口、C44 AIP-151 Operation 相互印證）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.3">HTTP Semantics §15.3.3（RFC 9110）</a> — 一手 IETF spec、Internet Standard（STD 97）。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>RFC 9110 全文超出 WebFetch 摘要視窗、逐字原文以 rfc-editor 官方 .txt 直接取回抽段核對 —— 同源同權威、驗證工具為 curl 非 WebFetch。</p>
]]></content:encoded></item><item><title>11.C67 RFC 9110 502/504：gateway 只回報自己的觀察、不回報上游的執行狀態</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-502-504-gateway-ambiguity/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/status-502-504-gateway-ambiguity/</guid><description>&lt;p>這個案例的核心責任是提供 502/504 歧義的規範定義基礎：定義是 spec 事實、retry 安全歧義是從定義出發的推導（標明）。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>RFC 9110 §15.6.3：「The 502 (Bad Gateway) status code indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request.」§15.6.5：「The 504 (Gateway Timeout) status code indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request.」兩節全文僅此 —— 規範沒有任何欄位區分「上游根本沒收到請求」與「上游收到並執行了、只是回應沒回來或超時」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>（此段為推導、非 spec 明文。）spec 只定義 gateway 的觀察（沒收到有效或及時回應）、不定義上游的執行狀態。504 尤其如此：connect timeout（請求沒送到、retry 安全）跟 read timeout（請求已執行、對非冪等操作 retry 會重複執行）在 consumer 端拿到同一個 504 —— 兩種情況的 retry 安全性相反、status code 層無法區分。這是 status 表達力的第三種邊界形態：不是裝不下多個結果（C64）、也不是裝不下時間軸（C66）、是裝不下不確定性。緩解手段（idempotency key、上游去重）全在 status code 之外。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 status 表達力邊界章「502/504 歧義」段、接收方重試決策章的 retry 安全合判段（連 11.8 冪等）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.3">HTTP Semantics §15.6.3 / §15.6.5（RFC 9110）&lt;/a> — 一手 IETF spec、Internet Standard。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>逐字原文以 rfc-editor 官方 .txt 取回核對（WebFetch 視窗截斷）。「歧義 → retry 安全性相反」的判讀無單一一手來源明文、正文引用標為從 spec 定義出發的通用推導。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供 502/504 歧義的規範定義基礎：定義是 spec 事實、retry 安全歧義是從定義出發的推導（標明）。</p>
<h2 id="觀察">觀察</h2>
<p>RFC 9110 §15.6.3：「The 502 (Bad Gateway) status code indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request.」§15.6.5：「The 504 (Gateway Timeout) status code indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request.」兩節全文僅此 —— 規範沒有任何欄位區分「上游根本沒收到請求」與「上游收到並執行了、只是回應沒回來或超時」。</p>
<h2 id="判讀">判讀</h2>
<p>（此段為推導、非 spec 明文。）spec 只定義 gateway 的觀察（沒收到有效或及時回應）、不定義上游的執行狀態。504 尤其如此：connect timeout（請求沒送到、retry 安全）跟 read timeout（請求已執行、對非冪等操作 retry 會重複執行）在 consumer 端拿到同一個 504 —— 兩種情況的 retry 安全性相反、status code 層無法區分。這是 status 表達力的第三種邊界形態：不是裝不下多個結果（C64）、也不是裝不下時間軸（C66）、是裝不下不確定性。緩解手段（idempotency key、上游去重）全在 status code 之外。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 status 表達力邊界章「502/504 歧義」段、接收方重試決策章的 retry 安全合判段（連 11.8 冪等）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.3">HTTP Semantics §15.6.3 / §15.6.5（RFC 9110）</a> — 一手 IETF spec、Internet Standard。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>逐字原文以 rfc-editor 官方 .txt 取回核對（WebFetch 視窗截斷）。「歧義 → retry 安全性相反」的判讀無單一一手來源明文、正文引用標為從 spec 定義出發的通用推導。</p>
]]></content:encoded></item><item><title>11.C68 Exponential Backoff And Jitter：無 jitter 的退避是明確輸家</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-brooker-backoff-jitter/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-brooker-backoff-jitter/</guid><description>&lt;p>這個案例的核心責任是提供 backoff 公式選擇的一手實測：consumer 的責任除了退讓、還有彼此去相關。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Marc Brooker 在 AWS Architecture Blog 的實測：N 個 client 同時競爭時「the total amount of work done by the system increases with N²」。三種 jitter 公式逐字：Full Jitter &lt;code>sleep = random(0, min(cap, base * 2^attempt))&lt;/code>；Equal Jitter &lt;code>sleep = base*2^attempt/2 + random(0, base*2^attempt/2)&lt;/code>；Decorrelated Jitter &lt;code>sleep = min(cap, random(0, last_sleep * 3))&lt;/code>。結論：無 jitter 的純 exponential backoff 是「the clear loser」；100 個競爭 client 下 jitter「reduced call count by more than half」；Full Jitter 總工作量最少。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這篇補上「consumer 各自理性、集體災難」的機制：所有 client 同步 backoff 會形成整齊的 retry 波、每一波都是對 provider 的同步衝擊。jitter 把 consumer 之間的隱性同步打散 —— consumer 的契約責任除了「退讓」（backoff）、還有「彼此去相關」（jitter）、後者是單一 consumer 視角看不到的集體契約。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 接收方重試決策章「backoff 公式選擇」段、retry 風暴的同步波成因。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/">Exponential Backoff And Jitter（AWS Architecture Blog、Marc Brooker）&lt;/a> — 一手、作者本人。已 WebFetch 驗證、正文完整取回。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>發表於 2015、模擬情境是 OCC 寫入競爭而非 HTTP API retry —— 結論可遷移、但引用時說明實驗設定不同。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供 backoff 公式選擇的一手實測：consumer 的責任除了退讓、還有彼此去相關。</p>
<h2 id="觀察">觀察</h2>
<p>Marc Brooker 在 AWS Architecture Blog 的實測：N 個 client 同時競爭時「the total amount of work done by the system increases with N²」。三種 jitter 公式逐字：Full Jitter <code>sleep = random(0, min(cap, base * 2^attempt))</code>；Equal Jitter <code>sleep = base*2^attempt/2 + random(0, base*2^attempt/2)</code>；Decorrelated Jitter <code>sleep = min(cap, random(0, last_sleep * 3))</code>。結論：無 jitter 的純 exponential backoff 是「the clear loser」；100 個競爭 client 下 jitter「reduced call count by more than half」；Full Jitter 總工作量最少。</p>
<h2 id="判讀">判讀</h2>
<p>這篇補上「consumer 各自理性、集體災難」的機制：所有 client 同步 backoff 會形成整齊的 retry 波、每一波都是對 provider 的同步衝擊。jitter 把 consumer 之間的隱性同步打散 —— consumer 的契約責任除了「退讓」（backoff）、還有「彼此去相關」（jitter）、後者是單一 consumer 視角看不到的集體契約。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 接收方重試決策章「backoff 公式選擇」段、retry 風暴的同步波成因。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/">Exponential Backoff And Jitter（AWS Architecture Blog、Marc Brooker）</a> — 一手、作者本人。已 WebFetch 驗證、正文完整取回。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>發表於 2015、模擬情境是 OCC 寫入競爭而非 HTTP API retry —— 結論可遷移、但引用時說明實驗設定不同。</p>
]]></content:encoded></item><item><title>11.C69 Google SRE Book：retry 放大與跨層疊乘、per-request 上限與 retry budget</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-sre-book-cascading-failures/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-sre-book-cascading-failures/</guid><description>&lt;p>這個案例的核心責任是提供接收方重試決策的量化判準、以及「retry 該放哪一層」的架構責任分配。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Google SRE Book「Addressing Cascading Failures」章：retry 放大例 —— 100 QPS 失敗、retry 疊加成 200 QPS、再 300 QPS、「fewer and fewer requests are able to succeed on their first attempt, so less useful work is being performed」。四條逐字建議：「Always use randomized exponential backoff when scheduling retries」；「Limit retries per request. Don&amp;rsquo;t retry a given request indefinitely」；「Consider having a server-wide retry budget. For example, only allow 60 retries per minute in a process」；避免「amplifying retries by issuing retries at multiple levels」—— 三層各 retry 3 次會在最底層產生 64 次嘗試。另要求用不同 response code「separate retriable and nonretriable error conditions」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>這章把責任明確放到兩端：provider 要用 status/error 區分可重試與不可重試（這正是雙向契約的 provider 義務、對應 11.4 的第一刀）；consumer 要有 per-request 上限加全程序 retry budget。跨層疊乘（64 倍）說明 retry 決策必須在架構層指定「哪一層負責 retry」—— 責任沒分配時、每層的局部自保疊成全域攻擊。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 接收方重試決策章「retry budget 量化判準」「retry 放哪一層」段、provider 的 retriable/nonretriable 標示義務（回扣 11.4）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://sre.google/sre-book/addressing-cascading-failures/">Addressing Cascading Failures（Google SRE Book）&lt;/a> — 一手、官方站。已 WebFetch 驗證、正文完整取回。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>本章未討論 circuit breaker（取回內容確認）—— circuit breaker 段以 C71（Slack）承接。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供接收方重試決策的量化判準、以及「retry 該放哪一層」的架構責任分配。</p>
<h2 id="觀察">觀察</h2>
<p>Google SRE Book「Addressing Cascading Failures」章：retry 放大例 —— 100 QPS 失敗、retry 疊加成 200 QPS、再 300 QPS、「fewer and fewer requests are able to succeed on their first attempt, so less useful work is being performed」。四條逐字建議：「Always use randomized exponential backoff when scheduling retries」；「Limit retries per request. Don&rsquo;t retry a given request indefinitely」；「Consider having a server-wide retry budget. For example, only allow 60 retries per minute in a process」；避免「amplifying retries by issuing retries at multiple levels」—— 三層各 retry 3 次會在最底層產生 64 次嘗試。另要求用不同 response code「separate retriable and nonretriable error conditions」。</p>
<h2 id="判讀">判讀</h2>
<p>這章把責任明確放到兩端：provider 要用 status/error 區分可重試與不可重試（這正是雙向契約的 provider 義務、對應 11.4 的第一刀）；consumer 要有 per-request 上限加全程序 retry budget。跨層疊乘（64 倍）說明 retry 決策必須在架構層指定「哪一層負責 retry」—— 責任沒分配時、每層的局部自保疊成全域攻擊。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 接收方重試決策章「retry budget 量化判準」「retry 放哪一層」段、provider 的 retriable/nonretriable 標示義務（回扣 11.4）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://sre.google/sre-book/addressing-cascading-failures/">Addressing Cascading Failures（Google SRE Book）</a> — 一手、官方站。已 WebFetch 驗證、正文完整取回。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>本章未討論 circuit breaker（取回內容確認）—— circuit breaker 段以 C71（Slack）承接。</p>
]]></content:encoded></item><item><title>11.C70 AWS DynamoDB 2015 事故：內部元件的 retry 自保把錯誤率推到 55%（反例）</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-dynamodb-2015-storm/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-dynamodb-2015-storm/</guid><description>&lt;p>這個案例的核心責任是提供 retry 風暴的教科書實例：consumer 是 AWS 自己的內部元件、說明這是任何 caller 的結構性行為、不是外部客戶不守規矩。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AWS 官方 postmortem（Summary of the Amazon DynamoDB Service Disruption、2015-09-20、US-East）：GSI 採用讓 membership 資料膨脹（「a table with large numbers of partitions could have its contribution of partition data to the membership lists quickly double or triple」）。網路擾動後、大量同時的 membership 請求讓 metadata 服務處理變慢並超過時限 —— 逾時的 storage server 自我下線、再重試、進一步壓垮 metadata 服務。「By 2:37am PDT, the error rate … had risen far beyond any level experienced in the last 3 years, finally stabilizing at approximately 55%」。復原手段是 5:06am 主動暫停對 metadata 服務的請求以卸載、加容量、7:10am 恢復。事後修正：加大 metadata 容量、監控 membership 大小、降低 retry 請求速率、metadata 服務分片。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>consumer 的 retry 自保在 provider 過載時等效於 DDoS —— 而這裡的 consumer 是 AWS 自己的 storage server、證明這是結構性行為。風暴一旦成形、系統不會自癒：復原必須人為切斷 retry 迴路（暫停請求）讓 provider 喘息。事後修正同時動兩端 —— provider 加容量加分片、consumer 降 retry 率 —— 責任是雙向的、單邊修不了這類事故。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 接收方重試決策章「retry 風暴實例」段、「風暴成形後為何要人工斷路」。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/message/5467D2/">Summary of the Amazon DynamoDB Service Disruption（AWS 官方 postmortem）&lt;/a> — 一手。已 WebFetch 驗證、正文完整取回。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>postmortem 未逐字出現「retry storm」一詞 —— 放大機制從「simultaneous requests + 自我下線再重連」敘述推得、正文引述貼原句、不替 AWS 造詞（「retry storm」的官方定義見 C72）。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供 retry 風暴的教科書實例：consumer 是 AWS 自己的內部元件、說明這是任何 caller 的結構性行為、不是外部客戶不守規矩。</p>
<h2 id="觀察">觀察</h2>
<p>AWS 官方 postmortem（Summary of the Amazon DynamoDB Service Disruption、2015-09-20、US-East）：GSI 採用讓 membership 資料膨脹（「a table with large numbers of partitions could have its contribution of partition data to the membership lists quickly double or triple」）。網路擾動後、大量同時的 membership 請求讓 metadata 服務處理變慢並超過時限 —— 逾時的 storage server 自我下線、再重試、進一步壓垮 metadata 服務。「By 2:37am PDT, the error rate … had risen far beyond any level experienced in the last 3 years, finally stabilizing at approximately 55%」。復原手段是 5:06am 主動暫停對 metadata 服務的請求以卸載、加容量、7:10am 恢復。事後修正：加大 metadata 容量、監控 membership 大小、降低 retry 請求速率、metadata 服務分片。</p>
<h2 id="判讀">判讀</h2>
<p>consumer 的 retry 自保在 provider 過載時等效於 DDoS —— 而這裡的 consumer 是 AWS 自己的 storage server、證明這是結構性行為。風暴一旦成形、系統不會自癒：復原必須人為切斷 retry 迴路（暫停請求）讓 provider 喘息。事後修正同時動兩端 —— provider 加容量加分片、consumer 降 retry 率 —— 責任是雙向的、單邊修不了這類事故。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 接收方重試決策章「retry 風暴實例」段、「風暴成形後為何要人工斷路」。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/message/5467D2/">Summary of the Amazon DynamoDB Service Disruption（AWS 官方 postmortem）</a> — 一手。已 WebFetch 驗證、正文完整取回。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>postmortem 未逐字出現「retry storm」一詞 —— 放大機制從「simultaneous requests + 自我下線再重連」敘述推得、正文引述貼原句、不替 AWS 造詞（「retry storm」的官方定義見 C72）。</p>
]]></content:encoded></item><item><title>11.C71 Slack 2021-01-04 事故：復原期 retry 加 circuit breaking 是藥方</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-slack-2021-recovery/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-slack-2021-recovery/</guid><description>&lt;p>這個案例的核心責任是給 retry 敘事一個必要的反向平衡：retry 不是純反派、circuit breaker 是它的閘門。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Slack 官方 postmortem（2021-01-04 事故）：誘因是假期後第一個上班日「client caches are cold and clients pull down more data than usual on their first connection」、AWS Transit Gateway 未及時擴容而過載、造成「widespread packet loss」。雪上加霜：CPU 利用率下降「initially triggered some automated downscaling」（最需要容量時反而縮容）；緊急擴容撞到 Linux open files limit 與 AWS quota。復原關鍵句：「This — plus retries and circuit breaking — got us back to serving」、加上 load balancer 的 panic mode；AWS 手動加 TGW 容量後 10:40am 恢復正常。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>retry 的雙面性：底層網路恢復後、正是 retry 加 circuit breaking 讓系統爬回服務狀態。circuit breaker 是 retry 的閘門 —— 斷路時擋住無效重試保護 provider、半開時用少量探測請求驗證恢復、恢復後 retry 才轉為復原工具。責任判讀：consumer 的 retry 是否有害、取決於 provider 當下處於「過載中」還是「恢復中」—— 而 consumer 無法直接觀測這件事、所以需要 circuit breaker 這種本地推斷機制代替猜測。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 接收方重試決策章「circuit breaker 作為 retry 閘門」段、「retry 的雙面性」收束。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://slack.engineering/slacks-outage-on-january-4th-2021/">Slack&amp;rsquo;s Outage on January 4th 2021（Slack Engineering）&lt;/a> — 一手官方 postmortem。已 WebFetch 驗證、正文完整取回。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>事故主因是網路層（TGW）而非 API 契約層 —— 引用定位為「retry / circuit breaker 在復原期的角色」、不包裝成 API retry 風暴主案例（主案例是 C70）。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是給 retry 敘事一個必要的反向平衡：retry 不是純反派、circuit breaker 是它的閘門。</p>
<h2 id="觀察">觀察</h2>
<p>Slack 官方 postmortem（2021-01-04 事故）：誘因是假期後第一個上班日「client caches are cold and clients pull down more data than usual on their first connection」、AWS Transit Gateway 未及時擴容而過載、造成「widespread packet loss」。雪上加霜：CPU 利用率下降「initially triggered some automated downscaling」（最需要容量時反而縮容）；緊急擴容撞到 Linux open files limit 與 AWS quota。復原關鍵句：「This — plus retries and circuit breaking — got us back to serving」、加上 load balancer 的 panic mode；AWS 手動加 TGW 容量後 10:40am 恢復正常。</p>
<h2 id="判讀">判讀</h2>
<p>retry 的雙面性：底層網路恢復後、正是 retry 加 circuit breaking 讓系統爬回服務狀態。circuit breaker 是 retry 的閘門 —— 斷路時擋住無效重試保護 provider、半開時用少量探測請求驗證恢復、恢復後 retry 才轉為復原工具。責任判讀：consumer 的 retry 是否有害、取決於 provider 當下處於「過載中」還是「恢復中」—— 而 consumer 無法直接觀測這件事、所以需要 circuit breaker 這種本地推斷機制代替猜測。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 接收方重試決策章「circuit breaker 作為 retry 閘門」段、「retry 的雙面性」收束。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://slack.engineering/slacks-outage-on-january-4th-2021/">Slack&rsquo;s Outage on January 4th 2021（Slack Engineering）</a> — 一手官方 postmortem。已 WebFetch 驗證、正文完整取回。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>事故主因是網路層（TGW）而非 API 契約層 —— 引用定位為「retry / circuit breaker 在復原期的角色」、不包裝成 API retry 風暴主案例（主案例是 C70）。</p>
]]></content:encoded></item><item><title>11.C72 AWS retry 指南：retry storm 的官方定義與分層限制</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-aws-guidance-budget/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/retry-aws-guidance-budget/</guid><description>&lt;p>這個案例的核心責任是提供 retry storm 的官方定義、與「retry 放哪一層」的分層量化建議。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AWS Well-Architected REL05-BP03（Control and limit retry calls）逐字定義：「the network can quickly become saturated with new and retried requests … This can result in a &lt;em>retry storm&lt;/em>, which will reduce availability of the service」。分層建議逐字：「For services lower in the stack, a maximum retry limit of zero or one can limit risk yet still be effective as retries are delegated to services higher in the stack」。該文件官方引用 AWS Builders&amp;rsquo; Library 的「Timeouts, retries, and backoff with jitter」（Marc Brooker）—— 該文主張 retry 會放大依賴系統的負載、過載時 retry 讓過載更糟、偏好 token bucket 式的本地 retry 限制（意譯、見下方標注）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「低層 retry 0-1 次、委派給上層」跟 C69 的跨層疊乘（64 倍）互相印證：retry 是要在架構層分配的預算、不是每層預設行為。token bucket 的本地限制把「retry 是否過量」從每次請求的局部判斷、變成程序級的資源帳 —— consumer 端對 provider 的保護寫成了自己的限流。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 接收方重試決策章「retry 放哪一層」「retry budget」段（與 C69 互證）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.aws.amazon.com/wellarchitected/2022-03-31/framework/rel_mitigate_interaction_failure_limit_retries.html">REL05-BP03 Control and limit retry calls（AWS Well-Architected）&lt;/a> — 一手官方文件。已 WebFetch 驗證、正文完整取回、逐字引文以此為錨。&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/">Timeouts, retries, and backoff with jitter（AWS Builders&amp;rsquo; Library、Marc Brooker）&lt;/a> — 一手、但新站為 JS 渲染、WebFetch 僅取得頁殼。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>Builders&amp;rsquo; Library 的措辭（retry 放大、token bucket）為意譯 —— 逐字引文未能取得（JS 渲染）、以 REL05-BP03 的官方引用與逐字定義為錨。正文需要逐字引用時只引 REL05-BP03。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供 retry storm 的官方定義、與「retry 放哪一層」的分層量化建議。</p>
<h2 id="觀察">觀察</h2>
<p>AWS Well-Architected REL05-BP03（Control and limit retry calls）逐字定義：「the network can quickly become saturated with new and retried requests … This can result in a <em>retry storm</em>, which will reduce availability of the service」。分層建議逐字：「For services lower in the stack, a maximum retry limit of zero or one can limit risk yet still be effective as retries are delegated to services higher in the stack」。該文件官方引用 AWS Builders&rsquo; Library 的「Timeouts, retries, and backoff with jitter」（Marc Brooker）—— 該文主張 retry 會放大依賴系統的負載、過載時 retry 讓過載更糟、偏好 token bucket 式的本地 retry 限制（意譯、見下方標注）。</p>
<h2 id="判讀">判讀</h2>
<p>「低層 retry 0-1 次、委派給上層」跟 C69 的跨層疊乘（64 倍）互相印證：retry 是要在架構層分配的預算、不是每層預設行為。token bucket 的本地限制把「retry 是否過量」從每次請求的局部判斷、變成程序級的資源帳 —— consumer 端對 provider 的保護寫成了自己的限流。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 接收方重試決策章「retry 放哪一層」「retry budget」段（與 C69 互證）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/wellarchitected/2022-03-31/framework/rel_mitigate_interaction_failure_limit_retries.html">REL05-BP03 Control and limit retry calls（AWS Well-Architected）</a> — 一手官方文件。已 WebFetch 驗證、正文完整取回、逐字引文以此為錨。</li>
<li><a href="https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/">Timeouts, retries, and backoff with jitter（AWS Builders&rsquo; Library、Marc Brooker）</a> — 一手、但新站為 JS 渲染、WebFetch 僅取得頁殼。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>Builders&rsquo; Library 的措辭（retry 放大、token bucket）為意譯 —— 逐字引文未能取得（JS 渲染）、以 REL05-BP03 的官方引用與逐字定義為錨。正文需要逐字引用時只引 REL05-BP03。</p>
]]></content:encoded></item><item><title>11.C73 gRPC 兩層錯誤模型：status code 是保證層、richer detail 是選配層</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-grpc-two-layer-model/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-grpc-two-layer-model/</guid><description>&lt;p>這個案例的核心責任是提供錯誤契約分層的一手根據：保證層與選配層的傳播能力不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>gRPC 官方 error guide：標準模型是成功回 &lt;code>OK&lt;/code>、失敗「gRPC returns one of its error status codes instead, with an optional string error message」。richer error model（&lt;code>google.rpc.Status&lt;/code>）「enables servers to return and clients to consume additional error details expressed as one or more protobuf messages」、提供常見錯誤型別（invalid parameters、quota violations、stack traces）、實作上「as trailing metadata in the response」。官方自列三個風險：跨語言實作不一致（僅 C++/Go/Java/Python/Ruby 支援、「broader support remains uncertain」）、proxies 與 loggers 看不到 trailing metadata 裡的 error detail、payload 過大會撞 header size 上限。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>兩端張力在「保證層 vs 選配層」：status code 是所有語言 client 都拿得到的最低契約；richer detail 是選配、而且中間節點（proxy、logger）對它是盲的。中間服務作為 consumer 拿到 richer detail、作為 provider 轉發時不能假設下游也解得開 —— 轉譯責任落在它身上。錯誤契約設計要先分清哪些資訊放保證層（全鏈可見）、哪些放選配層（端到端可見、中間盲）。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 錯誤鏈傳播章「錯誤契約的保證層與選配層」開場、「中間件對錯誤細節的可見性」段。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://grpc.io/docs/guides/error/">Error handling（gRPC 官方 docs）&lt;/a> — 一手、現行版。已 WebFetch 驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>richer model 語言支援「broader support remains uncertain」—— 不可寫成「gRPC 全語言支援 richer errors」。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供錯誤契約分層的一手根據：保證層與選配層的傳播能力不同。</p>
<h2 id="觀察">觀察</h2>
<p>gRPC 官方 error guide：標準模型是成功回 <code>OK</code>、失敗「gRPC returns one of its error status codes instead, with an optional string error message」。richer error model（<code>google.rpc.Status</code>）「enables servers to return and clients to consume additional error details expressed as one or more protobuf messages」、提供常見錯誤型別（invalid parameters、quota violations、stack traces）、實作上「as trailing metadata in the response」。官方自列三個風險：跨語言實作不一致（僅 C++/Go/Java/Python/Ruby 支援、「broader support remains uncertain」）、proxies 與 loggers 看不到 trailing metadata 裡的 error detail、payload 過大會撞 header size 上限。</p>
<h2 id="判讀">判讀</h2>
<p>兩端張力在「保證層 vs 選配層」：status code 是所有語言 client 都拿得到的最低契約；richer detail 是選配、而且中間節點（proxy、logger）對它是盲的。中間服務作為 consumer 拿到 richer detail、作為 provider 轉發時不能假設下游也解得開 —— 轉譯責任落在它身上。錯誤契約設計要先分清哪些資訊放保證層（全鏈可見）、哪些放選配層（端到端可見、中間盲）。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 錯誤鏈傳播章「錯誤契約的保證層與選配層」開場、「中間件對錯誤細節的可見性」段。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://grpc.io/docs/guides/error/">Error handling（gRPC 官方 docs）</a> — 一手、現行版。已 WebFetch 驗證。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>richer model 語言支援「broader support remains uncertain」—— 不可寫成「gRPC 全語言支援 richer errors」。</p>
]]></content:encoded></item><item><title>11.C74 gRPC status code 產生者歧義：收到的 code 不一定來自 server 應用層</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-grpc-code-producer-ambiguity/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-grpc-code-producer-ambiguity/</guid><description>&lt;p>這個案例的核心責任是提供「consumer 對收到的錯誤能信多少」的一手根據：同一個 code、兩個可能的產生者。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>gRPC status codes guide 列 17 個 code、明確標注「Only a subset of the pre-defined status codes are generated by the gRPC libraries」。只由 user code 產生（library 從不產生）的 code：INVALID_ARGUMENT、NOT_FOUND、ALREADY_EXISTS、FAILED_PRECONDITION、ABORTED、OUT_OF_RANGE、DATA_LOSS。UNAVAILABLE 標為「most likely a transient condition, which can be corrected by retrying with a backoff」、但對非冪等操作要小心。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>client 收到 UNAVAILABLE、DEADLINE_EXCEEDED、INTERNAL 時、無法單從 code 分辨是 server 應用回的、還是中間 channel 或 library 自己產生的 —— 只有那 7 個「library 從不產生」的 code 能確定來自 server 邏輯。中間服務轉發錯誤時原樣透傳 UNKNOWN 或 INTERNAL、等於把「產生者是誰」的資訊消滅掉。這直接支撐「provider 暴露多少 vs consumer 能信多少」：錯誤的可信度不是均質的、契約設計要讓 consumer 分得出哪些 code 承載應用語意。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 錯誤鏈傳播章「consumer 對收到的錯誤能信多少」段；UNAVAILABLE 的 retry 判讀連接收方重試決策章。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://grpc.io/docs/guides/status-codes/">Status codes（gRPC 官方 docs）&lt;/a> — 一手、現行版。已 WebFetch 驗證。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「consumer 對收到的錯誤能信多少」的一手根據：同一個 code、兩個可能的產生者。</p>
<h2 id="觀察">觀察</h2>
<p>gRPC status codes guide 列 17 個 code、明確標注「Only a subset of the pre-defined status codes are generated by the gRPC libraries」。只由 user code 產生（library 從不產生）的 code：INVALID_ARGUMENT、NOT_FOUND、ALREADY_EXISTS、FAILED_PRECONDITION、ABORTED、OUT_OF_RANGE、DATA_LOSS。UNAVAILABLE 標為「most likely a transient condition, which can be corrected by retrying with a backoff」、但對非冪等操作要小心。</p>
<h2 id="判讀">判讀</h2>
<p>client 收到 UNAVAILABLE、DEADLINE_EXCEEDED、INTERNAL 時、無法單從 code 分辨是 server 應用回的、還是中間 channel 或 library 自己產生的 —— 只有那 7 個「library 從不產生」的 code 能確定來自 server 邏輯。中間服務轉發錯誤時原樣透傳 UNKNOWN 或 INTERNAL、等於把「產生者是誰」的資訊消滅掉。這直接支撐「provider 暴露多少 vs consumer 能信多少」：錯誤的可信度不是均質的、契約設計要讓 consumer 分得出哪些 code 承載應用語意。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 錯誤鏈傳播章「consumer 對收到的錯誤能信多少」段；UNAVAILABLE 的 retry 判讀連接收方重試決策章。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://grpc.io/docs/guides/status-codes/">Status codes（gRPC 官方 docs）</a> — 一手、現行版。已 WebFetch 驗證。</li>
</ul>
]]></content:encoded></item><item><title>11.C75 AIP-193 錯誤內容規範：三層受眾與「不假設使用者懂內部實作」</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-aip193-error-content/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-aip193-error-content/</guid><description>&lt;p>這個案例的核心責任是提供「provider 該暴露什麼、給誰」的規範根據：錯誤內容按受眾分三層。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AIP-193（Approved、updated 2024-10-18）規定 error 用 &lt;code>google.rpc.Status&lt;/code>（code / message / details）、details 必含 &lt;code>ErrorInfo&lt;/code>：reason（&lt;code>[A-Z][A-Z0-9_]+[A-Z0-9]&lt;/code>、max 63 字元）、domain（全域唯一、通常是服務名如 &lt;code>pubsub.googleapis.com&lt;/code>）、metadata（request-specific key-value）。(reason, domain) 組成機器可讀識別符、同一錯誤必須保持一致。信任邊界原文：「error messages &lt;strong>must not&lt;/strong> assume that the user will know anything about its underlying implementation」。message 定位是「developer-facing, human-readable &amp;ldquo;debug message&amp;rdquo;」、給終端使用者的文案走 &lt;code>LocalizedMessage&lt;/code>。另有穩定性規則：沒帶 ErrorInfo 的舊 API「The content of &lt;code>Status.message&lt;/code> &lt;strong>must&lt;/strong> be stable」—— client 已在 parse message、動了就 break。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>AIP-193 把「provider 暴露多少」拆成三個受眾層：機器（ErrorInfo 的 reason/domain、可程式化分支）、開發者（message、可變動但不可當 API）、終端使用者（LocalizedMessage）。「must not assume underlying implementation」是信任邊界的正面規範 —— 錯誤要用 consumer 的語彙寫、不是把內部狀態倒出來。message 穩定性規則反向揭露 Hyrum&amp;rsquo;s Law 張力：provider 沒給機器可讀欄位、consumer 就會把人類可讀欄位當契約、之後改字就是 breaking change。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 錯誤鏈傳播章「錯誤契約的三層受眾」「provider 該暴露什麼」段（與 C77 OWASP 對撞成中間路線）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://google.aip.dev/193">AIP-193 Errors&lt;/a> — Approved、updated 2024-10-18。已 WebFetch 驗證（兩次交叉確認）。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>AIP-193 沒有任何「中間服務怎麼轉譯 upstream 錯誤」的條文（兩次針對性取回確認缺席）—— 它只規範 outbound error 的形狀。「跨服務轉譯」的責任論證是從雙重身分推導、正文標明是推導不是引用。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「provider 該暴露什麼、給誰」的規範根據：錯誤內容按受眾分三層。</p>
<h2 id="觀察">觀察</h2>
<p>AIP-193（Approved、updated 2024-10-18）規定 error 用 <code>google.rpc.Status</code>（code / message / details）、details 必含 <code>ErrorInfo</code>：reason（<code>[A-Z][A-Z0-9_]+[A-Z0-9]</code>、max 63 字元）、domain（全域唯一、通常是服務名如 <code>pubsub.googleapis.com</code>）、metadata（request-specific key-value）。(reason, domain) 組成機器可讀識別符、同一錯誤必須保持一致。信任邊界原文：「error messages <strong>must not</strong> assume that the user will know anything about its underlying implementation」。message 定位是「developer-facing, human-readable &ldquo;debug message&rdquo;」、給終端使用者的文案走 <code>LocalizedMessage</code>。另有穩定性規則：沒帶 ErrorInfo 的舊 API「The content of <code>Status.message</code> <strong>must</strong> be stable」—— client 已在 parse message、動了就 break。</p>
<h2 id="判讀">判讀</h2>
<p>AIP-193 把「provider 暴露多少」拆成三個受眾層：機器（ErrorInfo 的 reason/domain、可程式化分支）、開發者（message、可變動但不可當 API）、終端使用者（LocalizedMessage）。「must not assume underlying implementation」是信任邊界的正面規範 —— 錯誤要用 consumer 的語彙寫、不是把內部狀態倒出來。message 穩定性規則反向揭露 Hyrum&rsquo;s Law 張力：provider 沒給機器可讀欄位、consumer 就會把人類可讀欄位當契約、之後改字就是 breaking change。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 錯誤鏈傳播章「錯誤契約的三層受眾」「provider 該暴露什麼」段（與 C77 OWASP 對撞成中間路線）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://google.aip.dev/193">AIP-193 Errors</a> — Approved、updated 2024-10-18。已 WebFetch 驗證（兩次交叉確認）。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>AIP-193 沒有任何「中間服務怎麼轉譯 upstream 錯誤」的條文（兩次針對性取回確認缺席）—— 它只規範 outbound error 的形狀。「跨服務轉譯」的責任論證是從雙重身分推導、正文標明是推導不是引用。</p>
]]></content:encoded></item><item><title>11.C76 W3C Trace Context：traceparent 的傳播義務與 security boundary 重開機制</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/trace-w3c-trace-context/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/trace-w3c-trace-context/</guid><description>&lt;p>這個案例的核心責任是提供「trace id 作為雙向 debug 契約」的規範根據：傳播義務與不信任機制同時內建。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>W3C Trace Context（Recommendation、2021-11-23）解決的問題原文：「Traces that are collected by different tracing vendors cannot be correlated as there is no shared unique identifier」。traceparent 格式 &lt;code>version-trace-id-parent-id-trace-flags&lt;/code>（trace-id 32 hex、parent-id 16 hex、flags 2 hex）。收到 traceparent 的服務 MUST 往 outgoing request 傳、允許的變更只有三種：更新 parent-id、更新 sampled flag、restart trace（在 security boundary 全部重新生成）。tracestate 載 vendor 專屬 key-value、「If the value of the traceparent field wasn&amp;rsquo;t changed before propagation, tracestate MUST NOT be modified」。無效 id 的處理：trace-id 全零或含非法字元、parent-id 無效時「Vendors MUST ignore the traceparent」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>consumer 回報問題時附的 trace id 之所以能關聯全鏈、是因為每一跳都有 MUST 級的傳播義務 —— 這是回饋迴路的規範地基。但 spec 同時內建信任邊界的兩個機制：security boundary 可 restart trace（provider 不必信 consumer 給的 trace-id）、無效 id MUST ignore（不把不可信識別符往下游傳播）。中間服務的雙重身分在這裡最具體：對 upstream 是「要不要信 incoming traceparent」的 consumer、對 downstream 是「必須產新 parent-id 再傳」的 provider。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 回饋迴路章「trace id 作為雙向 debug 契約」段；restart-at-boundary 交叉到錯誤鏈傳播章的信任邊界段。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.w3.org/TR/trace-context/">Trace Context（W3C Recommendation、2021-11-23）&lt;/a> — 一手 W3C 規範。已 WebFetch 驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>Trace Context Level 2 目前僅 Candidate Recommendation Draft（2024-03-28、自述「should not be cited as final」）—— 引用以 Level 1 Recommendation 為準、Level 2 最多當註腳。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「trace id 作為雙向 debug 契約」的規範根據：傳播義務與不信任機制同時內建。</p>
<h2 id="觀察">觀察</h2>
<p>W3C Trace Context（Recommendation、2021-11-23）解決的問題原文：「Traces that are collected by different tracing vendors cannot be correlated as there is no shared unique identifier」。traceparent 格式 <code>version-trace-id-parent-id-trace-flags</code>（trace-id 32 hex、parent-id 16 hex、flags 2 hex）。收到 traceparent 的服務 MUST 往 outgoing request 傳、允許的變更只有三種：更新 parent-id、更新 sampled flag、restart trace（在 security boundary 全部重新生成）。tracestate 載 vendor 專屬 key-value、「If the value of the traceparent field wasn&rsquo;t changed before propagation, tracestate MUST NOT be modified」。無效 id 的處理：trace-id 全零或含非法字元、parent-id 無效時「Vendors MUST ignore the traceparent」。</p>
<h2 id="判讀">判讀</h2>
<p>consumer 回報問題時附的 trace id 之所以能關聯全鏈、是因為每一跳都有 MUST 級的傳播義務 —— 這是回饋迴路的規範地基。但 spec 同時內建信任邊界的兩個機制：security boundary 可 restart trace（provider 不必信 consumer 給的 trace-id）、無效 id MUST ignore（不把不可信識別符往下游傳播）。中間服務的雙重身分在這裡最具體：對 upstream 是「要不要信 incoming traceparent」的 consumer、對 downstream 是「必須產新 parent-id 再傳」的 provider。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 回饋迴路章「trace id 作為雙向 debug 契約」段；restart-at-boundary 交叉到錯誤鏈傳播章的信任邊界段。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.w3.org/TR/trace-context/">Trace Context（W3C Recommendation、2021-11-23）</a> — 一手 W3C 規範。已 WebFetch 驗證。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>Trace Context Level 2 目前僅 Candidate Recommendation Draft（2024-03-28、自述「should not be cited as final」）—— 引用以 Level 1 Recommendation 為準、Level 2 最多當註腳。</p>
]]></content:encoded></item><item><title>11.C77 OWASP error handling：錯誤訊息是攻擊者的偵察面</title><link>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-owasp-error-handling/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/11-api-design/cases/errorchain-owasp-error-handling/</guid><description>&lt;p>這個案例的核心責任是提供「provider 暴露下限」的安全端論證、跟 C75（AIP-193）對撞出中間路線。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>OWASP Error Handling Cheat Sheet 核心規則：非預期錯誤時「a generic response is returned by the application but the error details are logged server side for investigation, and not returned to the user」。理由是偵察風險：「unhandled errors can assist an attacker in this initial phase」—— 實例包含 stack trace 洩漏 Struts2/Tomcat 版本、SQL error 洩漏安裝路徑並幫攻擊者「identify an injection point」。實作範例是統一回 HTTP 500 加 generic body（如 &lt;code>{&amp;quot;message&amp;quot;:&amp;quot;An error occur, please retry&amp;quot;}&lt;/code>）。另建議監控 5xx：「a good indication of the application failing for some sets of inputs」。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>OWASP 給了「provider 少暴露」的安全端論證、跟 AIP-193 的「多給機器可讀細節」形成張力的兩端 —— 全 generic 讓 consumer 完全無法自助、全細節變成攻擊偵察面。AIP-193 的 (reason, domain) 設計正是中間路線：給分支用的機器可讀識別符、不洩內部實作。這組對撞是「provider 該暴露什麼」的邊界討論骨架、對應 backend/07 的攻擊面思路。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>11.11 錯誤鏈傳播章「暴露的下限：安全邊界」段（與 C75 對照）、連 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 安全&lt;/a>。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html">Error Handling Cheat Sheet（OWASP Cheat Sheet Series）&lt;/a> — 一手、現行版。已 WebFetch 驗證。&lt;/li>
&lt;/ul>
&lt;h2 id="二手來源與狀態標注">二手來源與狀態標注&lt;/h2>
&lt;p>該頁完全沒提 error ID / correlation id 回傳給使用者 ——「generic message + 附 trace id 供回報」這個常見組合不能掛 OWASP 出處：trace id 部分引 C76（W3C Trace Context）、組合本身標明是常見實務而非 OWASP 規範。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是提供「provider 暴露下限」的安全端論證、跟 C75（AIP-193）對撞出中間路線。</p>
<h2 id="觀察">觀察</h2>
<p>OWASP Error Handling Cheat Sheet 核心規則：非預期錯誤時「a generic response is returned by the application but the error details are logged server side for investigation, and not returned to the user」。理由是偵察風險：「unhandled errors can assist an attacker in this initial phase」—— 實例包含 stack trace 洩漏 Struts2/Tomcat 版本、SQL error 洩漏安裝路徑並幫攻擊者「identify an injection point」。實作範例是統一回 HTTP 500 加 generic body（如 <code>{&quot;message&quot;:&quot;An error occur, please retry&quot;}</code>）。另建議監控 5xx：「a good indication of the application failing for some sets of inputs」。</p>
<h2 id="判讀">判讀</h2>
<p>OWASP 給了「provider 少暴露」的安全端論證、跟 AIP-193 的「多給機器可讀細節」形成張力的兩端 —— 全 generic 讓 consumer 完全無法自助、全細節變成攻擊偵察面。AIP-193 的 (reason, domain) 設計正是中間路線：給分支用的機器可讀識別符、不洩內部實作。這組對撞是「provider 該暴露什麼」的邊界討論骨架、對應 backend/07 的攻擊面思路。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>11.11 錯誤鏈傳播章「暴露的下限：安全邊界」段（與 C75 對照）、連 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 安全</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/11-api-design/cases/" data-link-title="模組十一案例庫：API 設計與對外契約" data-link-desc="API 風格流派、版本與相容、介面語意、規範治理的已驗證公開案例集；含反例與覆蓋缺口標明">模組十一案例庫</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html">Error Handling Cheat Sheet（OWASP Cheat Sheet Series）</a> — 一手、現行版。已 WebFetch 驗證。</li>
</ul>
<h2 id="二手來源與狀態標注">二手來源與狀態標注</h2>
<p>該頁完全沒提 error ID / correlation id 回傳給使用者 ——「generic message + 附 trace id 供回報」這個常見組合不能掛 OWASP 出處：trace id 部分引 C76（W3C Trace Context）、組合本身標明是常見實務而非 OWASP 規範。</p>
]]></content:encoded></item></channel></rss>