请求、方法与正文
逐项解释请求目标、方法语义、字段、编码和上传。
本页沿 HTTP 的具体接口和通信行为展开。沿用归档中的消息与代码示例,归档所称的实测仅作为原作者历史记录;2026-10-05 整理未向外部站点发送实验请求,也未操作浏览器。命令输出随服务、网络、版本和代理条件变化。
上一章我们跟着一次请求走完了全程。这一章换个姿势:把请求这封「信」摊在桌上拆开看——信封上写什么地址(URL)、开场白怎么说明来意(方法)、信封上贴了哪些备注(头部)、信纸里装了什么(请求体)。拆完你会发现,HTTP/1.x 的请求是一份格式严格、却人人能读懂的纯文本。
一、先亲眼看一个请求:四段结构
看原始报文不需要抓包工具,两个终端就够:
# 终端 A:监听 8081 端口,把收到的字节原样存盘
nc -l 8081 > get.txt
# 终端 B:向它发一个请求
curl "http://127.0.0.1:8081/search?q=http+protocol&page=2" \
-H "User-Agent: TutorialDemo/1.0"get.txt 里就是一字不差的原始报文:
GET /search?q=http+protocol&page=2 HTTP/1.1
Host: 127.0.0.1:8081
Accept: */*
User-Agent: TutorialDemo/1.0对照这封「信」,HTTP/1.x 请求的四段结构一目了然:
- 请求行(request-line):第一行
GET /search?... HTTP/1.1; - 头部(headers):
Host:、Accept:等,一行一条备注; - 空行:标志头部结束——这是协议规定的分隔符,没有它服务器不知道备注到哪为止;
- 请求体(body):可选。上面这个请求在头部后直接结束,没有正文。按 MDN 的说法,起始行与头部合称消息的 head;有没有 body,由起始行和头部共同决定。
为什么要长出这么规整的结构?最早的 HTTP/0.9(1989–1991)请求只有一行:
GET /my-page.html没有版本号、没有头部、没有状态码,响应只能是 HTML。结构是被需求逼出来的:HTTP/1.0(1996)引入版本号、状态码和头部;HTTP/1.1(1997)加上 Host 头和持久连接(一条连接上连续发多个请求,不必每次重新接线);再往后的 HTTP/2、HTTP/3 又各自更换了报文的底层设计(第 6 章细讲)。请求行末尾的版本号,就是这封信遵循的「书写规范版次」。
二、请求行:做什么、对谁、用什么版本
请求行的语法(RFC 9112 §3)是:方法 + 空格 + 请求目标(request-target)+ 空格 + HTTP 版本。注意方法名区分大小写,get 不等于 GET。
细看会发现:请求目标里只有路径和查询串,域名去哪了?——在 Host 头里。直接向源服务器发请求时必须只发路径与查询串(origin-form),路径为空也要写 /,同时携带 Host 头(RFC 9112 §3.2)。这个分工让一个 IP 能托管多个网站(虚拟主机):服务器先读 Host 认出「你找的是哪家店」,再看路径找「店里的哪件货」。HTTP/1.1 规定所有请求必须带 Host,缺失、重复或值非法都会被 400 拒绝。
请求目标共有四种形式:
| 形式 | 长相 | 出现场景 |
|---|---|---|
| origin-form | /search?q=... | 最常见,直连源服务器 |
| absolute-form | http://example.com/search | 经代理转发 |
| authority-form | example.com:8080 | 仅用于 CONNECT 建隧道 |
| asterisk-form | * | OPTIONS *,针对整台服务器探能力 |
表里的「代理(proxy)」指替客户端转发请求的中转服务器:请求先发给它,它再替你去访问目标站点——企业网络里常见的网关就是一类代理。
澄清一个误解:「HTTP/2 的请求也有
GET / HTTP/2这样的请求行,只是压缩了」——不对。HTTP/2 根本没有文本请求行,起始行的信息变成了四个冒号开头的伪头部(pseudo-headers)。用curl -sv https://example.com/ -o /dev/null能亲眼看到(节选自真实输出):
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: example.com]
* [HTTP/2] [1] [:path: /]行首的 [1] 是流编号(stream),四条伪头部各占一行;它们之后 curl 还会打印还原出的 > GET / HTTP/2 与普通头部——开发者工具里那行「GET / HTTP/2」同样是还原的展示。版本差异留到第 6 章《HTTP 的成长史》展开。
三、方法:向柜台说明来意
走进银行,柜员先问「办理什么业务」。HTTP 方法(method)就是你报的业务类型,服务器听懂来意才知道怎么处理。八种标准方法连同 PATCH 的语义与三项属性如下(依据 RFC 9110、RFC 5789 与 MDN):
| 方法 | 语义 | 安全 | 幂等 | 可缓存 |
|---|---|---|---|---|
| GET | 获取资源的当前表示 | ✓ | ✓ | ✓ |
| HEAD | 同 GET,但只要元数据不要正文 | ✓ | ✓ | ✓ |
| OPTIONS | 查询可用的通信选项 | ✓ | ✓ | ✗ |
| TRACE | 回显请求,用于诊断 | ✓ | ✓ | ✗ |
| PUT | 用请求体整体替换(或创建)资源 | ✗ | ✓ | ✗ |
| CONNECT | 经代理建立隧道(如 HTTPS 过代理) | ✗ | ✗ | ✗ |
| DELETE | 解除资源与 URI 的关联 | ✗ | ✓ | ✗ |
| POST | 按资源自身语义处理提交的数据 | ✗ | ✗ | 条件性 |
| PATCH | 应用一份「怎么改」的修改指令 | ✗ | ✗ | 条件性 |
(PATCH 由 2010 年的 RFC 5789 补充定义;POST/PATCH 的「条件性」缓存需满足 Content-Location 等苛刻条件,绝大多数缓存实现只认 GET 和 HEAD。)
表里「安全」「幂等」两个词必须说准:
安全方法(safe method)指客户端请求的语义为只读:客户端不请求、也不期待服务器状态因此改变。GET、HEAD、OPTIONS、TRACE 是安全方法。它照样允许副作用——服务器记访问日志、点广告触发计费都合法,关键是客户端没请求这些附加行为。区分它的意义:让爬虫、预取(prefetching)等自动检索敢放心发 GET,不必担心「只是看看却改了数据」。
幂等(idempotent)说的是预期效果而非「返回内容一模一样」:同一请求发一次和发 N 次,服务器的预期效果相同。PUT 发两次仍是「资源被替换成这份内容」,两次收到的响应却可能不同。幂等的工程价值是失败重试:连接断开时可安全地自动重发;POST 不行——连点两次「提交订单」,可能创建两份相同的订单。
再拆掉三个常见误解:
✗「POST 用来创建、PUT 用来更新」——按 RFC 语义,POST 是「按资源自身语义处理提交的数据」,发帖、交表单、创建新资源都算(创建成功应返回 201 并在 Location 头给出新地址);PUT 是整体替换,也可首次创建;只改局部用 PATCH,它的请求体是描述「如何改」的指令集,不是完整的新版本。
✗「DELETE 一定物理删除数据」——它只是「解除资源与其当前功能的关联」,类比 UNIX 的 rm 作用于服务器的 URI 映射;数据是否真被销毁由实现决定,回收站、软删除都合法。
✗「GET 绝对不能有请求体」——准确说法是:消息分帧与方法无关,协议层面 GET「可以有」内容,但语义未定义,客户端不应(SHOULD NOT)生成(RFC 9110 §9.3.1),部分实现还会因请求走私(request smuggling,一类利用相邻设备对报文边界判断不一致的攻击)的风险直接拒绝甚至断开连接。别给 GET 发 body,但采用依据是其未定义语义、兼容性和请求走私风险。
动手验证两个「探路」方法:
curl -sI https://example.com/ # HEAD:返回完整头部,没有响应体
curl -s -X OPTIONS -i https://example.com/ # 探测服务器支持的方法第一条返回 HTTP/2 200、content-type、last-modified 和 allow: GET, HEAD——HEAD 正是「没有身体的 GET」,适合测链接有效性、看资源最近修改时间。第二条返回 405 Method Not Allowed:现实中并非所有服务器都开放 OPTIONS,方法语义最终由服务器说了算。
四、URL:写给邮差的地址
RFC 3986 §3 的官方拆解图,五个组件一目了然:
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment- scheme(方案):走哪条「邮路」,网站通常是 https 或 http。它同时决定默认端口:http 是 80、https 是 443(RFC 9110 §4.2),用默认端口时可以省略——这就是网址里几乎看不到
:443的原因。 - authority(授权部分):主机(域名或 IP)加可选端口,是访问服务器的「技术闸门」。
- path(路径):早期对应服务器上的物理文件位置,如今大多是不对应物理现实的抽象路径。
- query(查询串):
?开头、&分隔、=连接键值的补充说明。非字母数字字符要 percent 编码(百分号编码):表单编码的惯例是空格写作+——第一节实验 URL 里的q=http+protocol,就是「http protocol」的空格被编码成+的实例。 - fragment(片段):
#之后的部分。它永远不会随请求发给服务器,只由浏览器本地处理(滚动到某处、跳到视频某秒)。好比邮差只送到门牌号,「进门左转第二个柜子」是写给收件人自己看的;想让参数到达服务器,必须放进 path 或 query。
澄清两个误解:①「URL 和 URI 是两种地址,网页用 URL、接口用 URI」——按 RFC 3986 §1.1.3,URI(统一资源标识符)是大概念,URL(统一资源定位符)是「以网络位置定位资源」的子集,URN 是「以名字标识」的子集,日常开发两者基本混用。②「协议规定 URL 最长 1024(或 2048)字符」——HTTP 对请求行长度没有预定义上限,RFC 9112 只建议至少支持 8000 字节(协议原文的用词是 octet,即 8 比特一组,可当作字节理解);真实限制来自具体软件:Apache 2.4 的请求行默认上限 8190 字节,nginx 默认单个 8K 缓冲区、超出返回 414(URI Too Long),典型场景正是把本该 POST 的大查询硬塞进 GET 的 URL。具体 URL 长度限制来自实现与部署配置。
还有一条安全常识:别把密码等敏感信息放进 URL。URI 天生是拿来分享的——屏幕上可见,还会被打印、收藏,被服务器和代理记录(RFC 9110 §17.9);https://user:password@host/ 这种内嵌凭据的写法也已被废弃。
五、头部:信封上的备注
头部是一行一条的「名称: 值」。字段名不区分大小写,HOST:、host:、Host: 都合法(HTTP/2 传输中统一小写);一行一条、以那个空行收尾。
MDN 按用途把头部粗分三类:请求头(request headers,描述客户端或想获取的资源)、表示头(representation headers,描述 body 的原始形式与编码)、载荷头(payload headers,如内容长度)。另有一组逐跳头(hop-by-hop headers)只对单条连接有意义。下面这张「常客」表按实战出场频率罗列,不严格对应上述分类——比如 Content-Type 属表示头、Content-Length 属载荷头,只因它们只随请求体出现,才和请求头们同场亮相:
| 头部 | 作用 |
|---|---|
| Host | 目标域名;HTTP/1.1 必带,缺失或重复会被 400 拒绝 |
| User-Agent | 我是什么客户端(浏览器/curl 及版本) |
| Accept / Accept-Language | 我能接受哪些格式 / 语言 |
| Cookie | 随身携带的「会员卡」 |
| Authorization | 身份凭证 |
| Referer / Origin | 我从哪个页面来:Referer 带完整路径,Origin 只含协议 + 主机 + 端口 |
| Content-Type / Content-Length | 请求体的格式与字节数 |
(自定义头部沿用 X- 前缀的惯例,已于 2012 年被 RFC 6648 弃用。)
六、请求体:随信寄出的正文
MDN 的表述很直接:只有 PATCH、POST 和 PUT 请求有请求体——GET 是「要东西」,这三个才是「给东西」。(这跟第三节的澄清并不矛盾:MDN 讲的是惯例——谁该带 body;RFC 那句讲的是底线——谁被允许带 body。)有 body 时通常带 Content-Length 告诉对方正文有多少字节。装什么、怎么装,由 Content-Type 决定,三个真实场景:
场景一:表单默认的 application/x-www-form-urlencoded(HTML 表单 enctype 属性的默认值,另两种可选是 multipart/form-data 和 text/plain):
POST /submit HTTP/1.1
Host: 127.0.0.1:8085
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
name=Zhang+San&email=zs%40example.com键值用 = 连接、& 分隔,非字母数字一律 percent 编码:Zhang+San 里的空格变成了 +,zs%40example.com 里的 @ 变成了 %40——也因此它不适合二进制数据。
场景二:上传文件用 multipart/form-data(真实报文节选):
Content-Type: multipart/form-data; boundary=------------------------og1fQXHxZjYWoJCtsi9EkA
--------------------------og1fQXHxZjYWoJCtsi9EkA
Content-Disposition: form-data; name="username"
zhangsan
--------------------------og1fQXHxZjYWoJCtsi9EkA
Content-Disposition: form-data; name="avatar"; filename="upload.txt"
Content-Type: text/plain
hello world
--------------------------og1fQXHxZjYWoJCtsi9EkA--像一个用隔板分开的包裹:boundary 是隔板编号,普通字段和文件各占一格,每格有自己的小头部(Content-Disposition 说明字段名,文件格还带 filename 与自己的 Content-Type),最后一格编号末尾多两个连字符表示结束。
场景三:调用 API 常用 application/json:
POST /api/users HTTP/1.1
Host: 127.0.0.1:8083
Content-Type: application/json
Content-Length: 29
{"name":"Zhang San","age":20}澄清一个误解:「application/json 是提交数据的默认格式」——HTML 表单的默认 enctype 为 application/x-www-form-urlencoded;该默认值限于表单语境(enctype 默认 urlencoded);json 是开发者用 fetch/XHR 调接口时手动设置的,浏览器从不自动生成它。服务器若严格校验 Content-Type,不匹配会返回 415。
七、GET 与 POST:一对最常被误解的搭档
用 MDN 表单教程的同一份数据 say=Hi&to=Mom 做对照:表单用 method="GET" 提交后,地址栏变成 https://www.example.com/?say=Hi&to=Mom,请求行是 GET /?say=Hi&to=Mom HTTP/1.1(MDN 原示例把版本写作 HTTP/2.0,那只是示意——文本请求行只存在于 HTTP/1.x,HTTP/2 起改用伪头部,见第二节);改成 POST 后 URL 不变,数据进了请求体(Content-Type: application/x-www-form-urlencoded、Content-Length: 13、body 为 say=Hi&to=Mom)。两者的真实差异:
| 维度 | GET | POST |
|---|---|---|
| 数据位置 | URL 查询串 | 请求体 |
| 安全 / 幂等 / 可缓存 | 是 / 是 / 是 | 否 / 否 / 仅条件性 |
| 地址栏、书签、历史 | 可见、可收藏 | 不出现 |
| 敏感数据 | 绝不可用(MDN 明确警告) | 仍非加密 |
| 大数据 | 不合适(URL 受实现限制) | 首选(MDN 建议) |
澄清两个误解:①「POST 比 GET 安全,因为数据不显示在 URL 里」——POST 只是把数据从地址栏挪进请求体,不进书签和历史,这减少地址栏、书签等途径的无意泄露;传输保密仍需 HTTPS;明文 HTTP 下抓包两者都直接可见,真正的传输保密要靠 HTTPS。②「GET 的参数只能放 URL、POST 的参数只能放 body」——协议层面没有这种强制:POST 的 URL 上带查询串非常常见(分页、过滤参数照样拼在 URL 上)。两者真正的协议差异,是 safe、幂等、缓存这些语义,以及「GET 不应有 body」。
最后更新于