4. 从缓存构造响应
当收到一个请求时, 除非满足以下全部条件, 否则缓存 MUST NOT 重用已存储响应:
-
所呈现的有效请求 URI ([RFC7230] 第 5.5 节) 与已存储响应的有效请求 URI 匹配, 并且
-
与已存储响应关联的请求方法允许它用于所呈现的请求, 并且
-
由已存储响应指定的选择头字段 (如果有) 与所呈现请求中的相应字段匹配 (见第 4.1 节), 并且
-
所呈现请求既不包含 no-cache pragma (第 5.4 节), 也不包含 no-cache 缓存指令 (第 5.2.1 节), 除非已存储响应已成功验证 (第 4.3 节), 并且
-
已存储响应不包含 no-cache 缓存指令 (第 5.2.2.2 节), 除非它已成功验证 (第 4.3 节), 并且
-
已存储响应满足以下任一条件:
-
新鲜 (见第 4.2 节), 或
-
允许以过期状态提供 (见第 4.2.4 节), 或
-
已成功验证 (见第 4.3 节).
-
注意, 上述任何要求都可以被缓存控制扩展覆盖; 见第 5.2.3 节.
当使用已存储响应在不进行验证的情况下满足请求时, 缓存 MUST 生成 Age 头字段 (第 5.1 节), 并将响应中已有的任何 Age 字段替换为等于该已存储响应的 current_age 的值; 见第 4.2.3 节.
缓存 MUST 将使用不安全方法 ([RFC7231] 第 4.2.1 节) 的请求 write through 到源服务器; 即, 缓存不允许在转发该请求并收到相应响应之前, 为此类请求生成回复.
另外注意, 不安全的请求可能使已存储的响应失效; 见第 4.4 节.
当存储了多个合适的响应时, 缓存 MUST 使用最新的响应 (由 Date 头字段确定). 它也可以带着 "Cache-Control: max-age=0" 或 "Cache-Control: no-cache" 转发该请求, 以消除应使用哪个响应的歧义.
没有可用时钟的缓存 MUST NOT 使用已存储响应, 除非在每次使用时都重新验证它们.
4.1. 使用 Vary 计算二级键
当缓存收到一个请求, 且该请求可以由带有 Vary 头字段 ([RFC7231] 第 7.1.4 节) 的已存储响应满足时, 除非 Vary 头字段指定的所有选择头字段在原始请求 (即与已存储响应关联的请求) 和所呈现请求中都匹配, 否则缓存 MUST NOT 使用该响应.
来自两个请求的选择头字段被定义为匹配, 当且仅当第一个请求中的这些字段可以通过应用以下任一变换转换为第二个请求中的相应字段:
-
在头字段语法允许的位置添加或删除空白
-
合并具有相同字段名的多个头字段 (见 [RFC7230] 第 3.2 节)
-
按其规范以已知具有相同语义的方式对两个头字段取值进行规范化 (例如, 当顺序不重要时重排字段值; 在取值被定义为大小写不敏感时进行大小写规范化)
如果在 (可能进行的任何规范化之后) 某个头字段在请求中不存在, 那么只有当它在另一个请求中也不存在时, 才能与之匹配.
Vary 头字段取值为 "*" 时总是匹配失败.
选择头字段匹配的已存储响应被称为选中响应 (selected response).
如果有多个选中响应可用 (可能包括没有 Vary 头字段的响应), 缓存需要选择其中一个来使用. 当某个选择头字段有已知的取舍机制时 (例如 Accept 及类似请求头字段上的 qvalue), 可以 MAY 使用该机制来选择优先响应; 对于其余部分, 按第 4 节的规定使用最新的响应 (由 Date 头字段确定).
如果没有选中响应可用, 缓存就无法满足所呈现的请求. 通常, 它会以 (可能带条件的; 见第 4.3 节) 请求转发到源服务器.
4.2. 新鲜度
新鲜响应是指其年龄尚未超过其新鲜度生命周期的响应. 相反, 过期响应是指已经超过的响应.
响应的新鲜度生命周期是从它由源服务器生成到其过期时间之间的时间长度. 显式过期时间是源服务器所期望的、已存储响应在此之后不再能被缓存使用而无需进一步验证的时间点; 而启发式过期时间是缓存在没有可用显式过期时间时自行指定的.
响应的年龄是从它由源服务器生成或成功验证以来所经过的时间.
当响应在缓存中是"新鲜的"时, 它可以在不联系源服务器的情况下用于满足后续请求, 从而提高效率.
确定新鲜度的主要机制是由源服务器使用 Expires 头字段 (第 5.3 节) 或 max-age 响应指令 (第 5.2.2.8 节) 提供一个未来的显式过期时间. 通常, 源服务器会为响应指定未来的显式过期时间, 因为它认为该表示在到期之前不太可能发生语义上显著的变化.
如果源服务器希望强制缓存验证每个请求, 它可以指定一个过去的显式过期时间, 以表明该响应已经过期. 合规的缓存在将过期的缓存响应用于后续请求之前, 通常会先验证它 (见第 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 或 UTC 的日期视为无效, 不用于计算过期时间.
注意, 新鲜度仅适用于缓存操作; 它不能用于强制用户代理刷新其显示或重新加载资源. 关于缓存与历史机制之间区别的说明, 见第 6 节.
4.2.1. 计算新鲜度生命周期
缓存可以使用以下第一个匹配项来计算响应的新鲜度生命周期 (记为 freshness_lifetime):
-
如果缓存是共享缓存且存在 s-maxage 响应指令 (第 5.2.2.9 节), 则使用其取值, 或
-
如果存在 max-age 响应指令 (第 5.2.2.8 节), 则使用其取值, 或
-
如果存在 Expires 响应头字段 (第 5.3 节), 则使用其取值减去 Date 响应头字段的取值, 或
-
否则, 响应中不存在显式过期时间. 可能适用启发式新鲜度生命周期; 见第 4.2.2 节.
注意, 该计算不受时钟偏移 (clock skew) 的影响, 因为所有信息都来自源服务器.
当给定指令存在多个取值时 (例如两个 Expires 头字段、多个 Cache-Control: max-age 指令), 该指令的取值被视为无效. 鼓励缓存将新鲜度信息无效的响应视为过期.
4.2.2. 计算启发式新鲜度
由于源服务器并不总是提供显式过期时间, 当未指定显式时间时, 缓存 MAY 指定一个启发式过期时间, 所采用的算法使用其他头字段取值 (例如 Last-Modified 时间) 来估计一个合理的过期时间. 本规范没有给出具体算法, 但对其结果施加了最坏情况约束.
当已存储响应中存在显式过期时间时, 缓存 MUST NOT 使用启发式方法确定新鲜度. 由于第 3 节中的要求, 这意味着实际上, 启发式方法只能用于那些没有显式新鲜度、且其状态码被定义为默认可缓存的响应 (见 [RFC7231] 第 6.1 节), 以及那些没有显式新鲜度但已被标记为显式可缓存的响应 (例如通过 "public" 响应指令).
如果响应有 Last-Modified 头字段 ([RFC7232] 第 2.2 节), 鼓励缓存使用的启发式过期时间不超过自该时间以来间隔的某个比例. 该比例的一个典型设置可能是 10%.
当使用启发式计算新鲜度生命周期时, 如果响应的 current_age 超过 24 小时且尚不存在此类警告, 则缓存 SHOULD 在响应中生成带有 113 warn-code 的 Warning 头字段 (见第 5.5.4 节).
注: [RFC2616] 第 13.9 节禁止缓存为带查询组件的 URI (即包含 '?' 的 URI) 计算启发式新鲜度. 在实践中, 这一点并未被广泛实现. 因此, 如果源服务器希望阻止缓存, 鼓励它们发送显式指令 (例如 Cache-Control: no-cache).
4.2.3. 计算年龄
Age 头字段用于在从缓存获得响应消息时传达该响应的估计年龄. Age 字段值是缓存对自响应由源服务器生成或验证以来秒数的估计. 实质上, Age 取值是响应在从源服务器出发的路径上驻留在每个缓存中的时间之和, 加上它沿网络路径传输所花的时间.
以下数据用于年龄计算:
age_value
术语 "age_value" 表示 Age 头字段 (第 5.1 节) 的取值, 其形式适合进行算术运算; 如果不可用则为 0.
date_value
术语 "date_value" 表示 Date 头字段的取值, 其形式适合进行算术运算. Date 头字段的定义以及关于缺少它的响应的要求, 见 [RFC7231] 第 7.1.1.2 节.
now
术语 "now" 表示"执行计算的主机上时钟的当前取值". 主机应当使用 NTP ([RFC5905]) 或类似协议将其时钟同步到协调世界时.
request_time
产生该已存储响应的请求发出时, 主机上时钟的当前取值.
response_time
收到响应时, 主机上时钟的当前取值.
响应的年龄可以按两种完全独立的方式计算:
-
"apparent_age": 如果本地时钟与源服务器的时钟同步得足够好, 则为 response_time 减去 date_value. 如果结果为负, 则用零代替.
-
"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_initial_age = max(apparent_age, corrected_age_value);
除非缓存对 Age 头字段的取值有信心 (例如, 因为 Via 头字段中没有 HTTP/1.0 跳), 此时 MAY 使用 corrected_age_value 作为 corrected_initial_age.
然后, 已存储响应的 current_age 可以通过将自该已存储响应上次由源服务器验证以来经过的时间 (以秒计) 加到 corrected_initial_age 上来计算.
resident_time = now - response_time;
current_age = corrected_initial_age + resident_time;
4.2.4. 提供过期响应
"过期" (stale) 响应是指具有显式过期信息, 或被允许计算启发式过期, 但按第 4.2 节的计算并不新鲜的响应.
如果被明确的协议内指令所禁止 (例如被 "no-store" 或 "no-cache" 缓存指令、被 "must-revalidate" 缓存响应指令, 或被适用的 "s-maxage" 或 "proxy-revalidate" 缓存响应指令禁止; 见第 5.2.2 节), 则缓存 MUST NOT 生成过期响应.
除非缓存处于断开状态 (即无法联系源服务器, 或以其他方式找到转发路径), 或者明确允许这样做 (例如通过 max-stale 请求指令; 见第 5.2.1 节), 否则缓存 MUST NOT 发送过期响应.
缓存 SHOULD 在过期响应中生成带有 110 warn-code 的 Warning 头字段 (见第 5.5.1 节). 同样, 如果缓存处于断开状态, 缓存 SHOULD 在过期响应中生成 112 warn-code (见第 5.5.3 节).
当转发没有 Age 头字段的响应时, 即使该响应已经过期, 缓存 SHOULD NOT 生成新的 Warning 头字段. 缓存无需验证仅仅在传输途中变为过期的响应.
4.3. 验证
当缓存对某个被请求的 URI 拥有一个或多个已存储响应, 但无法提供其中任何一个 (例如, 因为它们不新鲜, 或者无法被选择; 见第 4.1 节) 时, 它可以在转发的请求中使用条件请求机制 [RFC7232], 让下一个入站服务器有机会选择一个有效的已存储响应来使用 (并在此过程中更新已存储的元数据), 或者用新响应替换已存储响应. 这个过程被称为对已存储响应进行"验证" (validating) 或"重新验证" (revalidating).
4.3.1. 发送验证请求
当为缓存验证发送条件请求时, 缓存会发送一个或多个前置条件 (precondition) 头字段, 其中包含来自其已存储响应的验证器元数据; 接收方随后将其进行比较, 以确定某个已存储响应是否等价于该资源的当前表示.
其中一种验证器是 Last-Modified 头字段 ([RFC7232] 第 2.2 节) 中给出的时间戳, 它可以用在 If-Modified-Since 头字段中进行响应验证, 或者用在 If-Unmodified-Since 或 If-Range 头字段中进行表示选择 (即, 客户端特指某个此前获得的、具有该时间戳的表示).
另一种验证器是 ETag 头字段 ([RFC7232] 第 2.3 节) 中给出的实体标签 (entity-tag). 一个或多个实体标签 (表示一个或多个已存储响应) 可以用在 If-None-Match 头字段中进行响应验证, 或者用在 If-Match 或 If-Range 头字段中进行表示选择 (即, 客户端特指一个或多个此前获得的、具有所列实体标签的表示).
4.3.2. 处理接收到的验证请求
请求链中的每个客户端都可能拥有自己的缓存, 因此中间件处的缓存从其他 (出站) 缓存接收条件请求是很常见的. 同样, 一些用户代理利用条件请求将数据传输限制为最近修改过的表示, 或者完成部分获取的表示的传输.
如果缓存收到一个请求, 且该请求可以通过重用其已存储的某个 200 (OK) 或 206 (Partial Content) 响应来满足, 则缓存 SHOULD 针对选中响应中包含的相应验证器, 评估该请求中收到的任何适用的条件头字段前置条件. 缓存 MUST NOT 评估仅适用于源服务器的条件头字段、出现在语义无法由缓存响应满足的请求中的条件头字段, 或者应用于它没有已存储响应的目标资源的条件头字段; 此类前置条件很可能是给其他 (入站) 服务器的.
缓存对条件请求的正确评估取决于收到的前置条件头字段及其优先级, 如 [RFC7232] 第 6 节所定义. If-Match 和 If-Unmodified-Since 条件头字段不适用于缓存.
包含 If-None-Match 头字段 ([RFC7232] 第 3.2 节) 的请求表明, 客户端希望将它的一个或多个已存储响应与缓存所选出的任何已存储响应进行比较验证. 如果字段值为 "*", 或者字段值是一个实体标签列表且其中至少有一个与选中已存储响应的实体标签匹配, 则缓存接收方 SHOULD 生成 304 (Not Modified) 响应 (使用选中已存储响应的元数据), 而不是发送该已存储响应.
当缓存决定为包含 If-None-Match 实体标签列表的请求重新验证其自身的已存储响应时, 缓存 MAY 将收到的列表与其自身已存储响应集合 (新鲜或过期) 的实体标签列表合并, 并在转发的请求中把这两个列表的并集作为替换的 If-None-Match 头字段值发送. 如果某个已存储响应只包含部分内容, 则缓存 MUST NOT 将其实体标签纳入该并集, 除非该请求所请求的范围能被该部分已存储响应完全满足. 如果对该转发请求的响应是 304 (Not Modified), 且其 ETag 头字段值带有一个不在客户端列表中的实体标签, 则缓存 MUST 通过重用其相应的已存储响应 (按 304 响应的元数据更新后, 见第 4.3.4 节) 为客户端生成 200 (OK) 响应.
如果不存在 If-None-Match 头字段, 则包含 If-Modified-Since 头字段 ([RFC7232] 第 3.3 节) 的请求表明, 客户端希望按修改日期验证它的一个或多个已存储响应. 如果以下任一情况成立, 缓存接收方 SHOULD 生成 304 (Not Modified) 响应 (使用选中已存储响应的元数据): 1) 选中已存储响应的 Last-Modified 字段值早于或等于该条件时间戳; 2) 选中已存储响应中不存在 Last-Modified 字段, 但其 Date 字段值早于或等于该条件时间戳; 或者, 3) 选中已存储响应中既不存在 Last-Modified 也不存在 Date, 但缓存记录其接收时间早于或等于该条件时间戳.
实现了 [RFC7233] 所定义的范围请求部分响应的缓存, 还需要针对其选中的已存储响应评估收到的 If-Range 头字段 ([RFC7233] 第 3.2 节).
4.3.3. 处理验证响应
缓存对条件请求响应的处理取决于其状态码:
-
304 (Not Modified) 响应状态码表明已存储响应可以被更新并重用; 见第 4.3.4 节.
-
完整响应 (即带有载荷主体的响应) 表明条件请求中提名的已存储响应都不合适. 此时, 缓存 MUST 使用该完整响应来满足请求, 并 MAY 替换已存储响应.
-
但是, 如果缓存在尝试验证响应时收到 5xx (Server Error) 响应, 它可以将该响应转发给发起请求的客户端, 或者表现得如同服务器未能响应. 在后一种情况下, 缓存 MAY 发送此前存储的响应 (见第 4.2.4 节).
4.3.4. 验证时刷新已存储响应
当缓存收到 304 (Not Modified) 响应, 并且对同一缓存键已经拥有一个或多个已存储的 200 (OK) 响应时, 缓存需要确定哪些已存储响应被这个新响应更新, 然后用 304 响应中提供的新信息更新这些已存储响应.
要更新的已存储响应通过使用以下第一个匹配项 (如果有) 来确定:
-
如果新响应包含强验证器 (见 [RFC7232] 第 2.1 节), 则该强验证器标识出要更新的选中表示. 所有具有相同强验证器的已存储响应都被选中. 如果没有任何已存储响应包含相同的强验证器, 则缓存 MUST NOT 使用该新响应更新任何已存储响应.
-
如果新响应包含弱验证器, 且该验证器对应于缓存的某个已存储响应, 则在这些匹配的已存储响应中选择最新的一个进行更新.
-
如果新响应不包含任何形式的验证器 (例如客户端根据 Last-Modified 响应头字段以外的来源生成 If-Modified-Since 请求的情况), 且只有一个已存储响应, 并且该已存储响应也缺少验证器, 则该已存储响应被选中进行更新.
如果某个已存储响应被选中进行更新, 则缓存 MUST:
-
删除已存储响应中所有 warn-code 为 1xx 的 Warning 头字段 (见第 5.5 节);
-
保留已存储响应中所有 warn-code 为 2xx 的 Warning 头字段; 并且,
-
使用 304 (Not Modified) 响应中提供的其他头字段, 替换已存储响应中相应头字段的所有实例.
4.3.5. 通过 HEAD 刷新响应
对 HEAD 方法的响应等同于用 GET 发出的等价请求本应得到的响应, 只是它没有主体. HEAD 响应的这一特性可用于使缓存的 GET 响应失效或更新它, 前提是无法使用更高效的条件 GET 请求机制 (因为已存储响应中不存在验证器), 或者即使表示主体已经改变也不希望传输它.
当缓存针对某个请求目标发出入站 HEAD 请求并收到 200 (OK) 响应时, 缓存 SHOULD 更新或使其所有本可被该请求选中的已存储 GET 响应失效 (见第 4.1 节).
对于每个本可被选中的已存储响应, 如果已存储响应与 HEAD 响应在任何收到的验证器字段 (ETag 和 Last-Modified) 上取值匹配, 并且, 如果 HEAD 响应有 Content-Length 头字段, 其 Content-Length 取值与已存储响应的一致, 那么缓存 SHOULD 按下文所述更新该已存储响应; 否则, 缓存 SHOULD 认为该已存储响应已过期.
如果缓存使用 HEAD 响应中提供的元数据更新某个已存储响应, 则缓存 MUST:
-
删除已存储响应中所有 warn-code 为 1xx 的 Warning 头字段 (见第 5.5 节);
-
保留已存储响应中所有 warn-code 为 2xx 的 Warning 头字段; 并且,
-
使用 HEAD 响应中提供的其他头字段替换已存储响应中相应头字段的所有实例, 并将新的头字段追加到已存储响应的头部分, 除非 Cache-Control 头字段另有约束.
4.4. 失效
由于不安全请求方法 ([RFC7231] 第 4.2.1 节) 如 PUT、POST 或 DELETE 有可能改变源服务器上的状态, 中间缓存可以利用它们来保持其内容最新.
当对某个不安全请求方法收到非错误状态码时, 缓存 MUST 使有效请求 URI ([RFC7230] 第 5.5 节) 以及 Location 和 Content-Location 响应头字段中的 URI (如果存在) 失效.
但是, 如果 Location 或 Content-Location 响应头字段中 URI 的主机部分与有效请求 URI ([RFC7230] 第 5.5 节) 中的主机部分不同, 则缓存 MUST NOT 使该 URI 失效. 这有助于防止拒绝服务攻击.
当缓存对某个安全性未知的方法收到非错误响应时, 缓存 MUST 使有效请求 URI ([RFC7230] 第 5.5 节) 失效.
在此, "非错误响应"是指状态码为 2xx (Successful) 或 3xx (Redirection) 的响应. "失效"是指缓存将删除与有效请求 URI 相关的所有已存储响应, 或者将这些响应标记为"无效"、在能够响应后续请求之前需要进行强制验证.
注意, 这并不保证所有适当的响应都已被失效. 例如, 一个改变状态的请求可能使其途经的缓存中的响应失效, 但相关响应仍可能存储在它未途经的其他缓存中.