錯誤・限流・重試・冪等
每個回應都有 x-request-id
成功與失敗都有。它是:
- 你回報問題時必附的查詢鍵;
- 平台計量的 idempotency key——所以同一個請求重試不會被重複計量。
把它記進你的 log,對照你自己的 trace id。
錯誤形狀
所有錯誤共用一個極簡的 problem 物件(application/problem+json):
{
"error": "revision_conflict",
"message": "the resource was modified; re-read and retry",
"requestId": "req_01j8m4k2r7e9v3xq5w8n0tzh6p"
}
error是穩定的機器可讀代碼。程式判斷請用它。message是給人看的,隨時可能改字,不要拿它做判斷。它也刻意不含「資源是否存在」的線索、內部路徑或任何秘密。
穩定代碼清單
下表由契約產生:Problem.error 是封閉集合,Gateway 內部與下游的錯誤碼是它的
超集,會被折成這些公開代碼,不外流。
error |
狀態 | 意思 | 怎麼修 |
|---|---|---|---|
invalid_token |
401 | token 本身不可用:沒帶、無效、過期、簽章錯、aud/iss/typ 不符 |
|
client_unknown |
403 | 這個 client id 不存在 | 重新登記 app |
client_disabled |
403 | client 存在但已被停用(status=disabled)。與 client_unknown 分開:前者是「被關掉了」,後者是「從來沒有」,處理方式不同 |
tenant 管理員重新啟用。停用對在途 token 也立即生效——下一個請求就擋 |
actor_unknown |
403 | token 沒有指出誰在操作:self 缺 actor_account_id,或 workload 帶的 actor_account_id 不等於 sub |
重新取一張 token。這代表簽發端有問題,請附 x-request-id 回報 |
tenant_suspended |
403 | 租戶被停權。與呼叫端是誰、請求哪一項能力都無關——整個租戶的第三方存取都停,讀寫皆然 | 租戶要先恢復。重試沒有用;即使換一張 token 也一樣,簽發端同樣會拒 |
client_not_third_party |
403 | 這個 client 不是第三方類別(是第一方或內部工具) | 平台端問題;第三方不該拿到這種 client |
profile_not_supported |
403 | token 的 profile 不在 v1 範圍——delegated 與 shared 落在這裡 |
改用本人(self)或機器(workload)token;代理要等 v2 |
subject_revoked |
403 | service account 或使用者 session 已被撤銷/停用 | tenant 管理員重新啟用,或請使用者重新登入 |
epoch_changed |
403 | token 的 actor_epoch 與帳號現值不符——帳號被撤權過 |
重新取一張 token |
capability_not_granted |
403 | tenant 沒把這項能力授權給這個 app | tenant 管理員在後台加授權 |
plan_capability_excluded |
403 | 租戶方案不含這項能力 | 升級方案 |
capability_not_in_token |
403 | 能力有被授權,但這張 token 沒帶這個 scope | 換一張帶足 scope 的 token |
capability_unavailable |
403 | 這項能力在本部署尚未開放(目錄狀態不是可呼叫) | 看文件的可用狀態,等它上線 |
resource_type_mismatch |
403 | 能力宣告的資源型別與你打的資源對不上 | 修正呼叫 |
tenant_mismatch |
403 | 資源不屬於這張 token 的租戶 | 修正呼叫 |
no_relationship |
403 | 租戶對這個人沒有有效關係 | 修正呼叫,或先建立關係 |
forbidden |
403 | 兜底:Amygdala 回了本版 Gateway 不認識的 deny 代碼。仍然是拒絕,不會變成放行,也不會變成可重試的 503 | 回報並附上 x-request-id |
not_found |
404 | 資源不存在,或存在但呼叫端連它存不存在都不該知道。 | |
revision_conflict |
409 | 送出的 If-Match 與現行 ETag 不符(有人搶先寫了)。 重新 GET、合併、帶新的 ETag 重送。 |
|
already_exists |
409 | 送了 If-None-Match: * 要建立,但它已經存在。 改成 GET 讀出現行 ETag,再用 If-Match 更新。 |
|
payload_too_large |
413 | 單筆 data 超過 16 KiB。縮小內容,或把大東西放在你自己的資料庫 |
|
quota_exceeded |
422 | tenant reached 50000 records for this extension | |
schema_invalid |
422 | data does not match the registered extension schema | |
precondition_required |
428 | 這個寫入沒有表明它的前置條件(RFC 6585 §3)。 | |
rate_limited |
429 | 限流。限流有兩層:tenant 與 service account/使用者 | |
authorization_unavailable |
503 | 拿不到授權決定——Amygdala 不可用、逾時,或下游資料面不可用 | |
token_verification_unavailable |
503 | 驗簽就做不完——JWKS 取不到,或 unknown kid 的有界重取失敗 |
ℹ️
delegated與sharedprofile 的 token 不在這裡被擋:它們的簽章 是對的,會走完驗證再由授權決定拒絕,所以拿到的是 403profile_not_supported。
⚠️
tenant_mismatch與no_relationship打在 person 資源上時會變成 404, 不是 403——否則 403 與 404 的差別本身就是一支「這個 person 存不存在」的 探測器(ADR-004 D9)。不透露資源是否存在是刻意的。
ℹ️
tenant_suspended不折成 404,即使打在 person 資源上也一樣回 403。 它講的是你的租戶的狀態,不是目標資源存不存在,所以據此推不出 任何關於那個 person 的事——沒有要遮蔽的東西。
403 的代碼逐字來自授權決定。 它們告訴你要去修哪裡:client_disabled 是被停用
(不是不存在,那是 client_unknown);actor_unknown 表示 token 沒指出誰在操作,
那是簽發端的問題,請附 x-request-id 回報;forbidden 是兜底——新版授權端送來這一版
Gateway 不認識的代碼時用它,仍然是拒絕,不會變成放行,也不會變成可重試的 503。
狀態碼語意
| 狀態 | 何時 |
|---|---|
| 401 | 沒帶 token,或 token 無效、過期、簽章錯、aud/iss/typ 不符 |
| 403 | 驗證通過但授權拒絕 |
| 404 | 資源不存在,或存在但這個 caller 連「存在與否」都不該知道 |
| 409 | 寫入衝突:If-Match 與現行 revision 不符,或 If-None-Match: * 但已存在 |
| 413 | 單筆 metadata 超過 16 KiB |
| 422 | 額度用盡(筆數/extension 數)、schema 不符 |
| 428 | 寫入沒帶前置條件標頭,或 PUT 同時帶了 If-Match 與 If-None-Match |
| 429 | 限流(tenant + account 雙層,計畫 §7) |
| 503 | 現在判不了 |
404 同時是「沒有」與「不是你的」
tenant_mismatch(資源不屬於這張 token 的租戶)與 no_relationship(租戶對這個人
沒有有效關係)打在 person 資源上時回 404,不是 403。
這是刻意的:如果「不存在」回 404 而「存在但不是你的」回 403,那麼 403 與 404 的差別
本身就是一支「這個 person 存不存在」的探測器。所以兩者回同一個答案,message 也不
透露線索。不要用狀態碼去推斷某個 id 是否存在——那正是這條規則要擋的事。
tenant_suspended 不折成 404,打在 person 資源上也照樣回 403。它講的是你的租戶
的狀態,不是目標資源存不存在,據此推不出任何關於那個 person 的事——沒有要遮蔽的東西。
403 與 503 永遠不一樣
這是本 API 最重要的一條區分:
| 403 | 503 | |
|---|---|---|
| 意思 | 不准 | 現在判不了 |
| 重試 | 沒有意義 | 應該重試(指數退避) |
| 怎麼修 | 修 grant、方案或資源政策 | 等待;平台 fail-closed,不會降級放行 |
平台不會因為授權服務故障就只驗簽放行。 判不了就不放行。
重試策略
| 狀態 | 重試? | 做法 |
|---|---|---|
| 401 | 一次 | 換新 token 再試一次;仍失敗就停 |
| 403 | 否 | 修授權或降級功能 |
| 404 | 否 | 確認 id 來自平台,不是自己拼的。也可能是「不是你的」——見下 |
| 409 | 是(但要先重讀) | 重新 GET、合併、帶新的 ETag 再送 |
| 428 | 否 | 你沒表明前置條件:建立帶 If-None-Match: *,更新帶 If-Match |
| 413 | 否 | 縮小內容 |
| 422 | 否 | 額度或 schema 問題,見 Metadata |
| 429 | 是 | 依 Retry-After,再加抖動 |
| 503 | 是 | 指數退避+抖動,設上限。兩個代碼都可重試:authorization_unavailable(拿不到授權決定)與 token_verification_unavailable(驗簽做不完) |
| 網路逾時 | 是(依冪等性) | 見下一節 |
退避建議:起始 1 秒,倍增至上限 60 秒,加上 ±20% 抖動,總重試次數設上限。不要無限重試——故障迴圈會把你自己和平台一起拖垮,而且會在限流層被擋。
限流
限流有兩層:租戶層與 service account/使用者層。兩層都可能觸發,回應不會告訴你是哪一層。
- 看
Retry-After再重試。 x-ratelimit-remaining是資訊性欄位,不是契約的一部分,不要拿它當唯一的流量控制依據。- 合法的大量同步不會被丟棄,只會被要求放慢。需要更高額度請升級方案——用更多帳號繞過沒有用,租戶層還是會撞。
冪等
| 操作 | 冪等性 |
|---|---|
所有 GET |
天然冪等 |
PUT extension(更新) |
帶同一個 If-Match 重送:第一次成功後 ETag 已變,第二次會得到 409 revision_conflict。要安全重試就重讀再送。 |
PUT extension(建立) |
帶 If-None-Match: * 重送:第二次得到 409 already_exists,不會覆蓋。這正是安全的——重試不會蓋掉別人 |
DELETE extension |
冪等:已經不存在也回 204 |
網路逾時而不知道寫入是否成功時:重讀(GET)確認目前的 ETag 與內容,再決定要不要重送。不要盲目重送一個寫入。
計量以 x-request-id 為 idempotency key,所以重試不會重複扣量——但這是平台的計量保證,不是你的寫入語意保證。