HTTP 方法及其幂等性

本文内容评论 RFC 7231 第四章节

当我们试图通过浏览器的刷新功能重新加载购物网站的下单页面时,我们时常会遇到类似“要重新显示网页,浏览器可能需要重新提交请求”的提示——这表明用户浏览器认为重复向该网页提交数据可能会造成未预期的副作用(例如重复下单)。而对于另一些,无论用户执行多少次都不会影响系统状态的请求(比如获取用户购物车信息),我们则称它们是幂等(Idempotency)的:这个从数学中引入的术语表示某个元素无论经过多少次特定运算后,结果仍然不变。

https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/On_Off_-_Za%C5%82_Wy%C5%82_%283086204137%29.jpg/640px-On_Off_-_Za%C5%82_Wy%C5%82_%283086204137%29.jpg
对于一种机器的开始/结束按钮来说,它们各自都是幂等的,即无论按相同按钮一次或者更多次,产生的效果是相同的;图片采用 CC-BY-SA 2.0 协议发布

在用户浏览网页的过程中,浏览器通过名为超文本传输协议(Hypertext Transfer Protocol,HTTP)的协议与服务器交互。RFC 7231 为 HTTP 协议定义了一组被称为“请求方法”的动词。这组动词明确请求者发送该请求的目的,以及在成功请求时希望从服务器获得什么结果。我们最熟悉的两种 HTTP 方法是 GETPOST。但是实际上,互联网标准中一共规定了八种不同的 HTTP 方法,它们可能在浏览网页时不常用,但却广泛用于诸如 RESTful API 等其他场景:

方法名描述常用于
GET对于给定的资源传输其当前表示查看网页、获取数据
HEADGET 相同,不过只传输头部以及状态行
POST根据请求载荷部分的内容执行与资源相关的操作上传文件、填写表单、新增数据
PUT根据请求载荷部分的内容替换给定资源的当前表示更新数据
DELETE删除给定资源的当前表示删除数据
CONNECT与服务器上的给定资源建立隧道连接
OPTIONS描述与给定资源有关的传输选项CORS 预检请求
TRACE沿着通往给定资源的路径进行消息回环测试
RFC 7231 中规定的八种不同请求方法,前两列的内容我们忠于 RFC 标准原文翻译得来

RFC 中还存在一个名为“安全方法”的概念。简单来说,任何只读的方法均应属于“安全方法”——服务器收到标明为这些方法的请求不应该导致其资源状态上的改变,或者产生任何的副作用。有趣的是,虽然大多数网络服务器记录包括 GET 在内的所有请求,但即使是网络服务器会因为日志记录堆满而崩溃(副作用之一),这些请求仍然被认为是安全请求。一般地,我们认为 GETHEADOPTIONSTRACE 方法属于安全方法。

用户代理程序(如浏览器,译者注)应当在为用户执行相应操作前区分安全请求与非安全请求,这样,用户便可在执行一个非安全请求之前得知此事。

翻译自 RFC 7231 Chapter 4.2.1, 2014 年

举例:以下请求从 example.com 上获取 user 类型资源 DGideas 的当前状态,该请求是安全的:

GET example.com/user/DGideas

我们应该将“安全方法”与请求的幂等性加强区分:幂等性说的是,同一个操作执行一次与执行多次的效果是相同的。虽然 DELETE 方法会改变服务器上资源的状态(将其删除),但因为执行一次该操作与执行多次该操作造成的结果是相同的,所以 DELETE 方法具有幂等性。“安全方法”不对服务器上的资源状态发生改变,这些方法显然也是幂等的。PUT 方法与 DELETE 类似,也满足幂等性质。

举例:以下请求从 example.com 中尝试删除 fruit 类型资源 banana,该请求是幂等的,因为删除一次 banana 与删除多次 banana 的结果是相同的:

DELETE example.com/fruit/banana?token=I_have_permission

注意到上文的请求携带了名为 token=I_have_permission 的请求参数。一般来说对于可能改变资源状态的请求来说,我们都会向其中添加鉴权机制,以避免任意用户均有权限更改服务器上的资源状态。

讨论 HTTP 方法的上述两种性质的意义在于,区分携带哪些方法的请求可以被重复发送多次是重要的——由于网络波动等无法确认请求是否实际到达的情况下,发送多次幂等请求是安全的。对于上例,在第二次请求时 banana 已经被删除,服务器对于该请求可能会给出“错误!要删除的资源已不存在”的提示,但显然,该重复发起的 DELETE 请求并没有产生任何不良副作用。

在标准中提到的另一个关于 HTTP 请求方法的性质是可缓存性(Cacheable)。这种性质描述诸如浏览器等的客户代理程序是否可以将先前请求的响应结果缓存。浏览器大多会遵循这个指引,这也是为什么在进行 Web 开发时,开发人员时常需要手动使用浏览器的“刷新”功能查看网页最新版本的原因。内容分发网络(Content Delivery Network,CDN)在默认情况下通常也会依此来判断哪些请求是否可被缓存。

