LightUp 第三方 API 契約草案・端點尚未上線 v1.0.0-draft · a9c0c2b0d98a

錯誤・限流・重試・冪等

每個回應都有 x-request-id

成功與失敗都有。它是:

把它記進你的 log,對照你自己的 trace id。

錯誤形狀

所有錯誤共用一個極簡的 problem 物件(application/problem+json):

{
  "error": "revision_conflict",
  "message": "the resource was modified; re-read and retry",
  "requestId": "req_01j8m4k2r7e9v3xq5w8n0tzh6p"
}

穩定代碼清單

下表由契約產生Problem.error 是封閉集合,Gateway 內部與下游的錯誤碼是它的 超集,會被折成這些公開代碼,不外流。

error 狀態 意思 怎麼修
invalid_token 401 token 本身不可用:沒帶、無效、過期、簽章錯、audisstyp 不符
client_unknown 403 這個 client id 不存在 重新登記 app
client_disabled 403 client 存在但已被停用(status=disabled)。client_unknown 分開:前者是「被關掉了」,後者是「從來沒有」,處理方式不同 tenant 管理員重新啟用。停用對在途 token 也立即生效——下一個請求就擋
actor_unknown 403 token 沒有指出誰在操作:selfactor_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 範圍——delegatedshared 落在這裡 改用本人(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 的有界重取失敗

ℹ️ delegatedshared profile 的 token 不在這裡被擋:它們的簽章 是對的,會走完驗證再由授權決定拒絕,所以拿到的是 403 profile_not_supported

⚠️ tenant_mismatchno_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/使用者層。兩層都可能觸發,回應不會告訴你是哪一層

冪等

操作 冪等性
所有 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,所以重試不會重複扣量——但這是平台的計量保證,不是你的寫入語意保證。