📘 第六章 HTTP首部
📄 一句话概括

Expires 给一个绝对时刻,过了就作废。 它靠双方时钟对齐,所以一旦和 Cache-Control: max-age 同时出现,规范规定 max-age 赢、 它被忽略。

📝 本节和前面几节的关系

6.3.1 讲 Cache-Control 时提过一句「max-age 赢过 Expires」,这一节把六种写法摆开实测一遍。

一、六种写法,六种判定

Demo 里同一份正文配六种 Expires: | Expires 写的是 | 判定 | |---|---| | (没有) | 没说过期,缓存自己决定 | | 现在 + 1 小时 | 新鲜 | | 现在 − 1 小时 | 已过期 | | 0 | 非法日期,按已过期处理 | | 2015 年某刻 | 早就过期 | | 未来 + max-age=0 | max-age 赢,立即过期 | 最后一行是关键:Expires 明明指向未来一小时,但因为有 Cache-Control: max-age=0, 实际判定是「立即过期」。两者冲突时,Cache-Control 永远优先。 这也解释了为什么现代站点几乎只用 Cache-Control:它用相对秒数,不依赖时钟,还能表达 no-store、 must-revalidate 这些细腻的意思,而 Expires 只能表达「到某个时刻为止」。

二、Expires: 0 是个历史包袱

很多人写 Expires: 0 想要「立刻过期」。 它确实常被这么处理,但那靠的是接收方容错——0 不是合法的 HTTP 日期。 Demo 里那行响应 202 字节,比正经日期那行的 230 字节少 28 字节,少的就是日期串的长度。 能跑,但不该依赖。 想表达「别缓存」,正确写法是 Cache-Control: no-store。 另一种是 Cache-Control: max-age=0, must-revalidate:可以存,但每次都得问。 这两者意思不同,别混用。

三、时钟这个隐患

Expires 是绝对时刻,靠双方时钟对齐。服务器时钟慢一小时,所有资源的过期时间就都晚一小时; 客户端时钟快三天,它那边的缓存直接全废。 这在单机测试里几乎测不出来,一上 CDN、一跨时区就冒出来。 而 max-age 是相对秒数,收到那一刻开始算,跟谁都对得上。 所以 Expires 今天的定位很清楚:只在必须兼容 HTTP/1.0 客户端时才写, 而且是作为 Cache-Control 的补充,不是替代。

四、动手 Demo:六种 Expires,看谁说了算

同一份正文,配六种不同的 Expires(其中一种还叠加了 Cache-Control),量出每条响应的大小, 逐条判断该被判成新鲜还是过期。

def http_date(offset):
    return time.strftime(u'%a, %d %b %Y %H:%M:%S GMT',
                           time.gmtime(time.time() + offset))

cases = [
    ([], u'(没有)'),
    ([(u'Expires', http_date(3600))], u'现在 + 1 小时'),
    ([(u'Expires', http_date(-3600))], u'现在 - 1 小时'),
    ([(u'Expires', u'0')], u'0'),
    ([(u'Expires', http_date(3600)),
      (u'Cache-Control', u'max-age=0')], u'未来 + max-age=0'),
]

实跑输出:

--------------------------------------------------------------------
Expires:一个绝对时刻,过了就作废
--------------------------------------------------------------------
  Expires 写的是           整条响应   判定
  (没有)                 190        没说过期,缓存自己决定
  现在 + 1 小时            230        新鲜
  现在 − 1 小时            230        已过期
  0                        202        非法日期,按已过期处理
  2015 年某刻              230        早就过期
  未来 + max-age=0         256        max-age 赢,立即过期

  两个坑。第一个:`Expires: 0` 不是「立刻过期」的标准写法,
  它是个非法日期,靠接收方容错才按过期处理。想表达「别缓存」,
  该写 `Cache-Control: no-store` 或者 `max-age=0, must-revalidate`。

  第二个,也是更要紧的:Expires 是**绝对时刻**,靠双方时钟对齐。
  服务器时钟慢了一小时,所有资源的过期时间就都晚一小时;
  客户端时钟快了三天,缓存直接全废。max-age 是相对秒数,没这问题——
  所以两者同时出现时,规范规定 max-age 优先,Expires 被忽略。
  最后一行实测的就是这件事。

六行的响应大小是 190、230、230、202、230、256 字节。差的那几十字节,全是日期串本身的长度。 Expires: 0 那行 202 字节, 比正经日期少了 28 字节——它就是靠「接收方认得这是个约定俗成的非法值」才工作的。 最后一行 256 字节,多出来的是 Cache-Control 那一行。 多花这几十字节,换来的是「不依赖时钟」这个性质。 六种写法里唯一能被现代实现完全信任的是最后一行:max-age 说了算, Expires 只是给老客户端看的兼容项。

30 秒实验

  1. 把 Expires 写成 Thu, 01 Jan 1970 00:00:00 GMT,看判定是不是「已过期」。
  2. 把 max-age=0 改成 max-age=3600,看判定会不会反过来。
  3. 想一想:如果服务器时钟比客户端慢一天,Expires 会造成什么后果?

常见误区

  1. 「Expires: 0 是标准写法」——它是非法日期,靠容错才生效。
  2. 「Expires 和 max-age 谁写在后谁生效」——固定是 max-age 优先。
  3. 「Expires 更精确」——它是绝对时刻,受时钟影响,反而不稳。
自测三问(点开对答案) 1. `Expires` 和 `Cache-Control: max-age` 同时出现,听谁的? —— 听 max-age,`Expires` 被忽略。 2. 想表达「一点都别缓存」,该写什么? —— `Cache-Control: no-store`。 3. `Expires` 最大的隐患是什么? —— 它是绝对时刻,依赖双方时钟对齐。

过期时间有两个说法,一个是 Expires 的绝对时刻,另一个是 Last-Modified 的「上次动过」。

相关笔记