在调试一个跨域上传接口时,后端同事突然找你:“你传的文件名是乱码啊!”
你一脸懵:“我传的是 简历-张三.pdf
,怎么就乱码了?”
查了半天,发现是漏了一个关键请求头:
// ❌ 错误写法
fetch('/upload', {
method: 'POST',
body: formData
})
// ✅ 正确写法
fetch('/upload', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; charset=utf-8' // 🔍 指定字符集
},
body: formData
})
一、问题场景:文件上传中的中文乱码
有个简历上传功能,用户上传 简历-张三.pdf
,但后端收到的是 ç®å-å¼ ä¸.pdf
。
问题出在哪?—— 字符编码未声明。
HTTP 协议本身是 ASCII 的,但现代 Web 处理的是 UTF-8 中文。如果没有明确告诉服务器“这是 UTF-8”,它可能按 ISO-8859-1 解码,结果就是乱码。
二、解决方案:用正确的请求头声明编码
// 方案1:在 Content-Type 中声明
const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('filename', '简历-张三.pdf')
fetch('/api/upload', {
method: 'POST',
headers: {
// 🔍 明确指定字符集
'Content-Type': 'multipart/form-data; charset=utf-8'
},
body: formData
})
但 Content-Type
的 charset
对 multipart/form-data
实际上不起作用!真正的解决方案是:
// 方案2:在字段名中使用 RFC 5987 编码
formData.append('filename*', 'UTF-8\'\'%E7%AE%80%E5%8E%86-%E5%BC%A0%E4%B8%89.pdf')
或者让后端支持 UTF-8 解码 multipart
表单。
三、核心请求头详解:从表面到协议层的三层机制
1. 表面用法:最常用的 10 个请求头
请求头 | 作用 | 示例 |
---|---|---|
Host | 指定服务器域名和端口 | Host: api.example.com:8080 |
User-Agent | 客户端标识 | User-Agent: Chrome/128.0 |
Accept | 客户端能接收的响应类型 | Accept: application/json |
Accept-Encoding | 支持的压缩格式 | Accept-Encoding: gzip, deflate |
Accept-Language | 期望的语言 | Accept-Language: zh-CN,zh;q=0.9 |
Content-Type | 请求体的 MIME 类型 | Content-Type: application/json; charset=utf-8 |
Authorization | 身份认证凭证 | Authorization: Bearer <token> |
Referer | 来源页面 | Referer: https://example.com/search |
Origin | 跨域请求的源 | Origin: https://admin.example.com |
Cookie | 发送 Cookie | Cookie: session=abc123 |
2. 底层机制:浏览器如何使用这些请求头
我们来画一张 浏览器发送请求的流程图:
flowchart TD
A([1. 用户输入 URL 或发起 fetch]) --> B[2. 浏览器自动添加基础头]
B --> C[3. 开发者手动添加/覆盖 headers]
C --> D[4. 发送请求]
D --> E[5. 服务器根据请求头决定]
B --> B1["Host(从 URL 解析)"]:::auto
B --> B2["User-Agent(内置)"]:::auto
B --> B3["Accept(默认 */*)"]:::auto
B --> B4["Accept-Encoding(gzip 等)"]:::auto
B --> B5["Accept-Language(系统语言)"]:::auto
B --> B6["Cookie(匹配域名的)"]:::auto
E --> E1["返回格式(JSON/HTML)"]:::server
E --> E2["是否压缩响应"]:::server
E --> E3["使用什么语言"]:::server
E --> E4["是否允许跨域"]:::server
E --> E5["是否认证通过"]:::server
classDef auto fill:#d4edda,stroke:#333
classDef server fill:#ffd699,stroke:#333
classDef default fill:#f8f9fa,stroke:#333
关键点:
Host
是 HTTP/1.1 强制要求的头,用于虚拟主机User-Agent
被用于设备识别、兼容性处理Accept-Encoding
触发服务器压缩响应,节省带宽
3. 设计哲学:为什么需要这么多请求头?
HTTP 是“无状态、可扩展”的协议,请求头就是它的扩展机制。
类比:
HTTP 请求就像一封挂号信,请求头是信封上的标签:
Host
→ 收件人地址User-Agent
→ 寄件人身份Accept
→ “请用中文回复”Authorization
→ “我是 VIP,优先处理”Content-Type
→ “信纸是 PDF 格式”
没有这些标签,邮局(服务器)就不知道怎么处理这封信。
四、实战避坑指南
❌ 错误1:忘记设置 Content-Type
// ❌ 后端可能无法解析
fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ name: 'Alice' })
})
// ✅ 必须声明
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 🔍 告诉后端这是 JSON
},
body: JSON.stringify({ name: 'Alice' })
})
❌ 错误2:跨域请求缺少 Origin
// ❌ 手动设置 Origin 可能被阻止
fetch('https://api.other.com/data', {
headers: {
'Origin': 'https://myapp.com' // 浏览器会忽略!
}
})
// ✅ 浏览器自动添加 Origin(在跨域请求时)
fetch('https://api.other.com/data') // 自动带 Origin
📌 注意:
Origin
由浏览器在跨域请求时自动添加,不能手动设置。
❌ 错误3:压缩未启用
// ❌ 默认可能不压缩
fetch('/api/large-data')
// ✅ 显式声明支持压缩(现代浏览器默认已加)
// 浏览器自动加: Accept-Encoding: gzip, deflate, br
五、高级用法:条件请求与缓存
1. If-None-Match
/ ETag
:高效缓存
// 第一次请求
// ← 响应头: ETag: "abc123"
// 第二次请求带上 ETag
fetch('/api/config', {
headers: {
'If-None-Match': '"abc123"'
}
})
// ← 如果未修改,返回 304 Not Modified(无响应体)
2. If-Modified-Since
:基于时间的缓存
fetch('/api/report', {
headers: {
'If-Modified-Since': 'Wed, 28 Jul 2025 08:00:00 GMT'
}
})
六、对比主流场景下的请求头组合
场景 | 关键请求头 | 说明 |
---|---|---|
JSON API 请求 | Content-Type: application/json Authorization: Bearer ... | 标准 REST API |
表单提交 | Content-Type: application/x-www-form-urlencoded | 传统 form 提交 |
文件上传 | Content-Type: multipart/form-data | 支持文件和字段混合 |
跨域请求 | Origin Authorization (触发预检) | 触发 CORS 预检 |
服务端请求 | User-Agent: MyApp/1.0 X-API-Key: ... | 区分客户端,用 API Key 认证 |
七、举一反三:三个变体场景实现思路
-
需要实现“断点续传”
使用Range: bytes=0-1023
请求头,服务器返回206 Partial Content
。 -
防止 CSRF 攻击
前端在请求头中添加X-Requested-With: XMLHttpRequest
或自定义头,后端验证。 -
A/B 测试环境切换
通过X-Environment: beta
请求头,让网关路由到测试环境。
fetch('/api/data', {
headers: {
'X-Environment': 'beta',
'X-Request-ID': generateId() // 🔍 用于链路追踪
}
})
小结
HTTP 请求头不是“可有可无的配置”,而是客户端与服务器沟通的“业务语言”。
传数据,定类型(Content-Type);
要认证,加 Authorization;
跨域时,看 Origin;
想缓存,用 ETag;
中文名,编码传(filename)。*