Skip to main content

4. Constructing Responses from Caches (从缓存构造响应)

当收到请求时, 除非满足以下所有条件, 否则缓存禁止 (MUST NOT) 重用已存储的响应:

  • 呈现的目标 URI (参见 [HTTP] 第 7.1 节) 与已存储响应的目标 URI 匹配, 并且

  • 与已存储响应关联的请求方法允许将其用于呈现的请求, 并且

  • 已存储响应指定的请求头部字段 (如果有) 与呈现的请求头部字段匹配 (参见第 4.1 节), 并且

  • 已存储的响应不包含 no-cache 指令 (第 5.2.2.4 节), 除非它已成功验证 (第 4.3 节), 并且

  • 已存储的响应是以下之一:

    • 新鲜的 (参见第 4.2 节), 或

    • 允许作为过期响应提供 (参见第 4.2.4 节), 或

    • 已成功验证 (参见第 4.3 节).

请注意, 缓存扩展可以覆盖上面列出的任何要求; 参见第 5.2.3 节.

当使用已存储的响应来满足请求而不进行验证时, 缓存必须 (MUST) 生成 Age 头部字段 (第 5.1 节), 将响应中存在的任何 Age 字段替换为等于已存储响应的 current_age 的值; 参见第 4.2.3 节.

缓存必须 (MUST) 将具有不安全方法 (参见 [HTTP] 第 9.2.1 节) 的请求直写到源服务器; 即, 在转发请求并收到相应响应之前, 不允许缓存生成对此类请求的回复.

另外, 请注意, 不安全的请求可能会使已存储的响应失效; 参见第 4.4 节.

缓存可以使用已存储或可存储的响应来满足多个请求, 前提是允许为相关请求重用该响应. 这使缓存能够"折叠请求 (Collapse Requests)" -- 或在缓存未命中时将多个传入请求组合成单个转发请求 -- 从而减少源服务器和网络的负载. 但是, 请注意, 如果缓存无法对某些或所有折叠的请求使用返回的响应, 则需要转发这些请求以满足它们, 这可能会引入额外的延迟.

当存储了多个合适的响应时, 缓存必须 (MUST) 使用最新的响应 (由 Date 头部字段确定). 它还可以使用 "Cache-Control: max-age=0" 或 "Cache-Control: no-cache" 转发请求, 以消除使用哪个响应的歧义.

没有时钟 (参见 [HTTP] 第 5.6.7 节) 的缓存必须 (MUST) 在每次使用时重新验证已存储的响应.

4.1 Calculating Cache Keys with the Vary Header Field (使用 Vary 头部字段计算缓存键)

当缓存接收到可以由已存储响应满足的请求, 并且该已存储响应包含 Vary 头部字段 (参见 [HTTP] 第 12.5.5 节) 时, 除非呈现的所有请求头部字段 (由该 Vary 字段值指定) 与原始请求中的那些字段匹配 (即导致缓存响应被存储的请求), 否则缓存禁止 (MUST NOT) 在不重新验证的情况下使用该已存储的响应.

当且仅当第一个请求中的头部字段可以通过应用以下任何操作转换为第二个请求中的头部字段时, 来自两个请求的头部字段被定义为匹配:

  • 在头部字段的语法允许的情况下, 添加或删除空白

  • 组合具有相同字段名称的多个头部字段行 (参见 [HTTP] 第 5.2 节)

  • 以已知具有相同语义的方式规范化两个头部字段值, 根据头部字段的规范 (例如, 当顺序不重要时重新排序字段值; 大小写规范化, 其中值定义为不区分大小写)

如果 (在可能发生的任何规范化之后) 请求中缺少头部字段, 则仅当它在另一个请求中也缺少时才能匹配.

包含成员 "*" 的 Vary 头部字段值的已存储响应始终无法匹配.

如果多个已存储的响应匹配, 缓存将需要选择一个使用. 当指定的请求头部字段具有已知的排序偏好机制 (例如, Accept 和类似请求头部字段上的 qvalues) 时, 该机制可以 (MAY) 用于选择首选响应. 如果没有这样的机制, 或导致同等首选的响应, 则选择最新的响应 (由 Date 头部字段确定), 如第 4 节所述.

