一次生产请求可能经过:
Axios / 浏览器
→ CDN / Load Balancer
→ Nginx
→ Express connect-timeout
→ 下游 HTTP / Redis / MySQL
通常让内部依赖先超时,外层留出返回结构化错误的时间:
下游依赖 timeout
<
Express Route deadline
<
Nginx / 网关 timeout
<
Axios timeout
例如:
MySQL 查询限制 4 秒
下游 HTTP timeout 5 秒
Express deadline 8 秒
Nginx 9 秒
Axios 10 秒
这只是示例。真实值应根据接口 SLO、历史延迟、依赖能力和用户体验确定。
import express, {
type ErrorRequestHandler,
type NextFunction,
type Request,
type Response,
} from "express";
import timeout from "connect-timeout";
const app = express();
app.use(requestIdMiddleware);
app.use(accessLogMiddleware);
const apiRouter = express.Router();
apiRouter.use(timeout("8s"));
apiRouter.use(attachCancellation);
apiRouter.use(express.json({ limit: "1mb" }));
apiRouter.use(haltOnTimedout);
apiRouter.use(authenticate);
apiRouter.use(haltOnTimedout);
apiRouter.use(routes);
app.use("/api", apiRouter);
app.use(notFoundHandler);
app.use(errorHandler);
顺序的含义:
AbortSignal;如果请求体很大,Node.js requestTimeout、Nginx 请求体限制和上传策略仍需单独配置。connect-timeout 不能替代这些 HTTP 接收阶段限制。
interface TimeoutLikeError extends Error {
status?: number;
timeout?: number;
}
const errorHandler: ErrorRequestHandler = (
error: TimeoutLikeError,
req,
res,
next,
) => {
if (res.headersSent) {
next(error);
return;
}
if (error.timeout !== undefined) {
res.status(503).json({
code: "REQUEST_TIMEOUT",
message: "服务器未能在规定时间内完成请求",
requestId: res.locals.requestId,
});
return;
}
res.status(error.status ?? 500).json({
code: "INTERNAL_SERVER_ERROR",
message: "服务器内部错误",
requestId: res.locals.requestId,
});
};
官方中间件默认产生 503 Service Unavailable。
504 Gateway Timeout 从 HTTP 语义上通常表示网关或代理等待上游服务超时。如果 Express 自己充当某个下游服务的网关,可以根据团队规范映射为 504;但不要在日志中丢失真正的超时来源。
慢操作返回时,timeout error 可能已经发送响应。Route 必须停止:
const result = await slowOperation();
if (
req.timedout ||
req.execution?.controller.signal.aborted ||
res.headersSent ||
res.destroyed
) {
return;
}
res.json(result);
这些检查各自含义不同:
req.timedout:connect-timeout timer 已触发;signal.aborted:deadline、客户端断开或其他取消源已发出取消;res.headersSent:响应头已经发送,不能重新设置状态和 Header;res.destroyed:响应流已经被销毁。检查只能防止二次响应,不能取代底层操作取消。
超时日志建议使用结构化字段:
{
"event": "request_timeout",
"requestId": "req-123",
"method": "GET",
"route": "/api/reports/monthly",
"timeoutMs": 8000,
"elapsedMs": 8012,
"userId": 42,
"phase": "mysql_query",
"clientDisconnected": false
}
注意不要把以下内容直接写入日志:
建议区分:
deadline_exceeded 服务端应用期限到达
client_disconnected 客户端或代理提前关闭连接
dependency_timeout MySQL/HTTP/Redis 等依赖超时
queue_timeout 等待连接池或队列资源超时
如果全部记录为 REQUEST_TIMEOUT,后期很难找到真正瓶颈。
不要只看平均响应时间。平均值正常时,P99 仍可能严重超时。
import express from "express";
import request from "supertest";
import timeout from "connect-timeout";
const app = express();
app.get(
"/slow",
timeout("50ms"),
async (req, res) => {
await new Promise<void>((resolve) => {
setTimeout(resolve, 100);
});
if (!req.timedout) {
res.json({ success: true });
}
},
);
app.use(errorHandler);
await request(app)
.get("/slow")
.expect(503)
.expect(({ body }) => {
if (body.code !== "REQUEST_TIMEOUT") {
throw new Error("错误码不正确");
}
});
测试环境使用很短的 timeout 可以提高速度,但要留有合理余量,避免 CI 机器负载造成偶发失败。
只断言 503 不够,还要验证资源被取消:
let completed = false;
let cancelled = false;
async function cancellableWork(
signal: AbortSignal,
): Promise<void> {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
completed = true;
resolve();
}, 200);
signal.addEventListener(
"abort",
() => {
clearTimeout(timer);
cancelled = true;
reject(signal.reason);
},
{ once: true },
);
});
}
测试应断言:
HTTP 返回超时
cancelled === true
completed === false
没有未处理的 Promise rejection
没有残留数据库事务或连接
客户端断开测试需要验证:
close;res.writableFinished 为 false;client_disconnected 而不是普通 500;这类测试可以使用原生 node:http 创建请求,再主动调用 request.destroy(),因为部分高级测试库会隐藏底层断开行为。
示例:
location /api/ {
proxy_connect_timeout 3s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
proxy_pass http://node_backend;
}
proxy_read_timeout 更接近两次读取上游数据之间允许的空闲时间,并不总是“整个请求绝对只能执行 10 秒”。实际行为要结合响应是否持续输出数据理解。
如果 Nginx 先超时并关闭上游连接,Express 应通过连接 close 取消可取消任务。否则 Node.js 仍可能继续处理一个已无人等待的请求。
当接口经常需要几分钟,应优先判断它是否应该改为后台任务:
POST /api/reports
→ 202 Accepted
{
"jobId": "report-123"
}
查询进度:
GET /api/report-jobs/report-123
结果:
{
"status": "completed",
"downloadUrl": "/api/reports/report-123/download"
}
适合异步化的任务:
connect-timeout 与 Node HTTP timeout 的区别;AbortSignal;connect-timeout 错误转换为稳定 JSON。cancelled 为 true、completed 为 false。node:http 主动销毁客户端请求,验证 Express 能识别连接提前关闭。202 + jobId 的后台任务流程。