connect-timeout 工作原理、API 与配置一次 HTTP 调用可能同时存在很多超时:
客户端发送请求头 → server.headersTimeout
客户端发送完整请求体 → server.requestTimeout
Express 处理业务 → connect-timeout
响应后的连接复用等待 → server.keepAliveTimeout
前端愿意等待多久 → Axios timeout
connect-timeout 关注的是:
请求进入该中间件以后,在规定时间内是否开始发送响应。
它不是 Express 5 内置中间件,而是 Express 官方资源页收录的第三方中间件。
npm install connect-timeout
npm install --save-dev @types/connect-timeout
connect-timeout 本身不携带 TypeScript 声明,需要社区维护的 @types/connect-timeout。
当前项目采用 esModuleInterop 时通常可以这样导入:
import timeout from "connect-timeout";
如果项目没有开启相应的模块互操作配置,应根据 tsconfig.json 使用与当前模块配置匹配的导入方式。不要只为一个示例贸然切换整个工程到 ESM。
import express, {
type NextFunction,
type Request,
type Response,
} from "express";
import timeout from "connect-timeout";
const app = express();
app.get(
"/api/report",
timeout("10s"),
async (req: Request, res: Response) => {
const report = await createReport();
if (req.timedout) {
return;
}
res.json(report);
},
);
时间可以传毫秒数:
timeout(10_000);
也可以传给 ms 包能够识别的字符串:
timeout("500ms");
timeout("10s");
timeout("2m");
教程中更推荐显式字符串或带单位含义的常量,避免误把秒当成毫秒。
其核心逻辑可以简化为:
function timeoutMiddleware(delay: number) {
return (req: Request, res: Response, next: NextFunction) => {
const timer = setTimeout(() => {
req.timedout = true;
req.emit("timeout", delay);
}, delay);
req.timedout = false;
req.clearTimeout = () => clearTimeout(timer);
// 响应开始或结束后清除 timer。
// 真实源码使用 on-headers 和 on-finished。
next();
};
}
默认 respond: true 时,它还会监听 timeout 事件,并调用类似:
next(timeoutError);
这个错误具有:
error.status = 503
error.timeout = 本次超时毫秒数
中间件调用 next() 后,控制权已经交给后续中间件和 Route:
connect-timeout 启动 timer
↓
调用 next()
↓
Route 开始数据库查询
↓
timer 到期并传递 503 错误
↓
数据库查询仍由数据库驱动继续执行
JavaScript 没有一种通用机制,可以从外部强制取消任意 Promise。真正的操作是否停止,取决于数据库驱动、HTTP 客户端、文件 API 或业务函数是否支持取消。
底层使用 setTimeout()。如果事件循环被同步 CPU 任务阻塞:
while (true) {
// 阻塞事件循环
}
超时回调本身也没有机会运行。因此它无法解决同步死循环或长时间同步计算。CPU 密集任务应放到 Worker Thread、独立进程或任务队列。
源码使用 on-headers 和 on-finished 清理计时器。这意味着它主要约束“开始响应前的等待时间”,不是完整响应体传输的绝对总时间。
例如:
app.get("/stream", timeout("5s"), (req, res) => {
res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
});
// 响应头已经开始发送,connect-timeout 的 timer 会被清理。
// 后续流式传输可能持续很久。
});
SSE、文件下载和流式响应应使用各自的连接、空闲、心跳和生命周期策略,不能依赖 connect-timeout 控制整个传输时长。
respond默认配置:
timeout("10s", {
respond: true,
});
超时后中间件调用 next(error),由 Express 错误处理链返回响应。
设置为 false:
timeout("10s", {
respond: false,
});
这时中间件仍会:
req.timedout = true;timeout 事件;但不会自动把超时错误传给 next()。你必须自己监听事件并决定如何响应:
app.get(
"/api/task",
timeout("10s", { respond: false }),
(req, res, next) => {
req.once("timeout", () => {
if (!res.headersSent) {
res.status(503).json({
code: "REQUEST_TIMEOUT",
message: "请求处理超时",
});
}
});
next();
},
taskHandler,
);
一般情况下保留 respond: true,通过统一错误处理中间件格式化响应,更容易维护。
req.timedout 与 req.clearTimeout()req.timedoutif (req.timedout) {
return;
}
它只能告诉代码 timer 是否已经触发,不能证明底层任务已经取消。
req.clearTimeout()req.clearTimeout();
它会清除本次中间件设置的 timer,之后该 timer 不再触发。它不是“暂停”,也不能恢复剩余时间。
适合明确切换生命周期的场景,但不应随意调用,否则请求可能永久失去应用层 deadline。
haltOnTimedout 的作用官方示例使用:
function haltOnTimedout(
req: Request,
_res: Response,
next: NextFunction,
): void {
if (!req.timedout) {
next();
}
}
它只阻止尚未开始的下一个中间件继续执行:
app.use(timeout("5s"));
app.use(express.json());
app.use(haltOnTimedout);
app.use(loadCurrentUser);
app.use(haltOnTimedout);
app.use(apiRouter);
它无法打断已经进入的异步函数:
await slowDatabaseQuery();
所以 haltOnTimedout 是中间件流程防护,不是资源取消机制。
最简单的全局配置是:
app.use(timeout("10s"));
但官方明确不推荐在没有防护措施时把它作为顶层中间件,因为超时后后续流程可能继续运行。
全局使用通常需要同时满足:
req.timedout;不要先启动全局 10 秒 timer,再无条件叠加一个 60 秒 timer。两个 timer 是独立的,短的那个仍可能先触发。
可以根据路由分类选择一个预先创建的中间件:
const normalTimeout = timeout("10s");
const reportTimeout = timeout("60s");
function timeoutPolicy(
req: Request,
res: Response,
next: NextFunction,
): void {
if (req.path.startsWith("/api/reports/")) {
reportTimeout(req, res, next);
return;
}
normalTimeout(req, res, next);
}
app.use(timeoutPolicy);
但随着规则增加,路径判断会变得脆弱。更推荐按 Router 划分:
const normalApiRouter = express.Router();
const reportRouter = express.Router();
normalApiRouter.use(timeout("10s"));
normalApiRouter.use(haltOnTimedout);
reportRouter.use(timeout("60s"));
reportRouter.use(haltOnTimedout);
app.use("/api/reports", reportRouter);
app.use("/api", normalApiRouter);
每个请求只经过一个超时中间件,边界最清楚。
下面的写法不能可靠地覆盖全局 timeout:
app.use(timeout("10s"));
app.get(
"/api/reports/monthly",
timeout("60s"),
reportHandler,
);
因为全局 10 秒 timer 仍然存在,60 秒 timer 不会自动替换它。
const defaultTimeout = timeout("10s");
app.use("/api/reports", timeout("60s"), reportRouter);
app.use("/api", defaultTimeout, normalApiRouter);
要保证特殊 Router 被完整处理,不要在正常响应后继续 next() 进入普通 Router。
如果工程结构暂时无法调整,可以显式替换:
const longTimeout = timeout("60s");
function replaceWithLongTimeout(
req: Request,
res: Response,
next: NextFunction,
): void {
req.clearTimeout();
longTimeout(req, res, next);
}
app.use(timeout("10s"));
app.get(
"/api/reports/monthly",
replaceWithLongTimeout,
reportHandler,
);
注意:
req.clearTimeout 属性,设计会变得难以推理;headersTimeout、requestTimeout、connect-timeout 和 Axios timeout 各自负责的阶段。respond: false 自行监听 timeout 事件并返回统一 JSON。haltOnTimedout,并解释它为什么不能取消已经开始的 Promise。connect-timeout 的行为。