Skip to content

问题现象

通过域名访问 telegramdigest.shenzjd.com 显示旧版 4 卡片布局,但通过 IP 直接访问显示新版 2 卡片布局。Vercel 部署正常,Docker 自部署有问题。

<!-- more -->

排查过程

阶段一:怀疑 Next.js ISR 缓存头

观察到 Next.js ISR 生成的响应带有 cache-control: s-maxage=31536000(缓存 1 年),认为 Cloudflare CDN 据此缓存了旧版本 HTML。

尝试方案(全部失败):

提交方法结果
924700a拦截 ServerResponse.prototype.setHeader / writeHead生产环境无效
384fd34拦截 net.Socket.prototype.write 改写原始 HTTP 头无效
f1f6feeDockerfile 清除 .next/cache + socket.write 拦截无效
2bf68f1拦截 socket.writev() 处理 Node.js 22 批量写入无效
e2f5c1d拦截 _storeHeader 改写已序列化的响应头无效

失败原因: Next.js ISR 在框架内部设置缓存头,优先级高于所有外部拦截。自定义 server 中的 prototype patching 在不同 Node.js 版本和代码路径下不可靠。

阶段二:从 Next.js 层面禁用 ISR

提交方法结果
89ff60alayout.tsxexport const dynamic = 'force-dynamic'ISR 仍然生效
75c880b添加 middleware.tsNextResponse.next() 设置 no-storeISR 响应头覆盖了 middleware 的头

发现: 构建输出确认所有路由标记为 ƒ (Dynamic),但运行时 ISR 缓存仍然活跃。middleware 的响应头被 ISR 的 s-maxage 覆盖。

阶段三:定位真正的缓存层

通过对比测试找到关键线索:

bash
# 带查询参数 → 正确
curl -sI "https://telegramdigest.shenzjd.com/?v=2"
# cache-control: no-store, no-cache, must-revalidate, proxy-revalidate
# x-cache: MISS

# 不带参数 → 缓存
curl -sI "https://telegramdigest.shenzjd.com/"
# cache-control: s-maxage=31536000
# x-cache: HIT
# x-nextjs-cache: HIT

逐个分析响应头:

响应头来源含义
cf-cache-status: DYNAMICCloudflare未缓存(Cloudflare 不是问题)
x-nextjs-cache: HITNext.js ISRISR 缓存命中
x-cache: HITOpenResty代理缓存命中

结论: Cloudflare 完全没有缓存。真正的缓存层是 OpenResty (Nginx) 的 proxy_cache。1Panel 的 OpenResty 配置了代理缓存,看到了 Next.js ISR 发出的 s-maxage=31536000 后将响应缓存了 1 年。

阶段四:最终修复

提交方法结果
177cd31middleware 用 NextResponse.rewrite() 添加时间戳参数绕过 ISR有效
typescript
// src/middleware.ts
export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();
  url.searchParams.set('__nocache', Date.now().toString());
  const response = NextResponse.rewrite(url);
  response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
  return response;
}

原理:

  1. rewrite 内部将 / 改写为 /?__nocache=1234567890,浏览器 URL 不变
  2. Next.js ISR 缓存基于完整 URL(含查询参数),唯一 URL 永远不会命中缓存
  3. 响应头 no-store 防止 OpenResty 再次缓存

服务端操作: 手动删除 OpenResty 磁盘缓存目录后 reload。

阶段五:代码清理

今天共产生 12 个调试提交,大部分代码改动无效。清理后:

  • 保留src/middleware.ts(唯一有效修复)
  • 恢复src/server.ts(移除所有 prototype patching hack)
  • 恢复src/app/page.tsx(原始内联仪表盘)
  • 删除dashboard-page.tsx(调试期间创建的临时文件)
  • 恢复Dockerfilenext.config.ts.gitignore

架构分析

请求经过三层缓存,每一层都可能导致问题:

浏览器 → Cloudflare CDN → OpenResty (proxy_cache) → Docker/Next.js (ISR)
           cf-cache-status    x-cache              x-nextjs-cache
           = DYNAMIC ✓        = HIT ❌             = HIT ❌
层级是否缓存排查方法
Cloudflare否 (DYNAMIC)cf-cache-status 响应头
OpenResty (HIT)x-cache 响应头
Next.js ISR是 (HIT)x-nextjs-cache 响应头

经验总结

  1. 先看响应头再写代码cf-cache-status: DYNAMIC 早就说明 Cloudflare 没问题,不需要任何 header 拦截
  2. 注意 x-cache — 这是 Nginx/OpenResty proxy_cache 的标志头,不是 Next.js 的
  3. 重启不等于清缓存 — Nginx proxy_cache_path 的磁盘缓存文件在重启后仍然存在,需要手动 rm -rf
  4. Node.js 层面的 header 拦截不可靠 — Next.js ISR 在框架内部序列化响应头,外部 prototype patching 在生产环境下几乎不可能可靠工作
  5. Middleware rewrite 是可靠的 ISR 绕过方式 — 通过改变 URL 绕过 ISR 缓存,而不是试图覆盖 ISR 的响应头