Nginx 静态资源 304 与 200 反复切换:条件请求排查

    发布时间:2026-09-22 05:00 更新时间:2026-09-22 05:00 阅读量:0

    给客户改完模板样式,自己在浏览器里看着是新的,客户刷新却说还是旧样子;再让他按 Ctrl+F5,好了,过两天又变回去。这类问题八成不是文件没传上去,而是浏览器和 Nginx 之间的缓存协商没谈拢——同一个 CSS 文件,有时返回 304(用本地缓存),有时返回 200(重新下载)。想彻底弄明白,得先知道 304 是怎么来的。

    本文按「原理 → 用 curl 取证 → 定位与修复」的顺序展开,命令都可以直接复制执行,环境不同时以官方文档和实际返回为准。

    条件请求:304 并不是服务器偷懒

    浏览器第一次拿到 index.css 时,Nginx 通常会在响应头里带上两个身份标识:Last-Modified(文件最后修改时间)和 ETag(内容或元信息的指纹)。浏览器把文件连同这两个头一起存进本地缓存。

    下次再请求同一个 URL,浏览器会带上 If-Modified-Since(值就是上次的 Last-Modified)和 If-None-Match(值就是上次的 ETag)。Nginx 拿到后做比对:如果文件没变,就回 304 且不带响应体,浏览器直接用本地副本;如果变了,就回 200 加新内容。这就是「条件请求」,304 不是错误,而是协商成功。

    问题往往出在两者对不上:比如 ETag 变了但浏览器用的是 If-Modified-Since,或者相反,就会出现你看到的 304/200 反复横跳。

    用 curl -I 抓响应头,对比两次请求

    排查的第一步永远是看真实响应,而不是猜。用 curl -I 只取响应头,先看第一次请求返回什么:

    curl -I https://www.example.com/static/css/index.css
    
    

    典型响应片段(以实际环境为准)

    HTTP/2 200 last-modified: Tue, 15 Sep 2026 03:20:11 GMT etag: "68c8a1b3-1a2b" cache-control: max-age=3600 content-type: text/css

    记下 last-modified 和 etag 的值,然后用它们模拟一次条件请求,看服务器是回 304 还是 200:

    curl -I https://www.example.com/static/css/index.css \
      -H 'If-None-Match: "68c8a1b3-1a2b"' \
      -H 'If-Modified-Since: Tue, 15 Sep 2026 03:20:11 GMT'
    
    

    若返回 HTTP/2 304,说明协商正常

    如果这里返回的是 200 而不是 304,说明服务器认为文件已变。继续用 stat 看文件真实时间,并和响应头里的 last-modified 对照:

    stat /www/wwwroot/example.com/static/css/index.css
    ls -l --full-time /www/wwwroot/example.com/static/css/index.css

    有一种很隐蔽的情况:模板更新时用了 rsync、解压覆盖或从 Windows 上传,导致 mtime 被刷成当前时间,但内容其实没变;反过来,某些构建工具每次生成的文件内容一致,mtime 却在变。前者会让所有访客重新下载(流量涨),后者会让浏览器一直拿旧文件(页面错乱)。

    三处常见原因与对应处理

    第一,多台后端或多次发布导致 ETag 不一致。Nginx 默认的 ETag 由 mtime 和文件长度拼成,只要 mtime 变,ETag 就变。如果你的站点前面挂了 CDN 或有多台源站,不同节点的文件时间可能不同,访客一会儿命中这个节点、一会儿命中那个节点,就会出现 304/200 交替。处理办法是统一发布时间、保证各节点文件一致,或按需关闭 ETag 改用 Last-Modified 单一维度:

    location ~* \.(css|js|png|jpg|jpeg|gif|webp|svg|woff2?)$ {
        etag off;
        if_modified_since exact;   # 精确比对,避免秒级误差导致的误判
        expires 7d;
        add_header Cache-Control "public";
    }

    if_modified_since 的取值有 exact、before、off 等,具体语义以 Nginx 官方文档为准;exact 适合静态资源,能减少「时间差一秒就重下」的情况。

    第二,HTML 被长期缓存,引用的还是旧 CSS 地址。很多「样式没更新」其实不是 CSS 本身的问题,而是入口 HTML 被缓存了,页面里引用的仍是带旧版本号的文件名。稳妥做法是给 HTML 设很短的缓存,静态资源用文件名加指纹(如 index.a1b2c3.css)并设长缓存:

    location = /index.html {
        add_header Cache-Control "no-cache";
    }
    
    location ~* \.(css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    这样内容变了文件名就变,浏览器自然会去取新文件,不用依赖 304 协商。

    第三,CDN 与源站缓存策略冲突。CDN 回源后可能按自己的规则改写或忽略 ETag,导致浏览器拿到的标识和源站对不上。排查时先直连源站 IP 用 curl 对比(加 --resolve 指定域名解析到源站,避免改 hosts),再走 CDN 域名请求一次,两边响应头放在一起看差异:

    curl -I https://www.example.com/static/css/index.css \
      --resolve www.example.com:443:203.0.113.10
    
    curl -sI https://www.example.com/static/css/index.css | grep -iE 'etag|last-modified|age|x-cache'

    如果源站有 ETag、CDN 返回却没有,或者 age 很大,就该去 CDN 控制台检查缓存规则和「忽略源站缓存头」之类的开关。

    小结与下一步

    304 与 200 反复切换,本质是浏览器缓存标识和服务器当前文件状态对不上。排查顺序建议固定为:先 curl -I 看两次响应头,再用 stat 核对文件 mtime,然后检查 Nginx 的 etag、if_modified_since、expires 配置,最后才怀疑 CDN。日常维护里,给 HTML 短缓存、给静态资源带指纹的长缓存,比反复纠结 304 更省心。改完配置记得 nginx -t 校验再 reload,避免语法问题把站点带下线。

    继续阅读

    📑 📅
    宝塔面板SSL后www与裸域只生效一个:证书覆盖与server_name分流 2026-09-21
    服务器买多大够用:按日均PV估算CPU内存带宽 2026-09-21
    域名转入转出实操:转移码、60天锁与DNS不断线 2026-09-21
    宝塔面板新建站点访问却是默认页:根目录与index排查 2026-09-21
    WordPress换主题后排版错乱:数据迁移避坑指南 2026-09-21
    宝塔面板开CDN后日志全是节点IP:real_ip落地配置 2026-09-22
    WordPress数据库utf8转utf8mb4:emoji问号的处理步骤 2026-09-22
    服务器只开80/443:SSH端口转发访问面板与数据库 2026-09-22
    换服务器后百度收录掉了:改IP前后的抓取诊断与sitemap动作 2026-09-22
    Nginx 413 上传被拦:client_max_body_size 与多层限制联动排查 2026-09-23