1237 字
6 分鐘

Promise.any 搶到錯誤回應?先驗證 HTTP 與 JSON 再選最快結果

同一份唯讀資料有兩個鏡像,你用 Promise.any(urls.map(url => fetch(url))) 想拿最快的可用結果,卻拿到 503。原因是 Promise.any 選第一個 fulfilled,而 fetch 收到 HTTP 錯誤狀態仍可 fulfilled。先回傳 Response,並不代表內容可以顯示。

要選「第一份可用資料」,每個候選 Promise 都必須包含狀態碼檢查、本文讀取與資料驗證。等這些都通過才 fulfilled;這樣最快抵達的錯誤頁才不會搶先勝出。

Promise.any 的成功條件,由傳入的工作決定#

Promise.any 的規則是等待第一個 fulfilled;較早的 rejection 不會讓整批立刻失敗。Promise.race 則採第一個 settled,包含失敗。換成 race 無法解決錯誤頁勝出的問題,還會讓最快的網路錯誤直接結束整批等待。

三個邊界要放在同一份候選工作內:

  • response.ok:拒絕非 2xx 回應。若產品只接受特定狀態,例如必須有本文的 200,應再縮小條件。
  • await response.json():等本文完整讀取、解析成功,不能拿到標頭就宣布勝出。
  • 資料驗證:合法 JSON 也可能是 {"error":"unavailable"},仍要確認需要的欄位。

下面是文章自己的 helper,僅示範可公開讀取、內容等價的 JSON 鏡像。它要求 title 是非空白字串,回傳來源 URL 方便追查;正式資料還應依 schema 與版本檢查完整性。

async function readCandidate(url) {
const response = await fetch(url, {
signal: AbortSignal.timeout(3_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (typeof data?.title !== 'string' || data.title.trim() === '') {
throw new TypeError('title must be a non-empty string');
}
return { url, data };
}

每次呼叫建立自己的 timeout signal;本文讀取仍使用該請求的 signal。3 秒是 fixture 的上限,正式服務應另訂時間預算。對本文取消與 timeout reason 有需求時,可接著看 fetch timeout 與 AbortSignal 的生命週期。

用本機伺服器重現 503、錯誤格式與有效資料#

把上面的 helper 和下面程式存成同一份 any-check.mjs,執行 node any-check.mjs。fixture 刻意讓有效資料等待 gate;兩個不合格候選處理完才放行,避免測試依賴網路速度或毫秒競賽。

import assert from 'node:assert/strict';
import http from 'node:http';
import { once } from 'node:events';
let release;
const gate = new Promise(resolve => { release = resolve; });
const server = http.createServer(async (req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/good') {
await gate;
res.end('{"title":"可用資料"}');
} else if (req.url === '/status') {
res.statusCode = 503;
res.end('{"error":"unavailable"}');
} else if (req.url === '/syntax') {
res.end('not json');
} else {
res.end('{"error":"wrong shape"}');
}
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const base = `http://127.0.0.1:${server.address().port}`;
try {
// HTTP 503 本身不會讓 fetch reject。
const raw = await fetch(base + '/status');
assert.equal(raw.ok, false);
assert.equal(raw.status, 503);
await raw.text();
const urls = ['/good', '/status', '/shape'].map(path => base + path);
const jobs = urls.map(readCandidate);
let settled = false;
const winner = Promise.any(jobs).finally(() => { settled = true; });
await Promise.allSettled(jobs.slice(1));
assert.equal(settled, false);
release();
assert.deepEqual(await winner, {
url: base + '/good', data: { title: '可用資料' },
});
await Promise.allSettled(jobs);
await assert.rejects(Promise.any([
readCandidate(base + '/status'),
readCandidate(base + '/syntax'),
readCandidate(base + '/shape'),
]), error => {
assert.ok(error instanceof AggregateError);
assert.equal(error.errors.length, 3);
assert.equal(error.errors[0].message, 'HTTP 503');
assert.ok(error.errors[1] instanceof SyntaxError);
assert.ok(error.errors[2] instanceof TypeError);
return true;
});
await assert.rejects(Promise.any([]), error => {
assert.ok(error instanceof AggregateError);
assert.deepEqual(error.errors, []);
return true;
});
console.log('HTTP、解析、格式、全部失敗與空清單檢查通過');
} finally {
release();
server.closeAllConnections();
await new Promise(resolve => server.close(resolve));
}

這份刊登範例由內容維護流程於 2026-10-25 在 Node.js v24.13.0 執行。它驗證候選資料與錯誤分類,沒有量測鏡像加速效果,也沒有驗證瀏覽器、CORS、認證或正式 API。

全部失敗時,依來源保存 errors#

ECMAScript 規格以輸入索引保存 rejection reason。因此 AggregateError.errors[0] 對應第一個候選,並非最早出錯的來源。記錄時把原本的 URL 清單配回同索引,保留 HTTP、解析與 schema 錯誤的差別,不要只顯示 AggregateError 的通用 message。

空清單也會 reject,errors 為空;如果「沒有設定鏡像」屬於設定錯誤,應在建立請求前單獨回報。若某個候選一直 pending,且其他候選全失敗,any 也會繼續等,所以每個工作都需要適用的結束條件。

有了勝出者,其他請求仍可能繼續#

Promise.any 不會取消剩餘操作。範例以每次請求自己的 timeout 限制等待,但沒有在勝出後主動 abort 其餘請求;fixture 會等待本批 jobs 全部 settled 再釋放伺服器。若正式產品需要節省流量,可為各候選配置 controller,取得有效本文後取消剩餘工作,並另外驗證取消、清理與錯誤紀錄。

不要把這個模式套在建立訂單、扣款或其他有副作用的 POST:同時送出多份操作,拿到一份成功不會撤銷其餘操作。即使是唯讀鏡像,也要先確認內容版本等價、目的地允許存取,以及多份請求不會超出配額。需要呈現每個來源各自的結果時,應改看 allSettled 的部分成功處理;需要釋放共用資源,則先處理 Promise.all 失敗後仍在執行的工作。

參考資料:

MDN:Promise.any()

MDN:Using the Fetch API

ECMAScript:Promise.any

Promise.any 搶到錯誤回應?先驗證 HTTP 與 JSON 再選最快結果
https://laplusda.com/posts/javascript-promise-any-valid-response/
作者
Zero
發佈於
2026-10-25
許可協議
CC BY-NC-SA 4.0