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);

这个顺序的关键不是形式,而是:

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。但以下响应可能绕过它:

因此必须分别测试成功和失败路径,不能只测 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,那么:

统一 API 通常适合应用级或 Router 级 CORS。只有策略明显不同的公开资源才单独配置。

6. 下载、SSE 与文件预览

7. 最小回归矩阵

上线前至少验证:

场景 预期
允许 Origin + 正常请求 前端可读业务响应
允许 Origin + JWT 无效 前端可读 401/业务错误 JSON
允许 Origin + 路由不存在 前端可读统一 404
拒绝 Origin 浏览器不可读响应,服务端有审计日志
OPTIONS + Authorization 声明 不被认证中间件拦截
无 Origin 健康检查 按独立认证策略处理