POST为什么发送两次请求

想获取更多高质量的Java技术文章?欢迎访问 技术小馆官网,持续更新优质内容,助力技术成长!

你是否曾经在开发过程中发现一个奇怪的现象:明明只触发了一次表单提交,却在网络面板中看到两个POST请求?或者在调试API接口时,一个简单的数据提交却导致服务器收到重复数据?这不是你的代码出了问题,而是浏览器的一种特殊行为机制。

当我第一次遇到这个问题时,也是一头雾水,直到深入了解了HTTP协议和浏览器的工作原理,才恍然大悟。今天,就让我们一起揭开这个困扰许多开发者的谜团,看看那些"幽灵般"的双重POST请求到底是怎么回事。

一、OPTIONS预检请求:双重POST的主要元凶

1. 什么是CORS跨域资源共享

跨域资源共享(CORS)是浏览器的一种安全机制,用于控制不同源之间的HTTP请求。当你的前端应用(例如 https://myapp.com)尝试向不同源的服务器(例如 https://api.otherservice.com)发送请求时,浏览器会执行CORS检查。

简单来说,"同源"要求协议、域名和端口都相同,否则就是跨域请求:

// 同源示例
https://myapp.com/page1.html → https://myapp.com/api/data

// 跨域示例
https://myapp.com → https://api.service.com (不同域名)
http://myapp.com → https://myapp.com (不同协议)
https://myapp.com → https://myapp.com:8080 (不同端口)

2. 预检请求(Preflight)的作用

预检请求是浏览器在发送实际跨域请求前,先发送一个OPTIONS方法的HTTP请求,用来"询问"服务器是否允许接下来的实际请求。这就是为什么你会看到两个请求的原因 - 一个OPTIONS请求和一个实际的POST请求。预检请求的主要作用是保护服务器免受可能有害的跨域请求,特别是那些可能改变服务器数据的请求(如POST、PUT、DELETE等)。

3. 为什么浏览器需要发送OPTIONS请求

浏览器发送OPTIONS请求是为了:

  • 确认服务器是否允许该跨域请求
  • 检查服务器是否接受请求中使用的HTTP方法
  • 验证服务器是否接受请求中携带的自定义头部
  • 确认服务器是否允许请求中的内容类型

一个典型的OPTIONS请求头部如下:

OPTIONS /api/data HTTP/1.1
Host: api.otherservice.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

二、常见的触发双重POST场景

1. 跨域API调用时的情形

最常见的双重POST请求出现在前端应用调用不同域的API时:

// 前端代码 (运行在 https://myapp.com)
fetch('https://api.otherservice.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({ name: '张三', age: 30 })
})

这段代码会触发一个OPTIONS预检请求,然后才是实际的POST请求。

2. 携带自定义请求头的POST请求

即使是同源请求,如果添加了自定义请求头,也会触发预检:

fetch('/api/local', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': '自定义值' // 自定义头部会触发预检
  },
  body: JSON.stringify({ data: '测试数据' })
})

3. 使用非简单内容类型的请求

当使用除了以下内容类型之外的其他类型时,也会触发预检请求:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

例如,使用JSON格式:

axios.post('/api/data', 
  { userId: 123 },
  { headers: { 'Content-Type': 'application/json' } }
)

4. WebSocket连接建立过程中的预检

在建立WebSocket连接时,如果是跨域的,也会先发送一个OPTIONS请求:

const socket = new WebSocket('wss://api.otherservice.com/socket');

三、如何识别是否为预检请求

1. 网络面板中的请求特征

在Chrome开发者工具的Network面板中,预检请求有明显特征:

  • 请求方法显示为OPTIONS
  • 状态码通常为200或204
  • 请求大小通常很小,因为不包含实际数据

![网络面板示例]

2. OPTIONS方法与实际POST的关系

