1. 为什么POST请求会发送两次?
这个问题在前端开发面试中经常被问到,也是实际项目中常见的现象。当你在浏览器控制台的Network面板中看到同一个POST请求出现了两次时,这通常与浏览器的安全机制有关。
1.1 CORS预检请求机制
现代浏览器在发送某些跨域请求前,会先发送一个OPTIONS方法的预检请求(preflight request)。这个机制是跨域资源共享(CORS)规范的一部分,目的是确保服务器理解并允许实际的跨域请求。
预检请求会在以下情况触发:
- 使用了PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH方法
- 设置了自定义请求头(非简单头部)
- Content-Type不是application/x-www-form-urlencoded、multipart/form-data或text/plain
提示:简单请求不会触发预检,GET、HEAD、POST(使用上述Content-Type)以及只包含简单头部的请求属于简单请求。
1.2 预检请求的工作流程
当浏览器检测到需要预检的请求时,会按以下顺序执行:
首先发送OPTIONS请求,包含:
- Access-Control-Request-Method:告知服务器实际请求将使用的方法
- Access-Control-Request-Headers:告知服务器实际请求将携带的自定义头部
- Origin:请求来源
服务器响应OPTIONS请求,返回:
- Access-Control-Allow-Methods:允许的方法列表
- Access-Control-Allow-Headers:允许的头部列表
- Access-Control-Max-Age:预检结果缓存时间
如果预检通过,浏览器才会发送实际的POST请求
2. 如何识别和调试预检请求
2.1 浏览器开发者工具中的表现
在Chrome开发者工具的Network面板中:
- 预检请求会显示为OPTIONS方法
- 状态码通常是204或200
- 请求头包含Access-Control-Request-*系列头部
- 响应头包含Access-Control-Allow-*系列头部
2.2 常见问题排查
如果遇到预检问题,可以检查以下方面:
- 服务器是否正确响应OPTIONS请求
- Access-Control-Allow-Methods是否包含POST
- Access-Control-Allow-Headers是否包含所有自定义头部
- Access-Control-Allow-Origin是否正确设置
3. 服务器端配置示例
3.1 Node.js Express配置
const express = require('express'); const app = express(); // 处理OPTIONS预检请求 app.options('/api', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); res.sendStatus(204); }); // 处理实际POST请求 app.post('/api', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.json({ message: 'Success' }); }); app.listen(3000);3.2 Nginx配置示例
location /api { if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Content-Type'; add_header 'Access-Control-Max-Age' 86400; return 204; } add_header 'Access-Control-Allow-Origin' '*'; proxy_pass http://backend; }4. 性能优化建议
虽然预检请求是必要的安全机制,但我们可以优化其性能影响:
- 设置适当的Access-Control-Max-Age头部,让浏览器缓存预检结果
- 尽量使用简单请求避免预检
- 合并自定义请求头,减少需要预检的头部数量
- 对于频繁使用的API,考虑使用同源策略
5. 幂等性考虑
由于POST方法本身不是幂等的,而OPTIONS是安全的,这种两次请求机制不会导致数据不一致。但开发时仍需注意:
- 确保POST接口的幂等性设计
- 对于非幂等操作,考虑使用PUT方法替代
- 在服务端记录请求ID,防止重复处理
6. 实际项目中的经验
在最近的一个电商项目中,我们遇到了上传商品图片时触发预检请求的问题。解决方案是:
- 将自定义的X-Request-ID头部改为简单头部
- 设置Access-Control-Max-Age为86400秒(1天)
- 在后端统一处理OPTIONS请求
这样优化后,同一用户一天内只需要发送一次预检请求,显著提升了用户体验。