为什么浏览器不发送"If-None-Match"头?

19

我正在尝试在PHP中下载(并希望缓存)动态加载的图像。以下是已发送和已接收到的标头:

请求:

GET /url:resource/Pomegranate/resources/images/logo.png HTTP/1.1
Host: pome.local
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 Chrome/25.0.1364.160 Safari/537.22
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: PHPSESSID=fb8ghv9ti6v5s3ekkmvtacr9u5

响应:

HTTP/1.1 200 OK
Date: Tue, 09 Apr 2013 11:00:36 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.14 ZendServer/5.0
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Disposition: inline; filename="logo"
ETag: "1355829295"
Last-Modified: Tue, 18 Dec 2012 14:44:55 Asia/Tehran
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: image/png

当我重新加载URL时,发送和接收的标题完全相同。我的问题是,我应该发送什么样的响应来查看随后请求中的 If-None-Match 标头?

注:我认为这些标头不久前还很好用,虽然我不能确定,但我认为浏览器不再发送 If-None-Match 头部(我曾经看到过该头部)。 我正在使用Chrome和Firefox进行测试,两者都未能发送标题。


2
“Last-Modified: Tue, 18 Dec 2012…”和“Expires: Thu, 19 Nov 1981 08:52:00 GMT”有点相互矛盾,你觉得呢? - CBroe
这是因为我想确保它不会被浏览器缓存。我刚刚将“Expires”设置为“Last-Modified”,并得到了相同的结果。 - Mehran
你的问题一开始说你想要缓存,现在又不想要了? - CBroe
3
抱歉,依我看您只是随意混合各种标头,没有任何逻辑。您说“Cache-Control: no-store, no-cache”,却希望发生缓存? - CBroe
4
这真的很愚蠢,但我花了4个小时在我的.NET Web Api中使用不同的方法来实现它,结果发现我的Chrome开发工具缓存被禁用了。请确保在Chrome中测试时关闭缓存! - Mike Miner
显示剩余3条评论
6个回答

45

同样的问题,相似的解决方案

我一直在试图确定为什么Google Chrome在访问我正在开发的网站时不会发送If-None-Match头信息。(Chrome 46.0.2490.71 m,尽管我不确定版本是否相关。)

这个答案与OP最终引用的(在评论中关于接受的答案),虽然有所不同,但它解决了同样的问题:

当服务器端逻辑通过PHP或类似方式发送ETagLast-Modified头信息时,浏览器在后续请求中不会“按照应该的方式”发送If-None-Match头信息。

先决条件

使用自签名TLS证书会在Chrome中将锁变成红色,改变Chrome的缓存行为。在尝试解决这种问题之前,请按https://dev59.com/z3fZa4cB1Zd3GeqPRnB2#19102293中的说明将自签名证书安装到有效的受信任根存储区,并完全重启浏览器。

第一个顿悟:If-None-Match首先需要来自服务器的ETag

我很快意识到Chrome(以及可能大多数或所有其他浏览器)在服务器已经响应先前请求并发送ETag标头之前,不会发送If-None-Match标头。从逻辑上讲,这是完全合理的;毕竟,Chrome怎么能发送未给定值的If-None-Match呢?
这促使我查看我的服务器端逻辑——特别是当我希望用户代理缓存响应时如何发送标头——以确定为什么第一次请求资源时没有发送ETag标头。我曾经努力在我的应用程序逻辑中包含ETag标头。
我碰巧使用PHP,所以@Mehran(OP)的评论引起了我的注意(他/她说在发送所需的缓存相关标头之前调用header_remove()可以解决问题)。
坦白说,我对这个解决方案持怀疑态度,因为a)我相当确定PHP默认情况下不会发送任何头文件(根据我的配置它确实不会); b)当我在设置自定义缓存头之前调用var_dump(headers_list());时,唯一设置的头是我故意设置的一个头文件。
header('Content-type: application/javascript; charset=utf-8');