……(本文档)指明 GETHEADPOST 是可缓存的,尽管绝大多数的实现只支持 GETHEAD(方法)……

翻译自 RFC 7231 Chapter 4.2.1, 2014 年

对于更高水平的阅读者来说,我们接下来讨论 RFC 标准中关于八种基本请求方法的定义。这对于 HTTP API 比如 RESTful API 的开发者来说是十分有意义的。

GET

GET 请求方法传输给定资源的当前状态。同时,GET 方法也是在信息传输中最常用的方法,其主要关注于(传输)性能上的优化。通常大家在 HTTP 中所说的“获取一些资源/页面/信息”,默认都是指代使用 GET 方法来进行请求。

用户通过统一资源定位符(URL)访问给定服务器上的一系列资源。大多数网页服务器的默认配置是将 URL 映射到服务器自身文件系统的路径中,比如 example.com/index.htm 也许对应服务器 /var/www/html/index.htm 中的资源。而 example.com/download/cat.exe 则默认对应 /var/www/html/download/cat.exe。然而,由于所谓“资源”并不只是指代服务器本身磁盘上的文件,所以网页服务器可以设置一系列地址的转换规则,以实现对于其他位置上资源的访问。正如先前的例子中 example.com/user/DGideas 也许对应服务器后台数据库 user 表中的记录 user_name=DGideas 项,而后台数据库可能位于另一台甚至另一组服务器的不同位置中。资源的映射规则取决于 HTTP 服务器的实现。

HEAD

HEAD 方法与 GET 方法一致,但是对端服务器必须不能在响应中附带消息正文(响应消息在传输响应头部后便结束),除此以外,HEAD 方法响应头部的内容应当与服务器相应 GET 请求时的内容一致。该方法常用于请求者在并不想获取目标资源的情况下测试给定资源是否存在。

由于性质与 GET 方法类似,所以 HEAD 方法也是可缓存的。

POST

POST 方法根据请求载荷部分的内容执行与资源相关的操作,其最常用于以下用途中:

  • 提交一系列数据:比如将 HTML 表单提交至数据处理程序中
  • 将消息发布于公告板、邮件群组、新闻组、博客或是类似的地方
  • 在服务器中创建一个新的资源,以及
  • 向已有资源中追加内容或数据

RFC 标准指导我们,对于 HTTP 服务器来说,应当选择一个合适的 HTTP 状态码表示对 POST 方法请求的处理结果。特别地,如果 POST 请求用于在服务器中创建新的资源,服务器则应该响应 201Created)状态并在响应头中附上创建资源的 URI 地址。另外,如果对于 POST 请求的响应的内容是服务器上某个已经存在的资源时,服务器应当响应 303See Other)将客户端引导至已经存在的资源位置上。

PUT

PUT 方法用于根据请求载荷部分的内容替换给定资源的“当前表示”。标准认为,在对特定资源 URL 成功执行 PUT 请求后,对相同 URL 执行 GET 请求应当能获取到对应资源经过更新后的“最新表示”,就像下例中我们首先改变 user 类型资源 DGideas 的年龄,然后使用 GET 请求获取该资源的最新年龄属性:

PUT example.com/user/DGideas/age
GET example.com/user/DGideas/age

需要注意,如果有多个访问者在并发读写给定资源,则上述行为显然不总是能获取到预期结果。至少,标准中规定的一些有保证的行为是:如果资源原先不存在而被创建,对端服务器应当返回 HTTP 状态码 201Created)来反映这种情况;如果资源原先存在而通过本次 PUT 请求被更新,则服务器应当返回状态码 200OK)或者 204No Content);另外,如果资源被移动到新位置,则应当返回 3xxRedirection)状态码。

我们考虑另一种情况,如果尝试更新的数据和资源本身的类型不匹配,会发生什么?比如,目标资源的类型也许是 "text/html",但用户发起的 PUT 请求中希望更新的数据类型是 "image/jpeg"。作为对端服务器可以尝试将数据类型转换为原始格式存储,也可以将原始数据替换为更新的数据格式(大多数对象存储 API 的行为),也或许会返回 HTTP 错误状态码 409Conflict)或者 415Unsupported Media Type),并配以提示信息以解释为什么会出错。

我们应当理解在超文本传输协议(HTTP)中,并没有规定资源应当以何种状态存储于何处,也没有规定 PUT 请求应当怎样影响资源的状态。换句话说,所有有关资源的具体(存储/实现)细节都是由网页服务器刻意隐藏的。所以,大多数 HTTP 服务器提供了类似 .htaccess 的伪静态功能支持,以允许将资源重定向到另一些 URL 上。

根据标准,POSTPUT 方法的根本区别在于其对待目标资源的内部表示的意图上。POST 请求根据请求自身的语义来更新目标资源的表示,而 PUT 请求则意图去替换目标资源的状态。所以,也许一个 POST 请求可能令一个名为 day_counter 的计数器自增 1,而 PUT 请求只会将该计数器的值替换为指定值。这也从另一个侧面反映了 PUT 请求是幂等的。