某些资源错误地从其默认响应 (即当请求不表达任何偏好时发送的响应) 中省略了 Vary 头部字段, 其效果是即使有更可取的响应可用, 也会为对该资源的后续请求选择它. 当缓存对目标 URI 有多个已存储的响应, 并且其中一个或多个省略了 Vary 头部字段时, 缓存应该 (SHOULD) 选择具有有效 Vary 字段值的最新 (参见第 4.2.3 节) 已存储响应.

如果没有已存储的响应匹配, 缓存无法满足呈现的请求. 通常, 请求被转发到源服务器, 可能添加前提条件来描述缓存已经存储的响应 (第 4.3 节).

4.2 Freshness (新鲜度)

"新鲜 (Fresh)" 响应是其年龄尚未超过其新鲜度生命周期的响应. 相反, "过期 (Stale)" 响应是已超过的响应.

响应的"新鲜度生命周期 (Freshness Lifetime)" 是源服务器生成响应与其过期时间之间的时间长度. "明确过期时间 (Explicit Expiration Time)" 是源服务器打算在没有进一步验证的情况下缓存不再使用已存储响应的时间, 而"启发式过期时间 (Heuristic Expiration Time)" 是在没有明确过期时间可用时由缓存分配的.

响应的"年龄 (Age)" 是自源服务器生成或成功验证以来经过的时间.

当响应是新鲜的时, 它可以用于满足后续请求而无需联系源服务器, 从而提高效率.

确定新鲜度的主要机制是源服务器在未来提供明确的过期时间, 使用 Expires 头部字段 (第 5.3 节) 或 max-age 响应指令 (第 5.2.2.1 节). 通常, 源服务器会为响应分配未来的明确过期时间, 相信表示在到达过期时间之前不太可能以语义上显著的方式改变.

如果源服务器希望强制缓存验证每个请求, 它可以分配过去的明确过期时间以指示响应已经过期. 符合规范的缓存通常会在重用过期的缓存响应之前验证它 (参见第 4.2.4 节).

由于源服务器并不总是提供明确的过期时间, 在某些情况下, 缓存也被允许使用启发式来确定过期时间 (参见第 4.2.2 节).

确定响应是否新鲜的计算是:

response_is_fresh = (freshness_lifetime > current_age)

freshness_lifetime 在第 4.2.1 节中定义; current_age 在第 4.2.3 节中定义.

客户端可以发送 max-age 或 min-fresh 请求指令 (第 5.2.1 节) 来建议对相应响应的新鲜度计算的限制. 然而, 缓存不需要遵守它们.

在计算新鲜度时, 为避免日期解析中的常见问题:

  • 虽然所有日期格式都被指定为区分大小写, 但缓存接收方应该 (SHOULD) 以不区分大小写的方式匹配字段值.

  • 如果缓存接收方的时间内部实现的分辨率低于 HTTP-date 的值, 则接收方必须 (MUST) 在内部将解析的 Expires 日期表示为等于或早于接收值的最近时间.

  • 缓存接收方禁止 (MUST NOT) 允许本地时区影响年龄或过期时间的计算或比较.

  • 缓存接收方应该 (SHOULD) 将带有 "GMT" 以外的时区缩写的日期视为无效以计算过期.

请注意, 新鲜度仅适用于缓存操作; 它不能用于强制用户代理刷新其显示或重新加载资源. 有关缓存和历史机制之间差异的解释, 请参见第 6 节.

4.2.1 Calculating Freshness Lifetime (计算新鲜度生命周期)

缓存可以通过评估以下规则并使用第一个匹配来计算响应的新鲜度生命周期 (表示为 freshness_lifetime):

  • 如果缓存是共享的并且存在 s-maxage 响应指令 (第 5.2.2.10 节), 使用其值, 或

  • 如果存在 max-age 响应指令 (第 5.2.2.1 节), 使用其值, 或

  • 如果存在 Expires 响应头部字段 (第 5.3 节), 使用其值减去 Date 响应头部字段的值 (如果不存在, 则使用接收消息的时间, 如 [HTTP] 第 6.6.1 节所述), 或

  • 否则, 响应中不存在明确的过期时间. 可能适用启发式新鲜度生命周期; 参见第 4.2.2 节.