所以,没有什么好失去的,我尝试在发送自定义标头之前调用header_remove();。令我惊讶的是,PHP突然开始发送ETag标头!
第二个顿悟:压缩响应会改变其哈希值
然后我像被一袋砖头击中一样,意识到通过在PHP中指定Content-type标头,我告诉NGINX(我使用的Web服务器)将响应GZIP一次,一旦PHP将其交还给NGINX!要明确的是,我指定的Content-type在NGINX的gzip类型列表中。
为了详尽起见,我的NGINX GZIP设置如下,并且PHP通过php-fpm与NGINX连接:
gzip            on;
gzip_min_length 1;
gzip_proxied    expired no-cache no-store private auth;
gzip_types      text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;

gzip_vary on;

我思考为什么NGINX在指定“可gzip压缩的”内容类型时会删除我通过PHP发送的ETag,现在得到了一个很明显的答案:因为NGINX修改了PHP返回的响应正文当NGINX对其进行gzip压缩时!这完全是有道理的;如果不会匹配用于生成它的响应,则发送ETag没有意义。NGINX如此聪明地处理这种情况真是太巧妙了。
我不知道NGINX是否一直足够聪明,不会压缩响应主体,但包含未压缩的ETag头信息,但这似乎就是这里发生的情况。
更新:我找到了解释NGINX在这方面行为的评论,其中引用了两个关于此主题的有价值的讨论。
  1. NGINX论坛讨论该行为的帖子
  2. 在项目仓库中有类似的讨论;请查看评论由Massive Bird于2013年6月15日发布

为了保留这个有价值的解释,如果它不幸消失了,我引用Massive Bird在讨论中的贡献:

Nginx在即时压缩响应时会剥离ETag。 这是根据规范的要求,因为未经压缩的响应与经过压缩的响应不能按字节逐一比较。

然而,NGINX在此方面的行为可能被认为略有缺陷,因为同一规范

“... 还提到了一种称为弱 Etag 的东西(以 W/ 开头的 Etag 值),并告诉我们它可以用于检查响应是否语义等效。在这种情况下,Nginx 不应该对其进行处理。不幸的是,这个检查从未被纳入源代码树中 [引文现在充斥着垃圾邮件]。”
“我不确定 NGINX 在这方面的当前态度,特别是它是否已经添加了对“弱”Etag 的支持。”
“那么,有什么解决办法呢?如何将 ETag 返回到响应中?在 PHP 中进行 gzip 压缩,这样 NGINX 就会看到响应已经被压缩,并且只需保留 ETag 标头而将其传递即可:”
ob_start('ob_gzhandler');

在发送头部和响应正文之前,我添加了这个调用,PHP开始在每个响应中发送ETag值。是的!

其他经验教训

以下是我从研究中获得的一些有趣信息。当尝试测试服务器端缓存实现时,无论是在PHP还是其他语言中,这些信息都非常有用。

Chrome及其开发者工具"Net"面板会根据请求的发起方式而表现不同

如果请求是"新鲜的",例如通过按下Ctrl+F5进行刷新,Chrome会发送这些头部:

Cache-Control: no-cache
Pragma: no-cache

当服务器回复 200 OK 时,表示请求成功。

如果只使用 F5 发送请求,则 Chrome 会发送以下标头:

Pragma: no-cache

如果服务器没有新的数据需要返回,那么服务器会响应 304 Not Modified

最后,如果请求是通过点击已经打开的页面上的链接 或者 在Chrome地址栏中输入URL并按下回车键发起的,Chrome会发送以下头信息:

Cache-Control: no-cache
Pragma: no-cache

服务器响应 200 OK (from cache)

虽然这种行为一开始有点令人困惑,如果你不知道它是如何工作的,但这是理想的行为,因为它允许人们非常彻底地测试每种可能的请求/响应方案。

最令人困惑的可能是,Chrome 在向外发出请求时自动插入了 Cache-Control: no-cachePragma: no-cache 标头,而事实上 Chrome 正从其缓存中获取响应(如在 200 OK (from cache) 响应中所示)。

这次经历对我来说相当有启发性,我希望其他人在未来也能从这个分析中获得价值。


