API请求超时?我翻了15个案例总结的
一、超时到底是怎么发生的
在说解决方案之前,先搞清楚超时从哪来。总结 15 个项目案例,超时的根因无非这几类:
🔍 常见超时场景
网络层:用户弱网(地铁、电梯)、跨区跨境延迟(200ms → 5s)、DNS 解析慢、TCP 握手卡住
服务端层:数据库慢查询(没索引,扫全表)、外部 API 调用超时(第三方支付/短信)、连接池耗尽(高并发)、GC 暂停(老年代大对象)
代理层:Nginx/Koa/Express 默认 timeout 太长或根本没配、负载均衡器 idle timeout
客户端层:axios 默认没设 timeout、fetch 没配 AbortController、请求发了就收不回来
关键认知:超时不是单点问题,是整条链路的短板决定上限。你前端 retry 三次,但 Nginx 5 秒就断了,看起来"重试"没生效。所以超时处理要从前端→网关→后端→数据库全链路一起配,缺一不可。
二、前端处理方案
1. axios 配置 timeout — 最基础的保命项
很多人 axios 用了一年,timeout 还是默认的 0(永不超时)。这是最常见的坑。
// ❌ 危险:永不超时,请求卡死用户无感知
const api = axios.create({ baseURL: 'https://api.example.com' });
// ✅ 正确:根据业务场景设 timeout
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 8000, // 8 秒,大多数接口够用
timeoutErrorMessage: '请求超时,请检查网络后重试'
});
// 全局错误拦截里区分超时
api.interceptors.response.use(
res => res,
err => {
if (axios.isAxiosError(err)) {
if (err.code === 'ECONNABORTED' || err.message.includes('timeout')) {
console.error('⏰ 请求超时:', err.config?.url);
}
}
return Promise.reject(err);
}
);
注意:axios 的 timeout 指的是从发请求到收到响应的时间,不包括 DNS 解析和 TCP 握手。所以 8 秒看起来够用,实际链路可能已经 12 秒了。
2. AbortController — 手动取消请求的标配
fetch 默认不支持取消,但现代浏览器都支持 AbortController。用好它,你可以做到:请求超过 5 秒自动取消、页面切换时取消上一个列表请求、用户点取消按钮即时终止。
class HttpService {
private controller = new AbortController();
private timer: ReturnType<typeof setTimeout>;
request(url: string, timeoutMs = 6000) {
// 超时控制器
this.controller = new AbortController();
this.timer = setTimeout(() => this.controller.abort(), timeoutMs);
return fetch(url, { signal: this.controller.signal })
.then(res => {
clearTimeout(this.timer);
return res.json();
})
.catch(err => {
clearTimeout(this.timer);
if (err.name === 'AbortError') {
throw new Error(`请求超时(${timeoutMs}ms)`);
}
throw err;
});
}
cancel() {
this.controller.abort();
clearTimeout(this.timer);
}
}
// 使用
const http = new HttpService();
// 页面切换时取消
onMount(() => {
fetchList();
});
onUnmounted(() => {
http.cancel();
});
3. 重试机制 — exponential backoff 是核心
超时不等于失败,很多临时抖动重试一次就好了。但绝对不要无脑重试(立即重试三次),只会把已经压力很大的服务端打爆。标准做法是 指数退避 + 随机抖动。
async function fetchWithRetry(
fn: () => Promise<any>,
maxRetries = 3,
baseDelayMs = 1000
): Promise<any> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
// 最后一次尝试不等待
if (attempt === maxRetries) break;
// 指数退避:1s → 2s → 4s
const delay = baseDelayMs * Math.pow(2, attempt);
// 加随机抖动,避免多客户端同时重试造成惊群效应
const jitter = delay * (0.5 + Math.random() * 0.5);
console.warn(`⏳ 第 ${attempt + 1} 次失败,${Math.round(jitter)}ms 后重试...`);
await sleep(jitter);
}
}
throw lastError!;
}
// 配合 axios 的用法
const api = axios.create({ baseURL: BASE_URL, timeout: 5000 });
const res = await fetchWithRetry(
() => api.get('/user/profile'),
3,
1000
);
5xx 和网络错误值得重试。4xx(401、403、422)重试没有意义,只会让问题更糟。重试前判断错误类型。
// 判断是否可重试
function isRetryable(err: any): boolean {
if (!err.response) return true; // 网络错误,可重试
const status = err.response?.status;
return status >= 500 || status === 429; // 服务器错误或限流
}
4. 请求队列 + 并发控制
大量请求同时发出时,超时会呈指数级放大。不是所有请求都要第一时间发出去的。做一个请求队列,限制并发数,既能保活又能提升整体成功率。
class RequestQueue {
private queue: Array<() => Promise<any>> = [];
private running = 0;
private readonly limit: number;
constructor(limit = 4) {
this.limit = limit;
}
add(fn: () => Promise<any>) {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await fn();
resolve(result);
} catch (e) {
reject(e);
}
});
this.process();
});
}
private process() {
if (this.running >= this.limit) return;
const task = this.queue.shift();
if (!task) return;
this.running++;
task().finally(() => {
this.running--;
this.process();
});
}
}
三、后端处理方案
1. Nginx 超时配置 — 最容易被忽略的瓶颈
后端接口再快,Nginx 超时不配好,一切白搭。15 个案例里,有 4 个项目的超时问题根因都在 Nginx 配置。这里给一个生产可用的配置模板:
# Nginx 超时配置(server 区块内)
# 读取上游响应超时(接口处理完成后数据传给客户端的时间)
proxy_read_timeout 30s;
# 向 upstream 发送请求的超时(还没到接口逻辑,Nginx 等待连接的时间)
proxy_connect_timeout 10s;
# 发送请求体到 upstream 的超时
proxy_send_timeout 30s;
# 客户端请求超时(读请求头 + 请求体)
client_body_timeout 10s;
client_header_timeout 10s;
# keepalive 保持连接不断开
proxy_http_version 1.1;
proxy_set_header Connection "";
# 失败重试到另一台 upstream
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_read_timeout 指的是 Nginx 等待上游响应的总时间,不是单次数据传输时间。如果你的接口正常要 20 秒处理,就设 30s 以上,否则到 20 秒 Nginx 会主动断开,后端还在跑,白浪费资源。
2. 接口超时设计 — 从根上控制
服务端要给每个接口设一个"业务超时",不是物理超时,而是业务允许的最大等待时间。超过这个时间,接口直接返回超时错误,而不是让请求继续消耗资源。
// Node.js / Express:给每个路由加超时中间件
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
const withTimeout = (ms, fallbackMsg = '接口处理超时') => {
return (req, res, next) => {
const timer = setTimeout(() => {
console.warn(`⏰ 接口超时: ${req.path}`);
res.status(504).json({ code: 504, msg: fallbackMsg });
}, ms);
res.on('finish', () => clearTimeout(timer));
};
};
// 用法
app.get('/api/export', withTimeout(15000), asyncHandler(async (req, res) => {
const data = await heavyQuery();
res.json({ data });
}));
// Java Spring Boot:全局超时配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer config) {
config.setDefaultTimeout(8000); // 8 秒全局超时
}
}
// 或者注解方式
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
}
)
public String slowMethod() {
return externalApi.call();
}
3. 慢查询 — 超时最常见的根因
接口超时 80% 的情况是数据库慢查询。一个没建索引的 SQL,在百万级表上能跑 30 秒。处理方案:
-- 1. 慢查询日志定位(MySQL)
slow_query_log = 1
long_query_time = 2 -- 超过 2 秒的记录
slow_query_log_file = /var/log/mysql/slow.log
-- 2. EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'paid' ORDER BY created_at DESC;
-- 3. 给高频查询加索引
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- 4. 超时熔断:单次查询超过 5 秒直接取消
SET MAX_EXECUTION_TIME = 5000; -- MySQL 8.0+
业务层面,给数据库操作加超时:
// Node.js + MySQL 加 timeout
const db = mysql.createPool({
connectionLimit: 20,
connectTimeout: 10000, // 连接建立超时 10s
// 每个查询不单独设 timeout,但配合后端的请求超时就够了
});
// 给查询加执行超时
async function safeQuery(sql: string, params: any[], timeoutMs = 5000) {
return Promise.race([
db.execute(sql, params),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('DB query timeout')), timeoutMs)
)
]);
}
四、降级与熔断 — 兜底思维
1. 为什么要熔断
想象这个场景:上游服务 A 开始变慢,超时率从 1% 升到 30%。你的前端疯狂重试,请求全打到你这里,你的服务也开始变慢,最终自己也崩了。熔断就是当上游失败率过高时,主动"断路",不再把请求发过去,直接返回降级结果,保护自己也保护上游。
2. 熔断器实现(TypeScript)
class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private readonly threshold = 5, // 失败 5 次就开路
private readonly timeout = 30000, // 30 秒后半开尝试
private readonly resetInterval = 60000 // 1 分钟后重置计数器
) {}
async call<T>(fn: () => Promise<T>, fallback: () => T): Promise<T> {
if (this.state === 'OPEN') {
const elapsed = Date.now() - this.lastFailureTime;
if (elapsed > this.timeout) {
this.state = 'HALF_OPEN'; // 半开,允许一个请求试试
console.log('🔄 熔断器半开');
} else {
console.warn('⚡ 熔断中,直接返回降级数据');
return fallback();
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
return fallback();
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
console.warn('🔴 熔断器打开!');
}
}
}
// 使用
const breaker = new CircuitBreaker(5, 30000);
const result = await breaker.call(
() => userService.getUser(openId),
() => ({ id: openId, name: '游客', avatar: '/default.png' }) // 降级兜底数据
);
3. 前端降级 — 服务降级三件套
超时发生时,前端不应该让用户看到空白或报错,而是给一个有意义的降级结果。
// 请求结果降级
async function fetchBanner() {
try {
const res = await api.get('/banner', { timeout: 3000 });
return { type: 'data', content: res.data };
} catch (e) {
// 降级:缓存优先,缓存也没有返回兜底
const cached = localStorage.getItem('banner_cache');
if (cached) {
return { type: 'cache', content: JSON.parse(cached) };
}
return {
type: 'fallback',
content: [{ id: 0, title: '网络不给力,请稍后再试', image: '/default-banner.jpg' }]
};
}
}
// 接口超时 → 显示骨架屏 + 重试按钮
// 第三方服务超时 → 用本地假数据先顶着
// 非核心接口 → 直接隐藏,不影响主流程
4. 接口超时设计的核心原则
作为后端工程师,你要给前端一个"可信"的接口契约:
- 接口最大响应时间写入 API 文档,超时返回 504,不返回 200 + 空数据
- 分页接口设 maxLimit,防止用户请求 10 万条数据导致超时
- 长任务用异步:导出、统计这种耗时操作,不要让前端同步等待,改用任务队列 + 查询进度接口
- 健康检查独立:别把监控探针和业务接口绑一起,健康检查要优先返回
# Nginx 健康检查(独立路径,不受业务超时影响)
location /health {
access_log off;
return 200 'ok';
add_header Content-Type text/plain;
}
# 大文件导出:走异步任务队列
# 前端: POST /api/export → 返回 { taskId: "xxx" }
# 前端: GET /api/export/xxx/status → { progress: 75, status: "running" }
# 前端: GET /api/export/xxx/download → 文件 URL
五、实战 checklist — 15 分钟自检清单
对照这个清单,检查你现在项目里的超时处理:
✅ 前端检查
□ axios 请求是否配置了合理的 timeout(5-10s)
□ 是否有统一的 AbortController 管理页面生命周期请求
□ 重试是否有指数退避 + 错误类型判断(5xx 重试,4xx 不重试)
□ 超时后是否有降级 UI(骨架屏、兜底数据、错误提示)
□ 高频请求是否做了并发控制(避免打爆服务端)
✅ 后端检查
□ Nginx proxy_read_timeout 是否覆盖接口最大处理时间
□ 慢查询日志是否开启,>2s 的 SQL 有没有优化计划
□ 核心接口是否加了业务超时限制(防止极端情况)
□ 是否有熔断器,防止故障级联
□ 长耗时操作是否改成了异步任务(导出、批量处理)
© CloverTools · 全栈开发工具站 · clovertools.cn
常见问题
A: 这类工具一般有明确的输入框和输出框,按提示输入内容,点击对应按钮即可得到结果。建议先用简单示例测试功能是否正常,再处理实际数据。
A: 根据具体工具类型决定。格式转换工具适合处理第三方数据,编码工具适合加密传输,压缩工具适合文件上传前处理。多积累工具使用经验,遇到问题时能快速判断用哪个工具解决。
A: 不同工具有不同侧重,重点是理解原理。可以同时安装多个类似工具,实际使用中对比效果,选择最顺手的一个。随着使用经验增加,你也能判断工具的好坏。