跳到主要内容

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 节), 用等于该已存储响应 current_age 的值替换响应中存在的任何 Age 字段; 见第 4.2.3 节.

缓存 MUST 将使用不安全方法的请求 (见 [HTTP] 第 9.2.1 节) 直写到源服务器; 即, 缓存在转发该请求并收到相应响应之前, 不允许为此类请求生成应答.

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

只要某个已存储或可存储响应允许被重用于相关请求, 缓存 MAY 使用该响应满足多个请求. 这允许缓存"折叠请求 (collapse requests)", 即在缓存未命中期间将多个传入请求组合成一个转发请求, 从而降低源服务器和网络的负载. 但是注意, 如果缓存无法将返回的响应用于部分或全部被折叠请求, 它仍需要转发这些请求来满足它们, 这可能引入额外延迟.

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

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

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

当缓存收到一个可由已存储响应满足的请求, 且该已存储响应包含 Vary 头字段 (见 [HTTP] 第 12.5.5 节) 时, 除非 Vary 字段值指定的所有请求头字段在原始请求 (即导致该缓存响应被存储的请求) 和当前请求中都匹配, 否则缓存 MUST NOT 在不重新验证的情况下使用该已存储响应.

当且仅当第一个请求中的头字段可以通过应用以下任一转换变成第二个请求中的头字段时, 这两个请求的头字段才被定义为匹配:

  • 在头字段语法允许的位置添加或移除空白.

  • 合并具有相同字段名的多个头字段行 (见 [HTTP] 第 5.2 节).

  • 按照该头字段的规范, 以已知具有相同语义的方式规范化两个头字段值 (例如, 当顺序不重要时重新排序字段值; 当值被定义为大小写不敏感时进行大小写规范化).

如果某个头字段在一个请求中不存在, 只有当它在另一个请求中也不存在时才可能匹配.

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

如果多个已存储响应匹配, 缓存需要选择其中一个使用. 当指定的请求头字段具有已知的偏好排序机制时 (例如 Accept 和类似请求头字段中的 qvalues), 该机制 MAY 用于选择首选响应. 如果不存在这样的机制, 或者该机制得出同等首选的响应, 则按第 4 节选择最新响应 (由 Date 头字段确定).

有些资源会错误地在默认响应中省略 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 响应头字段的值 (如果 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 节的要求意味着, 启发式方法只能用于没有显式新鲜度的响应, 且这些响应要么因其状态码而允许启发式缓存 (例如见 [HTTP] 第 15.1 节), 要么已经被显式标记为可缓存 (例如带有 public 响应指令).

注意, 在先前的规范中, 启发式可缓存的响应状态码称为"默认可缓存 (cacheable by default)".

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

Note: HTTP 规范的早期版本 ([RFC2616] 第 13.9 节) 禁止缓存为带有查询组件的 URI (即包含 "?" 的 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 节).

除非缓存处于断开连接状态, 或客户端或源服务器显式允许, 否则缓存 MUST NOT 生成陈旧响应 (例如通过第 5.2.1 节中的 max-stale 请求指令, 通过 [RFC5861] 定义的扩展指令, 或通过依据带外约定进行的配置).

4.3 验证 (Validation)

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

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

生成用于验证的条件请求时, 缓存要么从它正试图满足的请求开始, 要么在独立发起请求时, 使用已存储响应合成一个请求, 方法是复制由 Vary 头字段标识的方法, 目标 URI 和请求头字段 (第 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)

请求链中的每个客户端都可能有自己的缓存, 因此中介缓存常常会收到来自其他 (出站) 缓存的条件请求. 同样, 一些用户代理会发出条件请求, 以将数据传输限制为最近修改的表示或补全部分检索.

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

缓存 MUST NOT 评估仅适用于源服务器的条件头字段, 也 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 通过重用相应的已存储响应, 并用 304 响应元数据更新它 (第 4.3.4 节), 为客户端生成 200 (OK) 响应.

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

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

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

  • 完整响应 (即带有内容的响应) 表示条件请求中指定的所有已存储响应都不合适. 相反, 缓存 MUST 使用该完整响应来满足请求. 在遵守其约束的前提下, 缓存 MAY 存储这样的完整响应 (见第 3 节).

  • 但是, 如果缓存在尝试验证响应时收到 5xx (Server Error) 响应, 它可以将该响应转发给请求客户端, 或者表现得像服务器未响应一样. 在后一种情况下, 缓存 MAY 在满足相关约束的前提下发送先前已存储响应 (见第 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 按第 3.2 节用 304 (Not Modified) 响应中提供的头字段更新其头字段.

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

对 HEAD 方法的响应与使用 GET 发出的等价请求所得到的响应相同, 只是不会发送内容. 如果更高效的条件 GET 请求机制不可用 (因为已存储响应中不存在验证器), 或者即使内容已被修改也不希望传输内容, 则可以利用 HEAD 响应的这一属性来使缓存的 GET 响应失效或更新它.

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

对于每个本可被选择的已存储响应, 如果该已存储响应和 HEAD 响应中任何收到的验证器字段 (ETag 和 Last-Modified) 的值匹配, 并且如果 HEAD 响应具有 Content-Length 头字段, 其值也与已存储响应的值匹配, 则缓存 SHOULD 按如下说明更新该已存储响应; 否则, 缓存 SHOULD 将该已存储响应视为陈旧.

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

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

由于 PUT, POST 或 DELETE 等不安全请求方法 (见 [HTTP] 第 9.2.1 节) 可能改变源服务器上的状态, 中间缓存需要使已存储响应失效, 以保持其内容最新.

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

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

"使失效 (invalidate)" 表示缓存会移除目标 URI 与给定 URI 匹配的所有已存储响应, 或将它们标记为"无效 (invalid)", 并要求在它们可作为后续请求的响应发送之前进行强制验证.

"非错误响应 (non-error response)" 是具有 2xx (Successful) 或 3xx (Redirection) 状态码的响应.

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