📘 第二章 简单的HTTP协议
📄 一句话概括

请求行的第二段叫 request-target,有四种写法:路径、绝对 URI、授权形式、星号。 用哪种取决于你在跟谁说话。

📝 本节和前面几节的关系

上一节说请求里没带状态。 这一节看请求行里到底带了什么——那个被称作「URI」的东西,实际有四种形态。

一、四种形态

形态 长什么样 什么时候用
origin-form GET /a/b.html 直连源站,最常见
absolute-form GET http://h/a/b.html 经代理
authority-form CONNECT h:443 建隧道
asterisk-form OPTIONS * 问整台服务器
日常写网页只会遇到第一种。后三种出现在代理、隧道、以及各种「400 Bad Request」的排查现场。
为什么要有绝对形式?
因为代理可能同时服务多个站点,它得知道这个请求要去哪个主机——而 Host 首部在 HTTP/1.0 里没有,
所以老代理靠绝对 URI 判断目标。

二、主机名到底写在哪

HTTP/1.1 强制要求 Host 首部,所以源站可以用「路径形式」加 Host 知道目标。 一台服务器上跑几十个网站,靠的就是 Host 区分——这叫虚拟主机(5.1 节)。 缺 Host 的 HTTP/1.1 请求,服务端应当回 400。 这一条在实测里很值得注意:很多框架默认宽容,缺了也照处理,于是上线后接真实代理才炸。

三、几个容易踩的细节

请求目标区分大小写(路径部分),/Index.html 和 /index.html 是两个资源。域名部分不区分。 查询串里的 ?、&、= 是分隔符,值里要用必须编码。 片段(#)根本不会出现在请求行里——它在浏览器就被截掉了。 路径里的 .. 要小心:服务端必须做归一化再判断权限,否则就是目录遍历漏洞(11.2.6)。

四、动手 Demo:四种请求目标真发一遍

分别用四种形态构造请求行发出去,看服务端收到的第二段是什么。 服务端不做校验,原样打印,这样能看清「线上真正传的是什么」。

forms = [(u'origin-form', u'GET /a/b.html HTTP/1.1'),
         (u'absolute-form', u'GET http://example.com/a/b.html HTTP/1.1'),
         (u'authority-form', u'CONNECT example.com:443 HTTP/1.1'),
         (u'asterisk-form', u'OPTIONS * HTTP/1.1')]
for name, line in forms:
    raw = line.encode() + b'\r\nHost: 127.0.0.1\r\n\r\n'
    s.sendall(raw)

实跑输出:

--------------------------------------------------------------------
请求目标(URI)的四种写法
--------------------------------------------------------------------
  origin-form        GET /a/b.html HTTP/1.1
      服务端收到     GET /a/b.html HTTP/1.1
      用途           直连服务器时用,最常见
  absolute-form      GET http://example.com/a/b.html HTTP/1.1
      服务端收到     GET http://example.com/a/b.html HTTP/1.1
      用途           经代理时用
  authority-form     CONNECT example.com:443 HTTP/1.1
      服务端收到     CONNECT example.com:443 HTTP/1.1
      用途           建隧道时用
  asterisk-form      OPTIONS * HTTP/1.1
      服务端收到     OPTIONS * HTTP/1.1
      用途           问整台服务器能力时用

  这四种形态在 RFC 里叫 request-target,位置都是请求行的第二段。
  日常写网页只会遇到第一种;写代理、抓包、排查 400 错误时才需要后三种。
  一个常见故障:代理配置错了,客户端把绝对 URI 直接发给源站,
  源站要么当路径处理、要么回 400——日志里那行请求行会直接暴露。

四种形态服务端都原样收到了——因为它只是记字节,不做语义校验。 真实服务器会按 RFC 校验,形态不对直接 400。 CONNECT example.com:443 这一行之后,这条连接不再是 HTTP 了,它变成一条透明隧道, 里面跑的是 TLS 字节。所以 CONNECT 的响应必须是 2XX,之后双方开始裸传。 OPTIONS * 里的星号不是通配路径,是「针对整台服务器」的意思。 用它问服务器支持哪些方法,响应里会有 Allow 首部。

30 秒实验

  1. 把 origin-form 那行改成 GET (不带路径),看服务端收到什么。
  2. 发一个 GET /a/../b.html,服务端会收到原样的串——想想为什么必须由服务端归一化。
  3. 发一个带 #frag 的请求行,看服务端收到的是不是原样(会,因为浏览器才负责截掉)。

常见误区

  1. 「请求行里写的一定是完整 URL」——直连时只有路径,主机在 Host 首部。
  2. 「* 是通配符」——在 OPTIONS * 里它表示「整台服务器」。
  3. 「路径大小写无所谓」——Windows 服务器可能无所谓,Linux 上就是两个文件。
自测三问(点开对答案) 1. 直连源站时,请求目标用哪种形态? —— origin-form,只写路径与查询串,例如 `/a/b.html?x=1`。 2. 主机名在 HTTP/1.1 里写在哪? —— `Host` 首部。HTTP/1.0 没有这个要求,所以老代理用绝对 URI 形式。 3. `CONNECT` 之后这条连接还是 HTTP 吗? —— 不是。它变成透明隧道,里面传的是 TLS 之类的原始字节。

请求目标说清了。下一节看第一段:那个动词位置能填什么。

相关笔记