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 秒实验
- 把
Expires写成Thu, 01 Jan 1970 00:00:00 GMT,看判定是不是「已过期」。 - 把
max-age=0改成max-age=3600,看判定会不会反过来。 - 想一想:如果服务器时钟比客户端慢一天,
Expires会造成什么后果?
常见误区
- 「
Expires: 0是标准写法」——它是非法日期,靠容错才生效。 - 「Expires 和 max-age 谁写在后谁生效」——固定是 max-age 优先。
- 「Expires 更精确」——它是绝对时刻,受时钟影响,反而不稳。
自测三问(点开对答案)
1. `Expires` 和 `Cache-Control: max-age` 同时出现,听谁的? —— 听 max-age,`Expires` 被忽略。 2. 想表达「一点都别缓存」,该写什么? —— `Cache-Control: no-store`。 3. `Expires` 最大的隐患是什么? —— 它是绝对时刻,依赖双方时钟对齐。过期时间有两个说法,一个是 Expires 的绝对时刻,另一个是 Last-Modified 的「上次动过」。
- ☐ (本节完,下一节 6.6.10 Last-Modified)