OPTIONS请求总是先于实际的POST请求发送,如果OPTIONS请求失败,浏览器将不会发送后续的实际请求。这是一种"先问后做"的安全机制。

3. 预检请求的响应头分析

成功的预检请求响应通常包含以下头部:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

其中Access-Control-Max-Age指定了预检请求的缓存时间,单位为秒。

四、解决双重POST请求的策略

1. 服务端正确配置CORS响应头

在服务端正确配置CORS头部是最基本的解决方案:

// Node.js Express示例
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://myapp.com');
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Max-Age', '86400'); // 24小时缓存预检结果
  
  // 对OPTIONS请求直接返回200
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  
  next();
});

2. 使用简单请求规避预检

如果可能,使用简单请求可以避免预检:

// 使用表单数据而非JSON
const formData = new FormData();
formData.append('name', '张三');
formData.append('age', '30');

fetch('/api/data', {
  method: 'POST',
  body: formData // 不需要设置Content-Type
})

3. 预检请求缓存机制的利用

通过设置较长的Access-Control-Max-Age,可以减少预检请求的频率:

// 服务端设置
res.header('Access-Control-Max-Age', '86400'); // 24小时

这样,在缓存期内,浏览器不会重复发送预检请求。

4. 代理服务器解决跨域问题

在开发环境中,可以使用代理服务器转发请求,避免跨域:

// webpack开发服务器配置
module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.otherservice.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
};

五、常见框架中的POST请求处理

1. React中的Axios请求配置

在React应用中,可以配置Axios全局处理CORS相关问题:

// axios配置
import axios from 'axios';

const ts = axios.create({
  baseURL: 'https://api.service.com',
  timeout: 5000
});

// 请求拦截器
ts.interceptors.request.use(config => {
  // 避免OPTIONS请求携带认证信息
  if (config.method.toLowerCase() !== 'options') {
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
  }
  return config;
});

export default ts;

2. Vue项目中的请求拦截器设置

Vue项目中也可以类似配置:

// vue项目中的请求配置
import axios from 'axios';

const ts = axios.create({
  baseURL: process.env.VUE_APP_API_URL,
  timeout: 10000
});

// 在请求拦截器中处理
ts.interceptors.request.use(config => {
  // 简单请求不需要预检
  if (config.method === 'post') {
    // 对于某些接口使用表单格式而非JSON
    if (config.url.includes('/simple-endpoint')) {
      config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
      // 转换数据格式
      const formData = new URLSearchParams();
      for (const key in config.data) {
        formData.append(key, config.data[key]);
      }
      config.data = formData;
    }
  }
  return config;
});

3. Express后端的CORS中间件

在Express后端,可以使用专门的CORS中间件:

const express = require('express');
const cors = require('cors');
const app = express();

// CORS配置
const corsOptions = {
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400 // 预检请求缓存时间
};

// 应用CORS中间件
app.use(cors(corsOptions));

// 路由定义
app.post('/api/data', (req, res) => {
  // 处理POST请求
  res.json({ success: true });
});

app.listen(3000, () => {
  console.log('服务器运行在3000端口');
});

六、双重POST带来的性能影响

1. 网络延迟与用户体验

每个预检请求都会增加额外的网络往返时间:

  • 一次普通请求:客户端 → 服务器 → 客户端
  • 带预检的请求:客户端 → 服务器(OPTIONS) → 客户端 → 服务器(POST) → 客户端

这种额外的网络延迟在网络条件不佳时尤为明显,可能导致用户体验下降。

2. 服务器额外负载分析

预检请求会增加服务器的请求处理量:

// 假设每分钟有100个POST请求
无预检情况:100个请求/分钟
有预检情况:200个请求/分钟(100个OPTIONS + 100个POST)

对于高流量网站,这可能导致服务器负载显著增加。

3. 移动端环境下的特殊考量

在移动网络环境下,额外的网络往返尤其昂贵:

  • 增加电池消耗
  • 在弱网环境下可能导致请求超时
  • 增加用户流量消耗

