ETags

ETags 是“用于区分相同资源多个表示形式的“不透明验证器””。它们以各种方式用于 HTTP 中,以确定条件请求的结果,如 RFC 7232 中所述。完全理解 ETags 需要对 HTTP 以及资源及其表示形式的细微差别有非常全面的了解。本文档不试图一次解决 ETags 的所有应用,而是解决因其他指南而产生的特定用例。它将随着时间的推移而发展。

ETags 与丢失更新问题

问题

HTTP 基本上是一个在网络连接上传输资源表示形式的系统。一种常见的交互方式是 GET /some/resource,修改表示形式,然后 PUT /some/resource 以更新服务器上的资源。这是一种非常有用且表面上简单的模式,驱动着 OpenStack 及其他许多 API。

这种看似简单的特性具有误导性:如果两个或多个客户端在同一时间范围内对 /some/resource 执行操作,它们可能会遇到 丢失更新问题

  • 客户端 A 和客户端 B 都 GET /some/resource 并对其本地表示形式进行更改。

  • 客户端 B 在时间 1 执行 PUT /some/resource

  • 客户端 A 在时间 2 执行 PUT /some/resource

客户端 B 的更改已丢失。 任何客户端都不会意识到这一点。 这是一个问题。

解决方案

HTTP/1.1 及更高版本为此问题提供了一个名为 ETags 的解决方案。这些为资源的不同的表示形式提供验证器,使其能够直接确定请求或响应提供的表示形式是否与已经拥有的表示形式相同。这在验证缓存的 GET 请求(ETag 回答的问题是“我缓存中的内容与服务器提供的内容相同吗?”)时非常有用,但也有助于避免丢失更新问题。

如果修改上述场景以使用 ETags,它将像这样工作

  • 客户端 A 和客户端 B 都 GET /some/resource,包括一个名为 ETag 的响应头,该头对于两个客户端来说是相同的(我们假设 ETag 为“red57”)。有关 ETag 生成的详细信息,请参见下文。

  • 它们都对其本地表示形式进行更改。

  • 客户端 B 执行 PUT /some/resource 并包含一个名为 If-Match 的头,其值为 red57。由于请求中发送的 ETag 与服务器当前状态的资源生成的 ETag 相同,因此请求成功。

  • 客户端 A 执行 PUT /some/resource 并包含 If-Match 头,其值为 red57。此请求失败(返回 412 响应代码),因为 red57 不再与服务器生成的 ETag 匹配:其当前状态已由客户端 B 的请求更新。

客户端 B 的更改没有丢失,客户端 A 没有无意中更改其预期形式不正确的内容。客户端 A 通过响应代码意识到这一点。 在此阶段,客户端 A 可以选择再次 GET 资源,并将他们的本地表示形式与刚刚检索到的表示形式进行比较,并选择一个行动方案。

详细信息

如果服务接受 PUT 请求并需要避免丢失更新,可以通过以下方式实现

  • 向 GET 请求发送带有 ETag header 的响应(有关表示形式中的类似 ETag 属性的一些讨论,请参见下文)。

  • 要求客户端在处理 PUT 请求时发送带有有效 ETag 的 If-Match header。

  • 在服务器端处理 If-Match header,以比较请求中提供的 ETag 与当前存储表示形式生成的 ETag。如果匹配,则继续执行请求操作,否则,响应 412 状态代码。

注意

ETag 值是一个双引号字符串:"the etag"

注意

If-Match header 可以包含多个 ETags(用逗号分隔)。如果包含多个 ETags,则至少有一个必须匹配才能使请求继续。

注意

代码库的哪个部分负责管理 ETag 和 If-Match header,在很大程度上取决于服务的架构。通常,每个资源的处理器或控制器应该是责任的中心。可能存在可以共享的装饰器或库,但这些内容超出了本文档的范围。早期实施者鼓励编写透明且易于检查的代码,以便将来更容易提取。

注意

ETags 可以是强 ETag 或弱 ETag。请参阅 RFC 7232,了解有关弱 ETag 可能用法的讨论。它们在本文档中未涉及,因为它们的重要性主要与缓存处理有关。强 ETag 表示相同资源的表示形式之间存在字节对字节的等价性。弱 ETag 仅表示语义等价性。

上述每个步骤都需要功能来生成表示形式的 ETags。每当表示形式不同时,ETag 应该不同。RFC 7232#section-2.3.1 提供了有关如何生成良好 ETag 的建议。在实践中,它们应该是

  • 相同资源的不同的形式不同。例如,相同版本资源的 XML 和 JSON 表示形式应该具有不同的 ETags。

  • 版本不同。

  • 不基于在系统重新启动时会发生变化的内容。例如,不基于 inodes 或数据库键,这些键是 int 或其他非通用标识符。

  • 不基于对字符串的哈希,这些字符串没有可靠的排序。例如,使用 md5 或 sha 哈希表示资源的 JSON 字符串很诱人。如果该 JSON 中的排序没有保证,则 ETag 将无用。

理想情况下,它们应该计算速度快,或者如果速度不够快,则易于存储(在写入表示形式时)。最后更新时间戳和 content-type 的哈希可以工作,但前提是更新频率低于时钟更新频率。

注意

ETags 如何有用的许多细节在本文档中未提及。值得阅读 RFC 7232 的全文,以了解其目的、工作方式、边缘情况以及它与其他条件请求处理模式的交互方式。

特殊情况

对于表示单个统一实体的简单资源,上述处理方法效果很好。对于更复杂的资源,情况会变得更加复杂。一些值得考虑的场景

  • 当资源表示资源集合时(例如,GET /resourcesGET /resources/some-id),在使用 ETags 更新该集合中的一个资源的严格过程将是

    • GET /resources 以获取资源列表。

    • 执行一些客户端处理以选择单个资源的 ID。

    • GET /resources/that-id 以获取资源及其 ETag header。

    • 修改本地表示形式。

    • PUT /resources/that-id 并包含一个 If-Match header,其中包含 ETag。

    这可能被认为很繁琐。一种优化方法是在集合资源中单个资源的表示形式中包含一个其值为 ETag 的属性。然后可以跳过上述第二个 GET,因为 ETag 已经可用。

  • 当资源具有子资源时(例如,/image/id 资源包含其内容在 /image/id/metadata 处也可用),希望检索图像资源,然后 PUT 到元数据资源。严格来说,这将需要 GET 元数据资源以确定 ETag。

    如果这是一个问题,那么一种解决方法是允许图像资源的 ETag 作为在 If-Match header 中提供的元数据资源的有效 ETag。如果这样做,则重要的是反之不成立:发送到图像资源的 If-Match header 中发送的元数据资源的 ETag 不应有效。

注意

在上述两种情况下,ETags 的语义都被违反了。ETag 不是解锁资源并使其可写的神奇密钥。它是一个用于确定相同资源的两个表示形式是否确实相同的值。在上述情况下,它们正在比较不同的资源。服务仅应这样做,如果必须这样做。因为性能优势巨大(在这种情况下,请考虑修复 API 的性能)或用户体验改进非常重要。后者比前者更重要和合法