在调试一个跨域上传接口时,后端同事突然找你:"你传的文件名是乱码啊!"
你一脸懵:"我传的是 简历-张三.pdf
,怎么就乱码了?"
查了半天,发现是漏了一个关键请求头:
js
// ❌ 错误写法
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 解码,结果就是乱码。
二、解决方案:用正确的请求头声明编码
js
// 方案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
实际上不起作用!真正的解决方案是:
js
// 方案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. 底层机制:浏览器如何使用这些请求头
我们来画一张 浏览器发送请求的流程图:
关键点:
Host
是 HTTP/1.1 强制要求的头,用于虚拟主机User-Agent
被用于设备识别、兼容性处理Accept-Encoding
触发服务器压缩响应,节省带宽
3. 设计哲学:为什么需要这么多请求头?
HTTP 是"无状态、可扩展 "的协议,请求头就是它的扩展机制。
类比:
HTTP 请求就像一封挂号信,请求头是信封上的标签:
Host
→ 收件人地址User-Agent
→ 寄件人身份Accept
→ "请用中文回复"Authorization
→ "我是 VIP,优先处理"Content-Type
→ "信纸是 PDF 格式"
没有这些标签,邮局(服务器)就不知道怎么处理这封信。
四、实战避坑指南
❌ 错误1:忘记设置 Content-Type
js
// ❌ 后端可能无法解析
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
js
// ❌ 手动设置 Origin 可能被阻止
fetch('https://api.other.com/data', {
headers: {
'Origin': 'https://myapp.com' // 浏览器会忽略!
}
})
// ✅ 浏览器自动添加 Origin(在跨域请求时)
fetch('https://api.other.com/data') // 自动带 Origin
📌 注意:
Origin
由浏览器在跨域请求时自动添加,不能手动设置。
❌ 错误3:压缩未启用
js
// ❌ 默认可能不压缩
fetch('/api/large-data')
// ✅ 显式声明支持压缩(现代浏览器默认已加)
// 浏览器自动加: Accept-Encoding: gzip, deflate, br
五、高级用法:条件请求与缓存
1. If-None-Match
/ ETag
:高效缓存
js
// 第一次请求
// ← 响应头: ETag: "abc123"
// 第二次请求带上 ETag
fetch('/api/config', {
headers: {
'If-None-Match': '"abc123"'
}
})
// ← 如果未修改,返回 304 Not Modified(无响应体)
2. If-Modified-Since
:基于时间的缓存
js
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
请求头,让网关路由到测试环境。
js
fetch('/api/data', {
headers: {
'X-Environment': 'beta',
'X-Request-ID': generateId() // 🔍 用于链路追踪
}
})
小结
HTTP 请求头不是"可有可无的配置",而是客户端与服务器沟通的"业务语言"。
*传数据,定类型(Content-Type);
要认证,加 Authorization;
跨域时,看 Origin;
想缓存,用 ETag;
中文名,编码传(filename)。**