Node/Fetch 超时与重试策略(最小可用)
TL;DR
- 没有超时的请求 = 迟早会卡死在生产上
- 超时用
AbortController - 重试要区分“可重试 / 不可重试”,并做指数退避(backoff)
1) fetch + timeout(AbortController)
ts
export async function fetchWithTimeout(
input: RequestInfo | URL,
init: RequestInit & { timeoutMs?: number } = {}
) {
const { timeoutMs = 10_000, ...rest } = init
const ctrl = new AbortController()
const t = setTimeout(() => ctrl.abort(), timeoutMs)
try {
return await fetch(input, { ...rest, signal: ctrl.signal })
} finally {
clearTimeout(t)
}
}注意:
AbortError需要在上层识别并做“超时”分类- Node 版本不同对 fetch 支持不同(若用 undici/axios 同理:总之要有 timeout)
2) 一个“可控”的重试封装(指数退避)
ts
type RetryOptions = {
retries?: number
baseDelayMs?: number
maxDelayMs?: number
jitter?: boolean
shouldRetry?: (err: unknown, attempt: number) => boolean
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
export async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions = {}) {
const {
retries = 3,
baseDelayMs = 200,
maxDelayMs = 2000,
jitter = true,
shouldRetry = () => true
} = opts
let lastErr: unknown
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn()
} catch (err) {
lastErr = err
if (attempt === retries) break
if (!shouldRetry(err, attempt)) break
const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt)
const delay = jitter ? Math.round(exp * (0.7 + Math.random() * 0.6)) : exp
await sleep(delay)
}
}
throw lastErr
}3) shouldRetry 怎么写(经验规则)
通常可以重试:
- 网络抖动:
ECONNRESET/ETIMEDOUT/EAI_AGAIN - 5xx(上游服务异常)
- 429(限流)—— 但最好配合
Retry-After
通常不要重试:
- 4xx(请求本身问题:401/403/404/422…)
- 非幂等写操作(POST 创建订单)—— 除非你做了幂等键(Idempotency-Key)
示例:
ts
function shouldRetry(err: any) {
const code = err?.code
if (code && ['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN'].includes(code)) return true
return false
}4) 一起使用(示例)
ts
const res = await withRetry(
() => fetchWithTimeout('https://example.com/api', { timeoutMs: 5000 }),
{ retries: 2, shouldRetry }
)