请注意, 此计算旨在通过尽可能使用源服务器提供的时钟信息来减少时钟偏差.

当给定指令存在多个值时 (例如, 两个 Expires 头部字段行或多个 Cache-Control: max-age 指令), 应该使用第一次出现或将响应视为过期. 如果指令冲突 (例如, 同时存在 max-age 和 no-cache), 应该遵守最严格的指令. 鼓励缓存将具有无效新鲜度信息 (例如, 具有非整数内容的 max-age 指令) 的响应视为过期.

4.2.2 Calculating Heuristic Freshness (计算启发式新鲜度)

由于源服务器并不总是提供明确的过期时间, 当未指定明确时间时, 缓存可以 (MAY) 分配启发式过期时间, 使用使用其他字段值 (例如 Last-Modified 时间) 来估计合理过期时间的算法. 本规范不提供特定算法, 但对其结果施加最坏情况约束.

当已存储响应中存在明确的过期时间时, 缓存禁止 (MUST NOT) 使用启发式来确定新鲜度. 由于第 3 节中的要求, 启发式只能用于没有明确新鲜度且其状态码定义为"启发式可缓存 (Heuristically Cacheable)" 的响应 (例如, 参见 [HTTP] 第 15.1 节) 以及没有明确新鲜度但已标记为明确可缓存的响应 (例如, 使用 public 响应指令).

请注意, 在以前的规范中, 启发式可缓存的响应状态码被称为"默认可缓存 (Cacheable by Default)".

如果响应具有 Last-Modified 头部字段 (参见 [HTTP] 第 8.8.2 节), 鼓励缓存使用不超过自该时间以来间隔的某个分数的启发式过期值. 此分数的典型设置可能是 10%.

注意: HTTP 规范的先前版本 ([RFC2616] 的第 13.9 节) 禁止缓存为具有查询组件 (即包含 "?") 的 URI 计算启发式新鲜度. 在实践中, 这并未得到广泛实施. 因此, 如果源服务器希望防止缓存, 鼓励它们发送明确的指令 (例如, Cache-Control: no-cache).

4.2.3 Calculating Age (计算年龄)

Age 头部字段用于传达从缓存获取的响应消息的估计年龄. Age 字段值是缓存对源服务器生成或验证响应以来的秒数的估计. 因此, Age 值是响应在从源服务器到达的路径上每个缓存中驻留的时间总和, 加上它在网络路径上传输的时间.

年龄计算使用以下数据:

age_value 术语 "age_value" 表示 Age 头部字段 (第 5.1 节) 的值, 以适合算术运算的形式; 或 0, 如果不可用.

date_value 术语 "date_value" 表示 Date 头部字段的值, 以适合算术运算的形式. 有关 Date 头部字段的定义以及对没有它的响应的要求, 请参见 [HTTP] 第 6.6.1 节.

now 术语 "now" 表示此实现的时钟 (参见 [HTTP] 第 5.6.7 节) 的当前值.

request_time 导致已存储响应的请求时的时钟值.

response_time 接收响应时的时钟值.

可以用两种完全独立的方式计算响应的年龄:

  1. "apparent_age": response_time 减去 date_value, 如果实现的时钟与源服务器的时钟合理同步. 如果结果为负, 则将结果替换为零.

  2. "corrected_age_value", 如果响应路径上的所有缓存都实现 HTTP/1.1 或更高版本. 缓存必须 (MUST) 相对于请求发起的时间解释此值, 而不是接收响应的时间.

apparent_age = max(0, response_time - date_value);

response_delay = response_time - request_time;
corrected_age_value = age_value + response_delay;

corrected_age_value 可以 (MAY) 用作 corrected_initial_age. 在可能存在非常旧的缓存实现可能无法正确插入 Age 的情况下, corrected_initial_age 可以更保守地计算为

corrected_initial_age = max(apparent_age, corrected_age_value);

