背景:一次“无辜”的版本升级
最近我在将底层的 fastify 从 v4.x 升级到 v5.x(例如 v5.6.2)后,发现原本运行完美的 NestJS 项目突然开始报跨域(CORS)错误。
最典型的症状是:
- GET/POST 请求偶尔正常,但 PUT/DELETE 请求全线飘红。
- 浏览器控制台报错提示缺少
Access-Control-Allow-Methods或Access-Control-Allow-Origin。 - 网络面板显示 OPTIONS 预检请求 返回了
404 Not Found或405 Method Not Allowed。
代码逻辑明明没有变,仅仅升级了依赖,为什么跨域策略就崩了?这篇文章将深入剖析 Fastify v5 的安全策略变更以及它与 NestJS 适配器之间的“代沟”。
核心原因剖析
这次“翻车”并非偶然,而是由三个层面的变化共同导致的:
1. 安全哲学的转变:从“隐式宽容”到“显式严格”
Fastify v5 引入了 "Secure by Default"(默认安全) 的设计理念。
- 在 v4 时代:如果你没有详细配置 CORS,框架往往比较宽容。它可能会默认允许所有来源,或者自动反射 Origin 头,甚至隐式地允许常见的 HTTP 方法。
- 在 v5 时代:一切都变了。如果你不显式声明
methods: ['PUT', 'DELETE'],Fastify 默认可能只放行最基础的请求。原本“虽未明说但默认允许”的灰色地带,现在变成了“未明说即拒绝”的红色警戒区。
2. OPTIONS 预检请求的“无人认领”惨案
这是导致报错的最直接原因。现代浏览器在发送复杂请求(如带自定义头或非 GET/POST 请求)前,会先发一个 OPTIONS 请求询问服务器权限。
-
旧版行为:NestJS 的
app.enableCors()底层依赖的旧版插件,往往会在路由匹配前就“激进”地拦截所有 OPTIONS 请求并直接返回204 OK。 -
新版行为:Fastify v5 优化了路由匹配机制。如果你的路由表中没有显式定义
OPTIONS /api/xxx,且 CORS 插件没有被正确挂载到全局上下文,Fastify 核心会优先接管请求并发现“路由不存在”,直接返回 404。- 结果:浏览器收到 404,认为服务器不支持跨域,直接扼杀后续的真实请求。
3. NestJS Adapter 的“版本时差”
这是一个典型的生态兼容性问题。
- 现状:
@nestjs/platform-fastify(尤其是 v10 及以下版本)是对 Fastify 的一层封装。它内部调用 CORS 的逻辑可能还是基于 v4 的 API 风格硬编码的。 - 断层:当你强制升级底层 Fastify 到 v5,但 NestJS 的适配器层还没来得及跟进 v5 的**插件封装(Encapsulation)**新特性时,
app.enableCors()注册的插件可能根本没有生效,或者生效的范围不对(没能覆盖到全局)。
解决方案
既然 NestJS 自带的 app.enableCors() 因版本代沟而失效,最稳妥的方案是绕过封装,直接对话底层。
我们不再依赖 NestJS 的自动处理,而是直接在 Fastify 实例上注册官方且兼容 v5 的 CORS 插件。
第一步:安装正确的依赖
确保安装适配 Fastify v5 的 CORS 插件(通常是 v10+):
npm install @fastify/cors --save
第二步:修改 main.ts
在 bootstrap 函数中,禁用 NestJS 的 CORS 方法,改用原生注册:
`import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify';
import { AppModule } from './app.module'; // 引入原生插件
import fastifyCors from '@fastify/cors';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( AppModule, new FastifyAdapter() );
// ❌ 弃用:NestJS 封装的方法可能与 Fastify v5 不兼容 // app.enableCors();
// ✅ 推荐:直接在底层 Fastify 实例上注册 // 这能确保 OPTIONS 预检请求被正确接管,且符合 v5 的安全规范
await app.register(fastifyCors, {
origin: true, // 根据需求配置,生产环境建议指定具体域名
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], credentials: true, }); await app.listen(3000, '0.0.0.0');
}
bootstrap();`
总结
Fastify v5 的这次升级并非是在“制造麻烦”,而是在收紧安全边界。它强迫开发者从“因为配置模糊而通过”转向“因为配置明确而通过”。
虽然这暂时给 NestJS 用户带来了适配上的阵痛,但通过改用原生 app.register 方式,我们不仅解决了报错,还让应用的安全策略变得更加清晰和可控。