虽然 PUT 请求是幂等的,但需要注意在一些特定情况中它仍然可能产生副作用。比如一个服务器通过 URI 表示一个资源的特定版本,比如:

example.com/linux -> example.com/linux/latest
example.com/linux/latest
example.com/linux/5.4
example.com/linux/5.4.1

现在试想我们希望向资源 linux 推送一个新版本,即对 example.com/linux 执行 PUT 请求,对端服务器的处理方式可能既更新了 example.com/linux 对应的资源,也为新推送的版本创建了新版本号,即创建了 example.com/linux/5.4.2 的 URI 条目。这种行为需要理解为 PUT 请求可能带来的副作用。

DELETE

DELETE 方法用于删除给定资源及其功能之间的联系。类似于我们在命令行中常使用的 rm 指令,它将 URI 与能够访问到的资源之间断开联系。我们可以理解为如果对一个资源执行 DELETE 请求后,就再也 GET 不到了。

特别地,如果一个资源存在多种表示形式(比如一个文件的不同版本,如上例中 linux),那么在执行完 DELETE 请求后,这些形式之间会受到怎么样的影响,标准中并未给出明确定义。一种可能的实现是将对应版本的资源隐藏掉(存档),这样虽然以后无法通过 URI 访问到特定资源,但资源实际上仍然存在于数据库中。另外一种实现就像在 GitHub 上删除对应的代码仓库一样:只要执行删除操作,代码库包括所有版本的历史数据就全部被删除了。

RFC 标准中推荐,对于成功接受但还未实际执行的删除操作,服务器应当返回 202Accepted)状态码;对于实际执行但无更多动态可以提供的状态应当以 204No Content)状态码描述;或者,对于已经成功执行删除操作并且返回成功信息的响应应当直接发送 200OK)作结。

CONNECT

CONNECT 方法用于与服务器上的给定资源之间建立隧道连接。隧道(Tunnel)是一种虚拟的点对点连接方式,由一个或多个中继服务器构成。RFC 标准中认为隧道连接可以通过 TLS 加密以提升安全性。

标准制定之初,认为这种 HTTP 方法只应用于充当代理服务器的场景下:对端服务器以 200 状态码表示成功建立隧道连接。不过事实上,大多数的网页服务器都并未支持 CONNECT 方法。

我们通过如下示例解释 CONNECT 方法的具体作用:

CONNECT server.example.com:80 HTTP/1.1
Host: server.example.com:80
Proxy-Authorization: basic aGVsbG86d29ybGQ=

在上例中,客户端首先与 server.example.com:80 进行 HTTP CONNECT 请求(第一行):客户端表示希望与 server.example.com:80 建立隧道连接(第二行)。注意,如果客户端希望 server.example.com 服务器充当代理,则第二行中的目标连接地址可能不同,此时, server.example.com 服务器转发客户端至目的服务器间的全部流量。请求内容的第三行中通过 Proxy-Authorization 字段实现了鉴权功能,即只有经过允许的客户端发送正确的用户名-密码对才有资格请求服务器与其建立隧道连接。

OPTIONS

OPTIONS 方法描述与给定资源有关的传输选项。对端服务器接收到该请求后,将为客户端提供有关资源的传输选项/兼容性/需求等信息,以便客户端在不执行实际操作之前预先了解资源本身的传输需求。最常用于 CORS 预检请求

特别地,标准中允许 OPTIONS 请求针对名为 * 的资源进行请求,以从服务器中获取一般性的传输需求而非特定资源的传输需求。标准中特别指出,如果服务器对于该请求的响应中响应载荷(Payload)部分为空,则必须在响应头中发送 Content-Length0 的字段;类似地,如果请求载荷非空,则请求者必须在请求头中设置对应格式的 Content-Type 字段。

当浏览器中的页面尝试获取跨域资源时,浏览器会在实际请求前预先向对端服务器发送一个 OPTIONS 请求作为预检请求,以检查该页面是否允许向对端服务器请求资源。有关该主题的更多内容,请参阅本博客中《互联网中的跨域资源共享(CORS)策略》一文。

TRACE

正如 RFC 标准中所述,TRACE 方法对请求消息执行远程的、应用程序级的回环请求。说得通俗一点,TRACE 方法就相当于我们进行网络诊断时常用的 Ping 命令。而对端网络服务器收到请求者的 TRACE 请求后,应当回复一个 HTTP 状态码为 200OK)、Content-Type"message/http" 且反映请求已经收到的消息。

标准中要求,在进行 TRACE 请求时必须不能携带任何可能被响应数据包披露的敏感数据。比如,客户端不应该在用于追踪目的服务器是否可达的请求中就将自己的用户凭据和 Cookie 附带在请求内容中。客户端不能在 TRACE 方法中附带消息载荷。

RFC 标准中共定义了八种 HTTP 方法;我们讨论了这些方法的安全性、幂等性和可缓存性;网页浏览器最常使用 GETPOST 请求,但在其他用途中可能用到更多类别;