好的发现,我勾选了禁用缓存框,发现总是有一个Cache-Control: no-cache头部,最后发现问题就在那个小复选框上... 唉 - James Yang
1
谢谢回复!我也花了一些时间找出为什么ETag不起作用。正如你所说,我启用了gzipping,这影响了我的缓存。干杯! - Ilja Hämäläinen
当ETag不会与用于生成它的响应匹配时,发送ETag没有意义。我不太理解这个...只是因为nginx压缩了内容,为什么不发送etag呢?关于如何生成etag没有规范 - 它们是不透明的。浏览器将在if-none-match中立即将其发送回nginx,nginx将其发送到应用程序(nginx无论如何都不知道应用程序如何生成etag)。而且,应用程序可以继续使用其常规逻辑来查看etag是否匹配。 - Tom Lianza
@TomLianza 我刚刚添加了一个解释。具有相同ETag的两个响应应该是完全相同的,字节对字节,而gzip会引入熵,从而破坏任何这样的比较。您描述的情况涉及“弱”(也称为“语义等效性”)验证,NGINX在剥离ETags时不考虑此类验证。为了转述NGINX开发人员Maxim Dounin的话,在我更新答案时引用的NGINX论坛主题底部,弱ETags在这种情况下很难实现,并且相关的缓存功能仍然存在于Last-Modified缓存验证器中。 - Ben Johnson

18

您的响应头包括Cache-Control: no-store, no-cache,这些防止缓存。

移除这些值(我认为must-revalidate,post-check=0,pre-check=0可以/应该保留 - 它们告诉浏览器与服务器检查是否有更改)。

如果您的资源更改可以仅使用Last-Modified判断,则建议仅使用该标记 - ETag处理起来更复杂(特别是如果您想在自己的PHP脚本中处理它),而Google PageSpeed/YSlow也不建议使用该标记。


似乎由于某些PHP升级,Cache-Control头部已经自动生成而我并不知情。在开始发送我的头部之前,我使用了PHP的header_remove来停止任何不需要的头部。非常感谢。 - Mehran
3
对于其他遇到相同问题的人,即使我删除了CacheControl并重新启动了nginx,Chrome仍然不会尊重Etags。我必须关闭浏览器标签页并重新打开它才能正常工作。 - Homer6

8

为了以后的我留个备忘录……

我遇到了一个类似的问题,我的响应头中发送了ETag,但是HTTP客户端在随后的请求中没有发送If-None-Match标头(这很奇怪,因为前一天还好好的)。

结果发现我正在使用http://localhost:9000进行开发(它不使用If-None-Match)——通过切换到http://127.0.0.1:9000 Chrome1会自动重新开始发送请求中的If-None-Match标头。

此外,请确保Devtools > Network > Disable Cache [ ]没有被选中。

Chrome浏览器版本: Version 71.0.3578.98 (Official Build) (64-bit)

1 我找不到任何文档记录这一点 - 我假设这是Chrome浏览器负责这个逻辑。

chrome dev tools description


请确保您的ETag响应头中包含双引号!例如:ETag: "<etag_value>" - Nick Grealy

0

这件事情发生在我身上有两个原因:

  1. 我的服务器没有发送etag响应头。我更新了我的jetty web.xml文件,通过添加以下内容返回etag:

    <init-param>
        <param-name>etags</param-name>
        <param-value>true</param-value>
    </init-param>
    
  2. 我调用的URL是xml文件的,当我将其更改为html文件时,Chrome开始发送“if-none-match”头!

希望能对某人有所帮助


0

类似问题

我试图使用If-None-Match头部获取条件GET请求,已经提供了正确的Etag头部,但在我尝试的任何浏览器中都无法实现。

经过多次尝试,我意识到浏览器将同一路径的GETPOST视为相同的缓存候选项。因此,即使通过X-Requested-With:"XMLHttpRequest"提供了正确的Etag,也会立即取消具有Cache-Control:"no-cache, private"的相同路径的"POST",这可能对某些人有所帮助。


0

这种情况发生在我身上是因为我设置了缓存大小太小(通过组策略)。

在隐身模式下没有发生这种情况,这让我意识到可能是这个原因。

修复了这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接