这是内容与编码阶段的最后一篇。前面讲了内容协商、压缩、分块、范围请求,这一篇聚焦一个高频场景:表单提交和文件上传。它看起来简单(填表点提交),但背后的 body 编码方式(enctype)有三种,选错了文件传不上去。
enctype:表单提交的三种 body 格式
HTML <form> 的 enctype 属性决定表单数据用什么格式编码进 body。三种选择:
第一种:application/x-www-form-urlencoded(默认)
最常见,所有表单默认用它。格式是把字段编码成 key=value&key2=value2,特殊字符做 URL 编码(空格变 + 或 %20,中文变 %E4%B8%AD)。
POST /submit HTTP/1.1
Content-Type: application/x-www-form-urlencoded
name=%E5%8D%81%E4%B8%89&age=30
它的问题:不能传二进制文件。因为 URL 编码只处理文本,文件里的二进制字节没法安全地塞进这种格式(强行编码会膨胀且可能损坏)。所以纯文本字段用它没问题,但文件上传不行。
第二种:multipart/form-data(文件上传必选)
传文件必须用它。第 5 篇讲过,multipart 用 boundary 分隔多段,每段可以有不同的 Content-Type,文件不编码直接以二进制原字节传输。
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4
------WebKitFormBoundary7MA4
Content-Disposition: form-data; name="title"
我的文章
------WebKitFormBoundary7MA4
Content-Disposition: form-data; name="file"; filename="a.png"
Content-Type: image/png
(PNG 二进制原字节)
------WebKitFormBoundary7MA4--
它为什么能传文件?因为每段独立,文件那段的 Content-Type 是 image/png,内容是原始二进制,不需要 URL 编码,不膨胀、不损坏。
第三种:text-plain(几乎不用)
enctype="text/plain" 把表单数据当纯文本发,格式是 key=value 换行,不做编码。因为不做编码,特殊字符原样发,服务器解析困难,而且不安全。基本没人用,知道有这么个选项就行。
为什么文件上传必须用 multipart
这是这一篇的核心问题。很多人疑惑:为什么不能用 urlencoded 传文件?
原因是 URL 编码处理不了二进制。文件是任意字节序列(0x00 到 0xFF),urlencoded 格式要求值是「能放进 key=value&key2=value2 这种文本结构」的。把二进制塞进去有两个办法,都不行:
- 直接塞:二进制里的
&、=、\r\n会被当成格式分隔符,解析全乱。 - 编码后塞(如 base64):每个字节变成文本,膨胀 33%,而且整个 body 成了一个巨大的字符串,服务器要整体解码,内存压力大。
multipart 的设计专门解决这个问题:用 boundary 分隔,每段独立,文件段带自己的 Content-Type,内容是原始二进制。不编码、不膨胀、不损坏。这就是文件上传必须用 multipart 的根本原因。
fetch 提交表单:FormData
传统表单和现代 fetch 在提交行为上有代际差异,先看清楚再选:
现代前端用 fetch 提交表单,对应的是 FormData 对象。它自动处理 enctype:
FormData默认用 multipart 格式提交。- 设置
fetch(url, { method: 'POST', body: formData }),浏览器自动设置Content-Type: multipart/form-data; boundary=...,自动生成 boundary,自动处理二进制文件。
你不需要手动拼 multipart body——浏览器替你做。这也是为什么手写 multipart 容易出错(boundary 要唯一、每段格式要严格),交给 FormData 最稳妥。
取舍与边界
表单提交有几个实践要点:
- 选 enctype 的规则:纯文本字段用默认 urlencoded;有文件用 multipart;想发 JSON 用 fetch 手动设
Content-Type: application/json。 - multipart 的 boundary 必须唯一。它要选一个「body 内容里绝对不会出现」的字符串。浏览器自动生成的 boundary 足够随机,手写时要用足够长的随机串。
- 文件大小限制。multipart 不限制单个文件大小(协议层),但服务器有
client_max_body_size(Nginx 默认 1MB),上传大文件要调服务器配置,或者用分片上传。 - CSRF 风险。表单提交天然容易被 CSRF(跨站请求伪造),因为浏览器会自动带 Cookie。防御靠 CSRF Token,这是安全阶段的内容。
收束:内容与编码阶段完成
到这里,内容与编码阶段讲完了:内容协商(返回什么格式)、压缩(怎么变小)、分块(怎么流式)、范围请求(怎么取一段)、表单提交(怎么上传数据)。这五块覆盖了 HTTP body 传输的核心场景。
下一篇进安全与边界阶段——HTTP 本身是明文、无状态的,Cookie 给它加了状态,CORS 管跨域,CSP 管内容安全,HSTS 强制加密。这是 HTTP 最贴近 Web 安全的部分。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

