个人站点的评论、点赞、订阅接口都会限流。多数前端把它们一律渲染成“操作失败,请重试”,于是用户对着一个“再试也没用”的按钮反复点击。限流响应的正确处理,是按状态码分流,而不是统一报错。
先看服务端给了什么信号
一个设计良好的接口,不同失败会给不同状态码 + 可选的重试提示:
| 状态码 | 含义 | 前端应做 |
|---|---|---|
| 400 | 输入无效(用户可修复) | 提示改输入,不自动重试 |
| 429 | 触发限流(等待可恢复) | 读 Retry-After,禁用提交并倒计时,可选自动退避重试 |
| 503 | 服务暂不可用(用户无法修复) | 提示稍后再试 / 换渠道,不误导用户改输入 |
Retry-After 头(429/503 常带)是服务端明确告诉你“多久后再来”:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
分流处理
async function submit(payload) {
const res = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(8000), // 关键:超时兜底,别让它无限挂着
});
const data = await res.json().catch(() => ({}));
if (res.status === 429) {
const wait = parseInt(res.headers.get('Retry-After') || '30', 10);
startCountdown(wait); // 禁用按钮 + 倒计时,防继续点
return show('操作太频繁,请 ' + wait + ' 秒后再试。');
}
if (res.status >= 500) {
return show(data.message || '服务暂时不可用,请稍后重试。'); // 不背锅给用户输入
}
if (!res.ok) {
return show(data.message || '提交的内容似乎有问题,请检查。'); // 4xx:用户可修复
}
return ok(data);
}
要点:
signal: AbortSignal.timeout(...)是第一道防线——没有它,接口一卡,按钮永久停在“正在提交…”,比任何文案错误都伤体验;- 透传服务端
message优先于前端硬编码(服务端最懂真实原因); - 429 要消费
Retry-After,禁用 + 倒计时,把“你别再点了”变成可见的确定信息; - 5xx 不翻译成输入错误——这是真实踩过的坑(详见踩坑《订阅接口 503 被前端翻译成了“邮箱格式不对”》)。
要不要自动重试?
- 幂等请求(GET 拉评论数)+ 5xx/网络错:可指数退避重试,上限 2–3 次;
async function withBackoff(fn, retries = 3) {
for (let i = 0; i < retries; i += 1) {
try {
return await fn();
} catch (e) {
if (i === retries - 1) throw e;
await new Promise((r) =>
setTimeout(r, 300 * 2 ** i + Math.random() * 100),
); // 加抖动
}
}
}
- 非幂等提交(POST 点赞/订阅):不要盲目自动重试,除非服务端支持幂等键。重复点击 + 自动重试 = 重复提交。宁可用户手点一次明确的“重试”。
- 退避要加抖动(jitter),否则多客户端会在同一时刻同步重试,把刚恢复的服务再打倒。
文案的三分法
给用户的错误信息按“下一步能做什么”分类,而不是按技术原因分类:
- 你能修的:说清楚改哪里(“昵称不能为空”),聚焦到出错字段;
- 等会儿就行的:给确定时间锚点(“30 秒后可再试”),别写“稍后再试”这种无信息量话;
- 你修不了、也不该你扛的:诚实说明是服务端问题 + 给替代渠道(“也可用 RSS 关注”),绝不暗示是用户操作失误。
小结
限流不是一个错误码,是一组需要区别对待的信号:超时要有 AbortSignal、429 要读 Retry-After 并禁用倒计时、5xx 要如实归因、幂等才可退避重试。把“操作失败请重试”拆成这三类可执行提示,接口的限流机制才第一次真正为用户所用,而不是只服务于服务端自保。