10 中间件顺序与错误响应
1. 推荐的基础顺序
const app = express();
app.use(requestIdMiddleware);
app.use(helmet());
app.use(cors(corsOptions));
app.use(express.json({ limit: "1mb" }));
app.use("/api", authenticationMiddleware, apiRouter);
app.use(notFoundHandler);
app.use(errorHandler);
这个顺序的关键不是形式,而是:
- 安全 Header 和 CORS 覆盖成功、404、认证失败和业务错误;
- OPTIONS 在进入 JWT/Session 校验前由 CORS 中间件回答;
- JSON 解析错误也能得到统一 Header;
- 错误处理器位于最后。
2. 为什么 CORS 通常放在认证之前
预检请求是在询问“正式请求是否允许”,通常不会携带业务 Bearer Token 或 Cookie。若先认证:
// 容易导致 OPTIONS 先返回 401
app.use(authenticationMiddleware);
app.use(cors(corsOptions));
浏览器无法完成预检,正式请求不会发出。更合理的是:
app.use(cors(corsOptions));
app.use(authenticationMiddleware);
CORS 放行不等于跳过认证:预检通过后,正式请求依旧进入认证中间件。
3. 错误响应为什么也必须带 CORS Header
如果成功响应有 CORS Header,但 401、404、500 或 Nginx 502 没有,浏览器会把真实错误隐藏成笼统的 CORS 错误。前端看不到 JSON 错误内容,排错困难。
全局 cors() 位于路由之前,通常能让 Express 后续错误响应保留相关 Header。但以下响应可能绕过它:
- CORS 中间件之前的中间件直接响应;
- Nginx 自己产生的 404/413/502/504;
- CDN/WAF 返回的错误;
- 连接在生成 HTTP 响应前中断。
因此必须分别测试成功和失败路径,不能只测 200。
4. Express 5 异步错误
Express 5 可以把异步路由中抛出的异常交给错误处理器:
app.get("/api/users/:id", async (req, res) => {
const user = await findUser(req.params.id);
if (!user) {
throw new Error("用户不存在");
}
res.json({ code: 0, data: user });
});
错误处理器必须保留四个参数:
import type { ErrorRequestHandler } from "express";
const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
if (res.headersSent) {
next(error);
return;
}
res.status(500).json({
code: "INTERNAL_ERROR",
message: "服务器内部错误",
requestId: res.locals.requestId,
});
};
若项目业务约定 HTTP 始终返回 200,也不改变 CORS 原理;但日志、监控和代理将难以依靠 HTTP 状态识别故障,应至少记录业务 code,并确认 Nginx 自身错误不可能遵循该 JSON 约定。
5. 路由级 CORS 的漏网风险
app.get("/api/data", cors(corsOptions), handler);
如果只有具体路由使用 CORS,那么:
- 路由匹配前产生的 404 可能无 CORS Header;
- 认证中间件若放在它前面,401 无 CORS Header;
- OPTIONS 可能匹配不到。
统一 API 通常适合应用级或 Router 级 CORS。只有策略明显不同的公开资源才单独配置。
6. 下载、SSE 与文件预览
- 下载:跨源前端要读取文件名时暴露
Content-Disposition。 - SSE:浏览器连接也受跨源规则影响;Cookie 场景需要凭据配置,并检查代理缓冲。
- HTML 预览:除了 CORS,还要特别考虑 CSP、iframe、COOP/CORP;不可信 HTML 不应仅靠 Helmet 就直接同源展示。
- 文件上传:预检通过不代表文件安全,还须大小、类型、内容、路径和授权校验。
7. 最小回归矩阵
上线前至少验证:
| 场景 | 预期 |
|---|---|
| 允许 Origin + 正常请求 | 前端可读业务响应 |
| 允许 Origin + JWT 无效 | 前端可读 401/业务错误 JSON |
| 允许 Origin + 路由不存在 | 前端可读统一 404 |
| 拒绝 Origin | 浏览器不可读响应,服务端有审计日志 |
| OPTIONS + Authorization 声明 | 不被认证中间件拦截 |
| 无 Origin 健康检查 | 按独立认证策略处理 |