然后可以通过将已存储响应自上次由源服务器验证以来的时间 (以秒为单位) 添加到 corrected_initial_age 来计算已存储响应的 current_age.

resident_time = now - response_time;
current_age = corrected_initial_age + resident_time;

4.2.4 Serving Stale Responses (提供过期响应)

"过期 (Stale)" 响应是具有明确过期信息或允许计算启发式过期的响应, 但根据第 4.2 节中的计算不新鲜.

如果明确的协议内指令禁止, 则缓存禁止 (MUST NOT) 生成过期响应 (例如, 通过 no-cache 响应指令、must-revalidate 响应指令或适用的 s-maxage 或 proxy-revalidate 响应指令; 参见第 5.2.2 节).

除非缓存断开连接或客户端或源服务器明确允许 (例如, 通过第 5.2.1 节中的 max-stale 请求指令、[RFC5861] 中定义的扩展指令或根据带外合同的配置), 否则缓存禁止 (MUST NOT) 生成过期响应.

4.3 Validation (验证)

当缓存对请求的 URI 有一个或多个已存储的响应, 但无法提供其中任何一个 (例如, 因为它们不新鲜, 或无法选择一个; 参见第 4.1 节) 时, 它可以在转发的请求中使用条件请求机制 (参见 [HTTP] 第 13 节) 为下一个入站服务器提供选择有效已存储响应使用的机会, 在此过程中更新已存储的元数据, 或用新响应替换已存储的响应. 此过程称为"验证 (Validating)" 或"重新验证 (Revalidating)" 已存储的响应.

4.3.1 Sending a Validation Request (发送验证请求)

在生成用于验证的条件请求时, 缓存要么从它试图满足的请求开始, 要么 -- 如果它独立发起请求 -- 通过复制方法、目标 URI 和 Vary 头部字段 (第 4.1 节) 标识的请求头部字段, 使用已存储的响应合成请求.

然后, 它使用一个或多个前提条件头部字段更新该请求. 这些包含从具有相同 URI 的已存储响应获取的验证器元数据. 通常, 这将仅包括具有相同缓存键的已存储响应, 尽管允许缓存验证它无法使用正在发送的请求头部字段选择的响应 (参见第 4.1 节).

然后, 接收方比较前提条件头部字段以确定任何已存储的响应是否等效于资源的当前表示.

一个这样的验证器是 Last-Modified 头部字段 (参见 [HTTP] 第 8.8.2 节) 中给出的时间戳, 它可以在 If-Modified-Since 头部字段中用于响应验证, 或在 If-Unmodified-Since 或 If-Range 头部字段中用于表示选择 (即, 客户端特指先前获得的具有该时间戳的表示).

另一个验证器是 ETag 字段 (参见 [HTTP] 第 8.8.3 节) 中给出的实体标签. 一个或多个实体标签 (指示一个或多个已存储的响应) 可以在 If-None-Match 头部字段中用于响应验证, 或在 If-Match 或 If-Range 头部字段中用于表示选择 (即, 客户端特指一个或多个先前获得的具有列出的实体标签的表示).

在生成用于验证的条件请求时, 缓存:

  • 如果正在验证的已存储响应中提供了实体标签, 则必须 (MUST) 发送相关的实体标签 (使用 If-Match、If-None-Match 或 If-Range).

  • 如果请求不是针对子范围, 正在验证单个已存储的响应, 并且该响应包含 Last-Modified 值, 则应该 (SHOULD) 发送 Last-Modified 值 (使用 If-Modified-Since).

  • 如果请求是针对子范围, 正在验证单个已存储的响应, 并且该响应仅包含 Last-Modified 值 (而不是实体标签), 则可以 (MAY) 发送 Last-Modified 值 (使用 If-Unmodified-Since 或 If-Range).

在大多数情况下, 即使实体标签明显优越, 缓存验证请求中也会生成两个验证器, 以允许不理解实体标签前提条件的旧中间件适当地响应.

4.3.2 Handling a Received Validation Request (处理接收到的验证请求)

请求链中的每个客户端可能都有自己的缓存, 因此中间件的缓存接收来自其他 (出站) 缓存的条件请求是很常见的. 同样, 某些用户代理使用条件请求来限制数据传输到最近修改的表示或完成部分检索的表示的传输.

