isr-cache-debug
# 问题现象
通过域名访问 telegramdigest.shenzjd.com 显示旧版 4 卡片布局,但通过 IP 直接访问显示新版 2 卡片布局。Vercel 部署正常,Docker 自部署有问题。
# 排查过程
# 阶段一:怀疑 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 头 | 无效 |
f1f6fee | Dockerfile 清除 .next/cache + socket.write 拦截 | 无效 |
2bf68f1 | 拦截 socket.writev() 处理 Node.js 22 批量写入 | 无效 |
e2f5c1d | 拦截 _storeHeader 改写已序列化的响应头 | 无效 |
失败原因: Next.js ISR 在框架内部设置缓存头,优先级高于所有外部拦截。自定义 server 中的 prototype patching 在不同 Node.js 版本和代码路径下不可靠。
# 阶段二:从 Next.js 层面禁用 ISR
| 提交 | 方法 | 结果 |
|---|---|---|
89ff60a | 在 layout.tsx 加 export const dynamic = 'force-dynamic' | ISR 仍然生效 |
75c880b | 添加 middleware.ts 用 NextResponse.next() 设置 no-store 头 | ISR 响应头覆盖了 middleware 的头 |
发现: 构建输出确认所有路由标记为 ƒ (Dynamic),但运行时 ISR 缓存仍然活跃。middleware 的响应头被 ISR 的 s-maxage 覆盖。
# 阶段三:定位真正的缓存层
通过对比测试找到关键线索:
# 带查询参数 → 正确
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: DYNAMIC | Cloudflare | 未缓存(Cloudflare 不是问题) |
x-nextjs-cache: HIT | Next.js ISR | ISR 缓存命中 |
x-cache: HIT | OpenResty | 代理缓存命中 |
结论: Cloudflare 完全没有缓存。真正的缓存层是 OpenResty (Nginx) 的 proxy_cache。1Panel 的 OpenResty 配置了代理缓存,看到了 Next.js ISR 发出的 s-maxage=31536000 后将响应缓存了 1 年。
# 阶段四:最终修复
| 提交 | 方法 | 结果 |
|---|---|---|
177cd31 | middleware 用 NextResponse.rewrite() 添加时间戳参数绕过 ISR | 有效 |
// 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;
}
原理:
rewrite内部将/改写为/?__nocache=1234567890,浏览器 URL 不变- Next.js ISR 缓存基于完整 URL(含查询参数),唯一 URL 永远不会命中缓存
- 响应头
no-store防止 OpenResty 再次缓存
服务端操作: 手动删除 OpenResty 磁盘缓存目录后 reload。
# 阶段五:代码清理
今天共产生 12 个调试提交,大部分代码改动无效。清理后:
- 保留:
src/middleware.ts(唯一有效修复) - 恢复:
src/server.ts(移除所有 prototype patching hack) - 恢复:
src/app/page.tsx(原始内联仪表盘) - 删除:
dashboard-page.tsx(调试期间创建的临时文件) - 恢复:
Dockerfile、next.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 响应头 |
# 经验总结
- 先看响应头再写代码 —
cf-cache-status: DYNAMIC早就说明 Cloudflare 没问题,不需要任何 header 拦截 - 注意
x-cache头 — 这是 Nginx/OpenRestyproxy_cache的标志头,不是 Next.js 的 - 重启不等于清缓存 — Nginx
proxy_cache_path的磁盘缓存文件在重启后仍然存在,需要手动rm -rf - Node.js 层面的 header 拦截不可靠 — Next.js ISR 在框架内部序列化响应头,外部 prototype patching 在生产环境下几乎不可能可靠工作
- Middleware rewrite 是可靠的 ISR 绕过方式 — 通过改变 URL 绕过 ISR 缓存,而不是试图覆盖 ISR 的响应头
编辑 (opens new window)
上次更新: 2026/05/28, 14:51:12