Fetch 发送两次请求的原因
理解 CORS 预检请求机制
问题
使用 fetch 发送请求时,为什么浏览器 Network 面板会显示发送了两次请求?
解答
fetch 发送两次请求是因为 CORS 预检请求(Preflight Request)。当发送跨域的”非简单请求”时,浏览器会先发送一个 OPTIONS 请求询问服务器是否允许该请求。
请求流程
第一次请求:OPTIONS(预检请求)
↓
服务器返回允许的方法、头部等信息
↓
第二次请求:实际的 GET/POST/PUT 等请求
触发预检请求的条件
只要满足以下任一条件,就会触发预检请求:
// 1. 使用了非简单方法
fetch('https://api.example.com/data', {
method: 'PUT' // PUT、DELETE、PATCH 等
})
// 2. 设置了自定义请求头
fetch('https://api.example.com/data', {
headers: {
'X-Custom-Header': 'value', // 自定义头部
'Content-Type': 'application/json' // 非简单 Content-Type
}
})
// 3. Content-Type 不是简单类型
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 触发预检
},
body: JSON.stringify({ name: 'test' })
})
简单请求(不触发预检)
// 满足以下所有条件的请求不会触发预检:
// 1. 方法是 GET、HEAD、POST 之一
// 2. 只使用了简单头部
// 3. Content-Type 是以下之一:
// - text/plain
// - multipart/form-data
// - application/x-www-form-urlencoded
// 这个请求不会触发预检
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'name=test&age=18'
})
预检请求示例
// 客户端代码
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'John' })
})
# 第一次请求(预检)
OPTIONS /users HTTP/1.1
Origin: https://mysite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# 服务器响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
# 第二次请求(实际请求)
POST /users HTTP/1.1
Content-Type: application/json
Authorization: Bearer token123
服务端配置(Node.js 示例)
// Express 处理 CORS
const express = require('express')
const app = express()
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://mysite.com')
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.header('Access-Control-Max-Age', '86400') // 缓存预检结果 24 小时
// 直接响应预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204)
}
next()
})
关键点
- 两次请求是 CORS 预检机制导致的,第一次是 OPTIONS 请求
- 非简单请求才会触发预检:非简单方法、自定义头部、
application/json等 - 简单请求条件:GET/HEAD/POST + 简单头部 + 简单 Content-Type
Access-Control-Max-Age可以缓存预检结果,减少 OPTIONS 请求次数- 同源请求不会触发预检,这是跨域特有的机制
目录