彻底搞懂浏览器Preflight请求的触发条件与解决方案
先看效果,再看代码
上周上线一个新功能,前端调用后端接口,本地跑得好好的,一上测试环境就 404 —— 不是接口不存在,而是 OPTIONS 请求直接被 Nginx 拦了,连后端的门都没摸到。抓包一看:浏览器发了个空 body 的 OPTIONS 请求,然后卡住不动了。那一刻我盯着 DevTools Network 面板,心里默念:又来了,Preflight 又来搞我。
今天不讲 CORS 是啥、RFC 是哪年写的——这些你搜一下比我看还快。我就说我在 jztheme.com 做实际项目时,怎么写、怎么配、怎么 debug,哪些方式亲测有效,哪些坑我反复踩了三次才记牢。
核心代码就这几行(但必须全)
假设你要往 https://jztheme.com/api/submit 发个带 token 和 JSON body 的 POST 请求:
fetch('https://jztheme.com/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer abc123',
'X-Request-ID': 'req-' + Date.now()
},
body: JSON.stringify({ name: '张三', email: 'zhang@example.com' })
})
这段代码一发,浏览器立刻悄悄发一个 OPTIONS 请求去“问路”。如果你后端没正确响应这个 OPTIONS,后续 POST 就不会发出去,fetch 会直接 reject,报错信息还特别含糊:Failed to fetch 或者 CORS error —— 但根本没告诉你到底是 preflight 挂了,还是 POST 挂了。
后端怎么接住这个 OPTIONS?别光靠 Express 中间件
我一开始图省事,在 Express 里加了个万能中间件:
app.options('*', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Request-ID');
res.status(204).end();
});
❌ 亲测无效。为什么?因为 它在所有路由之前执行,但没处理预检请求的路径匹配逻辑。比如你的 API 是 /api/submit,而 Express 的 * 匹配会走通,但某些反向代理(Nginx、Cloudflare)或网关层可能在到达 Node 之前就把 OPTIONS 给吞了或拒绝了。
✅ 我现在固定写法:在每个需要跨域的路由前,显式加一个同路径的 OPTIONS 处理:
// ✅ 正确姿势:和业务路由一一对应
app.options('/api/submit', (req, res) => {
res.set('Access-Control-Allow-Origin', 'https://your-app.com');
res.set('Access-Control-Allow-Methods', 'POST');
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Request-ID');
res.set('Access-Control-Allow-Credentials', 'true');
res.status(204).end();
});
app.post('/api/submit', (req, res) => {
// 实际业务逻辑
res.json({ ok: true });
});
注意三点:
- Origin 不要用 *:如果你用了 credentials(比如带 cookie 或 Authorization),
Access-Control-Allow-Origin就不能是*,必须写死域名(如https://your-app.com),否则浏览器直接拒收响应; - Headers 要列全:前端 header 里写了啥,这里
Access-Control-Allow-Headers就得包含啥,少一个(比如漏了X-Request-ID),preflight 就失败; - 状态码必须是 204:不是 200,不是 201,就是 204 No Content。有些老教程写 200,实测 Chrome 会认为响应非法,直接中断流程。
踩坑提醒:这三点一定注意
1. Nginx 默认拦 OPTIONS —— 我们线上环境 Nginx 配置里有一行:if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH)$) { return 405; }。结果 OPTIONS 直接 405。改法很简单,在 location 块里加:
location /api/ {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "https://your-app.com";
add_header Access-Control-Allow-Methods "POST,OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type,Authorization,X-Request-ID";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Max-Age 86400;
return 204;
}
}
2. Spring Boot 的 WebMvcConfigurer 默认不放行 OPTIONS —— 如果你用的是 Spring,别只配 @CrossOrigin 注解。它只对实际请求生效,对 preflight 无效。必须手动配置:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://your-app.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization", "X-Request-ID")
.allowCredentials(true);
}
}
3. Chrome DevTools 有时不显示 OPTIONS 请求 —— 特别是在“Disable cache”没勾上、或者页面刷新太快的时候。建议打开 Network → Filter 输入 “OPTIONS”,并勾选 “Preserve log”。不然你以为没发,其实是发了但被刷掉了。
高级技巧:如何让 preflight 缓存更久?
默认情况下,浏览器每次遇到新的请求头组合(比如多加一个 X-Trace-ID),就会重新发一次 OPTIONS。你可以通过 Access-Control-Max-Age 告诉它缓存多久(单位秒):
res.set('Access-Control-Max-Age', '86400'); // 缓存 24 小时
但注意:这个值不是越大越好。如果后端 CORS 策略变了(比如新增了一个 header),浏览器还在用旧缓存,就会出问题。我们目前统一设为 3600(1 小时),够用又不至于太僵硬。
这个场景最好用:微前端子应用跨域调主应用 API
我们有个 qiankun 微前端架构,子应用部署在 sub.jztheme.com,要调主应用的 https://jztheme.com/api/user。这时候 preflight 就特别敏感 —— 因为 Origin 是子域名,而主应用的 CORS 配置如果只写了 jztheme.com,不带 sub.,就会失败。
解决方案不是放宽 Origin,而是用动态匹配:
app.options('/api/user', (req, res) => {
const origin = req.headers.origin;
// 只允许 jztheme.com 及其子域名
if (/^https?://[^/]*.?jztheme.com(:d+)?$/.test(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
// 其他 header...
res.status(204).end();
} else {
res.status(403).end();
}
});
⚠️ 注意:正则里用了 ?.jztheme.com,是为了同时支持 jztheme.com 和 sub.jztheme.com,但不要用 .*.jztheme.com,容易被绕过。
结语
以上是我过去半年在多个项目里折腾 preflight 的全部实战经验。没有银弹,只有一个个 case 挨个解决。现在我已经把上面那套 Express + Nginx + Chrome 调试组合拳写进团队 Wiki 了,新人拉完代码跑一遍就能过。
这个技术的拓展用法还有很多,比如:如何用 Service Worker 拦截并伪造 preflight 响应(仅限调试)、如何在 Electron 中彻底关闭 CORS(有风险)、以及 WebAssembly 加载时的 preflight 特殊行为……后续会继续分享这类博客。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流。

暂无评论