維運與疑難排解
錯誤處理策略
錯誤回應物件、錯誤結構化資料、重試判準,以及客戶端處理樣板
錯誤回應物件
{
"error": {
"code": "ERR_NOT_FOUND",
"message": "Exam 'exam_9x8y7z' not found",
"details": {
"resource_type": "exam",
"resource_id": "exam_9x8y7z"
},
"request_id": "req_01j9f3k2m1"
}
}| 欄位 | 說明 |
|---|---|
code | 錯誤識別碼 |
message | 錯誤說明 |
details | 選填的結構化脈絡資料,形態依代碼而定(見下節) |
request_id | 伺服器指派的請求 id |
發生錯誤時,請記錄該請求回傳的 request_id,以提供技術支援追蹤查詢。
錯誤結構化資料
錯誤物件中的 details,依錯誤類別採用其中一種形態。
驗證錯誤
適用 ERR_VALIDATION_FAILED 錯誤,於請求內容不符合驗證條件時產生,錯誤結構化資料內容為陣列:
{
"error": {
"code": "ERR_VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{ "field": "responses[0].answer", "message": "Answer is required" }
],
"request_id": "req_01j9f3k2m1"
}
}field 為出錯輸入的 JSON 路徑。
脈絡錯誤
適用其餘所有錯誤代碼,採物件形態:
{
"error": {
"code": "ERR_EXAM_LOCKED",
"message": "Exam content is immutable; create a new exam instead",
"details": { "exam_id": "exam_abc123", "fields": ["questions"] },
"request_id": "req_01j9f3k2m1"
}
}重試請求
依照錯誤代碼類別,部分錯誤的請求可以重試。
| 類別 | 代碼 | 處理方式 |
|---|---|---|
| 可重試,定時 | ERR_RATE_LIMIT_EXCEEDED、ERR_SERVICE_UNAVAILABLE | 等待 Retry-After 標頭或 details.retry_after 秒數後重試 |
| 可重試,退避 | ERR_TIMEOUT、ERR_INTERNAL、非 JSON 的 502 / 504 | 指數退避,限制嘗試次數 |
| 不可重試,應修正請求 | ERR_VALIDATION_FAILED、ERR_INVALID_JSON、ERR_CONTENT_TOO_LONG、ERR_BATCH_TOO_LARGE、ERR_PAYLOAD_TOO_LARGE、ERR_UNSUPPORTED_MEDIA_TYPE | 修正輸入;原樣重送的結果不會改變 |
| 不可重試,應修正憑證 | ERR_UNAUTHORIZED、ERR_API_KEY_DISABLED、ERR_FORBIDDEN、ERR_INVALID_SCOPE | 調整金鑰、scope 或來源網段 |
| 不可重試,應檢查狀態 | ERR_NOT_FOUND、ERR_CONFLICT、ERR_DUPLICATE、ERR_DUPLICATE_TXID、ERR_EXAM_LOCKED、ERR_EXAM_ARCHIVED | 重新讀取資源並解決狀態問題,不要盲目重試 |
客戶端處理樣板
async function callNorma(
url: string,
init: RequestInit,
attempt = 0,
): Promise<unknown> {
const res = await fetch(url, init);
if (res.ok) return res.json();
const { error } = await res.json();
switch (error.code) {
case 'ERR_RATE_LIMIT_EXCEEDED':
case 'ERR_SERVICE_UNAVAILABLE': {
const wait = Number(
res.headers.get('Retry-After') ?? error.details?.retry_after ?? 60,
);
await sleep(wait * 1000);
return callNorma(url, init, attempt + 1); // 沿用同一把 Idempotency-Key
}
case 'ERR_TIMEOUT':
case 'ERR_INTERNAL':
if (attempt < 3) {
await sleep(2 ** attempt * 1000);
return callNorma(url, init, attempt + 1); // 沿用同一把 Idempotency-Key
}
throw new ApiError(error);
case 'ERR_VALIDATION_FAILED':
throw new ValidationError(error.details); // [{ field, message }]
case 'ERR_DUPLICATE_TXID':
// 客戶端問題:同鍵不同內容,不可視為成功
throw new ApiError(error);
default:
throw new ApiError(error); // error.request_id 供記錄與技術支援使用
}
}在上述範例中,init 在各次重試之間必須完全不變,包含 Idempotency-Key 標頭,以確保事件重送及重播正常。