结论是:
默认会继续,除非具体操作支持取消,而且你主动把取消信号传给它。
app.post(
"/api/orders",
timeout("5s"),
async (req, res) => {
await createSlowOrder(req.body);
if (req.timedout) {
return;
}
res.status(201).json({ success: true });
},
);
如果 createSlowOrder() 执行 12 秒:
0 秒 开始 createSlowOrder
5 秒 connect-timeout 产生 503 错误响应
12 秒 createSlowOrder 仍可能完成数据库写入
用户看到“失败”,数据库却可能已经成功。用户重试后,就可能生成两条订单。
可以为请求创建 AbortController:
interface RequestExecution {
controller: AbortController;
}
declare global {
namespace Express {
interface Request {
execution?: RequestExecution;
}
}
}
教学示例可以写成中间件:
function attachCancellation(
req: Request,
res: Response,
next: NextFunction,
): void {
const controller = new AbortController();
req.execution = { controller };
req.once("timeout", () => {
controller.abort(
new Error("Express request deadline exceeded"),
);
});
res.once("close", () => {
if (!res.writableFinished) {
controller.abort(
new Error("Client disconnected"),
);
}
});
next();
}
顺序:
app.get(
"/api/remote-data",
timeout("8s"),
attachCancellation,
remoteDataHandler,
);
因为 attachCancellation 需要监听 connect-timeout 发出的 timeout 事件,所以应放在它后面、耗时 handler 前面。
AbortSignal 的下游 HTTP 请求Node.js 内置 fetch() 支持 AbortSignal:
async function remoteDataHandler(
req: Request,
res: Response,
): Promise<void> {
const signal = req.execution?.controller.signal;
const response = await fetch(
"https://example.com/data",
{ signal },
);
const data: unknown = await response.json();
if (signal?.aborted || req.timedout) {
return;
}
res.json(data);
}
错误处理时要区分取消和真正的下游故障:
try {
// 支持 signal 的操作
} catch (error: unknown) {
if (signal?.aborted) {
return;
}
throw error;
}
AbortController.abort() 是发出取消请求,不保证所有库都能立即停止。必须查阅具体库是否接受 signal。
客户端可能因为以下原因断开:
AbortController.abort();在服务端,可以监听响应连接提前关闭:
res.once("close", () => {
if (!res.writableFinished) {
controller.abort(
new Error("Client disconnected before response finished"),
);
}
});
为什么检查 res.writableFinished:
发生中止后不要再调用:
res.json(...);
res.status(...).send(...);
连接已经不可用,继续写响应没有意义,还可能出现 headers already sent 或 socket 错误。
不一定。应根据任务语义分类。
用户已经不再等待,这些任务继续运行通常只会浪费资源。
这类任务不应把业务成功与 HTTP 连接寿命绑定。更好的设计是:
请求校验
→ 记录任务或写入 outbox
→ 尽快返回 202 / 任务 ID
→ 后台任务可靠执行
→ 客户端查询结果
客户端断开只意味着“无法把结果通过当前连接返回”,不一定意味着业务必须撤销。
读查询超时通常只是浪费资源;写请求超时还会造成“结果未知”:
客户端收到超时
↓
写入实际上可能:
A. 尚未开始
B. 已回滚
C. 已提交成功
D. 提交结果返回途中连接断开
客户端无法仅凭 timeout 判断数据库状态。
因此,写接口不能把“客户端没收到成功响应”直接等同于“写入没有发生”。
事务可以保证一组数据库操作原子提交或回滚:
const transaction = await sequelize.transaction();
try {
await Order.create(orderData, { transaction });
await OrderItem.bulkCreate(items, { transaction });
if (signal.aborted) {
await transaction.rollback();
return;
}
await transaction.commit();
} catch (error: unknown) {
await transaction.rollback();
throw error;
}
但要注意竞态:
检查 signal.aborted 为 false
↓
开始 commit
↓
客户端断开或 deadline 到期
↓
数据库 commit 仍可能成功
事务不能保证 HTTP 客户端一定收到提交结果,也不能自动撤销已经提交的数据。
AbortSignal,必须查具体版本和 API。客户端为写请求生成唯一键:
POST /api/orders
Idempotency-Key: 8a62f410-7ea8-4dbc-a344-4778c3f10e03
数据库建立唯一约束:
CREATE UNIQUE INDEX uk_orders_idempotency_key
ON orders (idempotency_key);
第一次请求即使响应丢失,第二次携带同一个键时,服务端也不会再创建一份订单,而是返回已有结果或当前处理状态。
幂等键比“超时后猜测是否成功”可靠得多,尤其适合:
对于当前项目的 mysql2/Sequelize,应分层处理:
Express deadline
→ 阻止 HTTP 请求无限等待
数据库语句/锁等待限制
→ 限制 MySQL 查询和锁等待
事务
→ 保证多条 SQL 原子性
唯一约束/幂等键
→ 防止超时重试产生重复写入
状态查询/对账
→ 处理 commit 结果未知
不要把销毁数据库连接当作常规取消方案。它的代价较高,并且在写操作提交边界附近仍可能无法判断最终状态。
假设一个报表请求会消耗大量 CPU 和数据库资源:
客户端发起请求
→ 服务端建立 AbortController
→ 数据库查询接受自己的短 timeout
→ CPU 工作放入 Worker/任务队列
→ 客户端断开时发出取消信号
→ 可取消任务尽快停止
→ 不可取消但必须完成的任务转为后台状态管理
具体建议:
AbortSignal 传播取消;202 Accepted + jobId;fetch() 调用慢服务的 Route,并在 connect-timeout 触发后通过 AbortController 取消 fetch。res.close,区分正常响应完成和客户端提前断开。idempotency_key 字段与唯一索引。202 + jobId 的异步任务接口。