因此,在移动应用中应特别注意减少不必要的预检请求。

七、调试与排查双重POST问题的工具

1. Chrome开发者工具的高级用法

Chrome开发者工具提供了强大的功能来分析网络请求:

  • 使用Network面板过滤OPTIONS请求:method:OPTIONS
  • 查看请求头和响应头详情
  • 使用"Disable cache"选项确保不受缓存影响
  • 使用"Preserve log"选项在页面跳转后保留请求记录

2. Fiddler/Charles抓包分析

Fiddler和Charles等代理工具可以更深入地分析请求:

// Fiddler过滤器示例
req.method == "OPTIONS" || (req.method == "POST" && req.url.contains("api"))

这些工具还可以修改请求头进行测试,帮助诊断问题。

3. Postman与浏览器行为的差异

值得注意的是,Postman等API测试工具不遵循浏览器的同源策略,因此不会发送预检请求:

// Postman中直接发送的请求
POST https://api.otherservice.com/data
Content-Type: application/json
Authorization: Bearer token123

{
  "name": "张三",
  "age": 30
}

这种差异有时会导致在Postman中能工作但在浏览器中失败的情况。

4. 服务端日志分析技巧

服务端日志分析可以帮助发现预检请求相关问题:

// Express日志中间件示例
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} | ${req.method} ${req.url}`);
  if (req.method === 'OPTIONS') {
    console.log('收到预检请求,响应头:', JSON.stringify(res.getHeaders()));
  }
  next();
});

通过分析日志,可以确认预检请求是否正确处理,以及实际请求是否到达服务器。

#java#
全部评论

相关推荐

07-02 13:29
已编辑
北京化工大学 Java
简历发邮箱:**********地点:上海虹桥公司:汽车行业-智驾方向岗位:JAVA开发工程师(数仓方向)工作职业1. 参与数仓指标体系建设- 基于业务需求,使用 SQL 和 UDF 开发数据指标计算任务,构建企业级数据仓库。- 优化数仓模型,提升数据质量和查询性能,支持业务决策分析。2. 数据集成与 ETL 开发- 使用 Seatunnel 开发数据同步任务,实现跨源数据(如 MongoDB、业务库)的抽取、清洗和转换。- 基于 DolphinScheduler 设计和调度数据处理工作流,确保任务稳定运行。3. 业务埋点体系开发- 参与设计业务埋点方案,跟踪用户行为数据,支持产品优化和运营分析。- 开发埋点数据采集、清洗和入库的全流程处理逻辑。4. 技术协作与问题排查- 与后端团队协作,基于 Spring 框架开发数据服务接口。- 使用 Doris 等 OLAP 引擎优化指标查询性能,解决数据处理中的疑难问题。岗位要求:1. Java 编程基础扎实,- 熟练掌握面向对象编程(OOP),包括类、继承、多态、接口等概念- 深入理解 Java 容器类(如 List、Map、Set 的实现原理)- 熟悉多线程编程和常用设计模式2. 数据库与 SQL- 精通 SQL,包括复杂查询、索引优化、窗口函数等- 熟悉 MySQL 数据库设计和调优,了解事务隔离级别3. 后端开发框架熟练使用 Spring/Spring Boot 框架,理解依赖注入(DI)和 AOP 原理掌握 MyBatis 或其他 ORM 框架的使用4. 有大数据基础- 了解大数据组件 Hadoop 生态(HDFS、YARN、MapReduce)- 熟悉 Flink 或 Spark 的基本原理和使用场景- 掌握 SeaTunnel(或类似 ETL 工具)的数据同步开发5. 数据仓库知识- 理解数仓分层架构(ODS/DWD/DWS/ADS)- 掌握维度建模方法(星型 / 雪花模型)- 了解 OLAP 数据库(如 Doris、ClickHouse)的特点和适用场景
Java求职圈
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务