如果缓存接收到可以通过重用已存储的 200 (OK) 或 206 (Partial Content) 响应来满足的请求 (根据第 4 节), 则缓存应该 (SHOULD) 相对于已存储响应中包含的相应验证器评估该请求中接收到的任何适用的条件头部字段前提条件.

缓存禁止 (MUST NOT) 评估仅适用于源服务器的条件头部字段, 出现在语义无法通过缓存响应满足的请求中, 或出现在其没有已存储响应的目标资源的请求中; 这些前提条件可能是为某个其他 (入站) 服务器准备的.

缓存对条件请求的正确评估取决于接收到的前提条件头部字段及其优先级. 总之, If-Match 和 If-Unmodified-Since 条件头部字段不适用于缓存, 并且 If-None-Match 优先于 If-Modified-Since. 有关前提条件优先级的完整规范, 请参见 [HTTP] 第 13.2.2 节.

包含 If-None-Match 头部字段 (参见 [HTTP] 第 13.1.2 节) 的请求表示客户端希望将其自己的一个或多个已存储响应与缓存选择的已存储响应 (根据第 4 节) 进行比较验证.

如果不存在 If-None-Match 头部字段, 包含 If-Modified-Since 头部字段 (参见 [HTTP] 第 13.1.3 节) 的请求表示客户端希望通过修改日期验证其自己的一个或多个已存储响应.

如果请求包含 If-Modified-Since 头部字段, 并且已存储响应中不存在 Last-Modified 头部字段, 则缓存应该 (SHOULD) 使用已存储响应的 Date 字段值 (或者, 如果不存在 Date 字段, 则使用接收已存储响应的时间) 来评估条件.

实现对范围请求的部分响应 (如 [HTTP] 第 14.2 节所定义) 的缓存还需要相对于缓存选择的响应评估接收到的 If-Range 头部字段 (参见 [HTTP] 第 13.1.5 节).

当缓存决定转发请求以重新验证其自己对包含 If-None-Match 实体标签列表的请求的已存储响应时, 缓存可以 (MAY) 将接收到的列表与来自其自己已存储响应集 (新鲜或过期) 的实体标签列表组合, 并将两个列表的并集作为转发请求中的替换 If-None-Match 头部字段值发送. 如果已存储的响应仅包含部分内容, 则缓存禁止 (MUST NOT) 在并集中包含其实体标签, 除非请求针对的范围将完全由该部分已存储响应满足. 如果对转发请求的响应是 304 (Not Modified) 并且具有不在客户端列表中的实体标签的 ETag 字段值, 则缓存必须 (MUST) 通过重用其相应的已存储响应为客户端生成 200 (OK) 响应, 如 304 响应元数据更新 (第 4.3.4 节).

4.3.3 Handling a Validation Response (处理验证响应)

缓存对条件请求的响应的处理取决于其状态码:

  • 304 (Not Modified) 响应状态码表示可以更新和重用已存储的响应; 参见第 4.3.4 节.

  • 完整响应 (即包含内容的响应) 表示条件请求中指定的已存储响应都不合适. 相反, 缓存必须 (MUST) 使用完整响应来满足请求. 缓存可以 (MAY) 存储这样的完整响应, 但要受其约束 (参见第 3 节).

  • 但是, 如果缓存在尝试验证响应时收到 5xx (Server Error) 响应, 它可以将此响应转发给请求客户端, 或表现得好像服务器未能响应. 在后一种情况下, 缓存可以发送先前存储的响应, 但要受其这样做的约束 (参见第 4.2.4 节), 或重试验证请求.

4.3.4 Freshening Stored Responses upon Validation (验证时更新存储的响应)

当缓存接收到 304 (Not Modified) 响应时, 它需要识别适合使用新信息更新的已存储响应, 然后这样做.

要更新的初始已存储响应集是那些本可以为该请求选择的响应 -- 即, 满足第 4 节中的要求的响应, 除了最后一个要求是新鲜、能够作为过期提供或刚刚验证.

然后, 该初始已存储响应集通过以下第一个匹配进一步过滤:

  • 如果新响应包含一个或多个"强验证器 (Strong Validators)" (参见 [HTTP] 第 8.8.1 节), 则这些强验证器中的每一个都标识要更新的选定表示. 初始集中具有这些相同强验证器之一的所有已存储响应都被标识为更新. 如果初始集中没有一个包含至少一个相同的强验证器, 则缓存禁止 (MUST NOT) 使用新响应来更新任何已存储的响应.

  • 如果新响应不包含强验证器但包含一个或多个"弱验证器 (Weak Validators)", 并且这些验证器对应于初始集的已存储响应之一, 则标识这些匹配的已存储响应中最新的一个进行更新.

  • 如果新响应不包含任何形式的验证器 (例如, 客户端从 Last-Modified 响应头部字段以外的源生成 If-Modified-Since 请求), 并且初始集中只有一个已存储的响应, 并且该已存储的响应也缺少验证器, 则标识该已存储的响应进行更新.

对于每个标识的已存储响应, 缓存必须 (MUST) 使用 304 (Not Modified) 响应中提供的头部字段更新其头部字段, 如第 3.2 节所述.

4.3.5 Freshening Responses with HEAD (使用 HEAD 更新响应)

对 HEAD 方法的响应与使用 GET 发出的等效请求相同, 但不发送内容. 如果更高效的条件 GET 请求机制不可用 (由于已存储响应中不存在验证器) 或即使内容已更改也不希望传输内容, 则可以使用 HEAD 响应的此属性来使缓存的 GET 响应失效或更新.

当缓存为目标 URI 发出入站 HEAD 请求并接收到 200 (OK) 响应时, 缓存应该 (SHOULD) 更新或使其本可以为该请求选择的每个已存储 GET 响应失效 (参见第 4.1 节).

对于每个本可以选择的已存储响应, 如果已存储响应和 HEAD 响应对于任何接收到的验证器字段 (ETag 和 Last-Modified) 具有匹配的值, 并且如果 HEAD 响应具有 Content-Length 头部字段, 则 Content-Length 的值与已存储响应的值匹配, 则缓存应该 (SHOULD) 如下所述更新已存储的响应; 否则, 缓存应该 (SHOULD) 将已存储的响应视为过期.

如果缓存使用 HEAD 响应中提供的元数据更新已存储的响应, 则缓存必须 (MUST) 使用 HEAD 响应中提供的头部字段更新已存储的响应 (参见第 3.2 节).

4.4 Invalidating Stored Responses (使已存储的响应失效)

由于不安全的请求方法 (参见 [HTTP] 第 9.2.1 节) (例如 PUT、POST 或 DELETE) 具有更改源服务器上状态的潜力, 因此需要中间缓存使已存储的响应失效以保持其内容最新.

当缓存接收到对不安全请求方法 (包括安全性未知的方法) 的非错误状态码响应时, 缓存必须 (MUST) 使目标 URI (参见 [HTTP] 第 7.1 节) 失效.

当缓存接收到对不安全请求方法 (包括安全性未知的方法) 的非错误状态码响应时, 缓存可以 (MAY) 使其他 URI 失效. 特别是, Location 和 Content-Location 响应头部字段 (如果存在) 中的 URI 是失效的候选对象; 可以通过本文档中未指定的机制发现其他 URI. 但是, 如果要失效的 URI 的源 (参见 [HTTP] 第 4.3.1 节) 与目标 URI (参见 [HTTP] 第 7.1 节) 的源不同, 则缓存禁止 (MUST NOT) 在这些条件下触发失效. 这有助于防止拒绝服务攻击.

"失效 (Invalidate)" 意味着缓存将删除其目标 URI 与给定 URI 匹配的所有已存储响应, 或将它们标记为"无效 (Invalid)" 并需要在可以发送以响应后续请求之前进行强制验证.

"非错误响应 (Non-Error Response)" 是具有 2xx (Successful) 或 3xx (Redirection) 状态码的响应.

请注意, 这并不能保证全局使所有适当的响应失效; 状态更改请求只会使其经过的缓存中的响应失效.