fix(cache): attach path tags to prerender-seeded entries so revalidatePath invalidates them#1688
fix(cache): attach path tags to prerender-seeded entries so revalidatePath invalidates them#1688james-elicx wants to merge 1 commit into
Conversation
…ePath invalidates them (#1486) Build-time prerender seeding (`isrSetPrerenderedAppPage`) and TPR upload were writing cache entries with `tags: []`. Tag-based invalidation could never reach them, so `revalidatePath('/foo')` left the seeded HTML/RSC stale until the natural `revalidateAt` expired. Compute the path-derived implicit tags via `buildAppPageCacheTags(pathname, [])` in both seed paths and forward them through to the cache handler. Closes (in part) #1486 — plain path invalidation. Rewrite-aware invalidation (canonical-vs-rewrite key mismatch) is left for a follow-up.
|
/bigbonk review |
commit: |
|
There was a problem hiding this comment.
Review: fix(cache): attach path tags to prerender-seeded entries
Tested locally — all 93 tests pass across the 3 modified test files. The fix is correct and well-scoped.
Correctness trace
The core bug: isrSetPrerenderedAppPage and buildTprKVPairs wrote entries with tags: []. When revalidatePath('/foo') fires, it calls handler.revalidateTag(encodeCacheTag('_N_T_/foo')), which marks that tag as invalidated. On the next handler.get(), entries are checked against invalidated tags — but entries with no tags can never match, so they persist until natural TTL expiry.
The fix correctly uses buildAppPageCacheTags(pathname, []) at all three seeding sites, which is the same function used by the runtime render path in app-page-cache.ts when writing cache entries after a fresh render. This ensures seeded entries and runtime-rendered entries carry identical tag sets, so revalidatePath invalidates both uniformly.
Tag matching trace for revalidatePath('/foo'):
revalidatePath('/foo')→ tag =_N_T_/foo→handler.revalidateTag('_N_T_/foo')buildAppPageCacheTags('/foo', [])produces['/foo', '_N_T_/foo', '_N_T_/layout', '_N_T_/foo/layout', '_N_T_/foo/page']_N_T_/foois in the set — match. Correct.
One behavioral discrepancy to consider (non-blocking)
isrSet (the runtime write path) always passes tags: tags ?? [] in the context — it explicitly includes an empty array when no tags are provided. The refactored isrSetPrerenderedAppPage uses:
if (tags && tags.length > 0) {
ctx.tags = tags;
}This means when tags is undefined or empty, the context object has no tags key at all, whereas isrSet would pass tags: [].
For the built-in MemoryCacheHandler and KVCacheHandler this is safe — they both handle missing tags gracefully. But a custom CacheHandler implementation that distinguishes between ctx.tags === undefined and ctx.tags === [] could behave differently for seeded vs runtime entries. This is an extremely unlikely edge case and not blocking, but worth noting for future parity. The guard tags.length > 0 is arguably more correct anyway (why store empty metadata?), but it's a deliberate divergence from isrSet's convention.
TPR path normalization
buildTprKVPairs feeds routePath (raw path from Cloudflare analytics) directly into buildAppPageCacheTags. The cache key is built via isrCacheKey("app", routePath, buildId) which normalizes trailing slashes. However, buildAppPageCacheTags does not normalize its input — if analytics returns /foo/ with a trailing slash, the tags would be ['/foo/', '_N_T_/foo/', ...] while revalidatePath('/foo') would produce _N_T_/foo. This is pre-existing (the cache key itself was already built from the raw path), and the filterTrafficPaths function filters paths that don't start with / but doesn't strip trailing slashes. Not a regression from this PR, just documenting the existing gap.
Test quality
Excellent test coverage:
- End-to-end
revalidatePaththrough the KV handler with page/layout/bare variants - Sibling isolation (invalidating
/blog/hellodoesn't affect/blog/world) - Root path
/edge case - Prerender-seeded entry invalidation via
isrSetPrerenderedAppPage - Regression guard documenting legacy no-tags behavior
- Seed cache integration test verifying both HTML and RSC artifacts are invalidated
- TPR tag assertion
The void revalidateTag; at the bottom of kv-cache-handler.test.ts looks like it exists to prevent the import from being tree-shaken or flagged as unused — since revalidateTag is imported but only referenced indirectly via revalidatePath. Minor style nit but not worth changing.
Summary
Clean fix, correct approach, good tests. The PR description accurately scopes what's covered (path invalidation) vs what's deferred (rewrite-aware invalidation). Ship it.
|
Review posted on PR #1688 with approval. Here's a summary of the review: Verdict: Approved The fix is correct and well-tested. Key points from the review:
|
Problem
Build-time prerender seeding (
isrSetPrerenderedAppPage) and TPR upload were writing cache entries withtags: []. Tag-based invalidation could never reach those entries, so after a server action calledrevalidatePath('/foo'), vinext kept serving the stale prerendered HTML/RSC until the naturalrevalidateAtexpiry. This matches the failure reported from the Next.js Deploy Suite (expect.not.toBe(...)— same value persists afterrevalidatePath()).Fix
Compute the path-derived implicit tags via
buildAppPageCacheTags(pathname, [])in both seed paths and forward them through to the cache handler:packages/vinext/src/server/seed-cache.ts— compute tags from the cache pathname and thread them throughseedHtml/seedRsc.packages/vinext/src/server/isr-cache.ts—isrSetPrerenderedAppPageaccepts an optionaltagsarray and forwards it into the cache handler context.packages/vinext/src/cloudflare/tpr.ts— TPR KV pairs now carry the path tags instead oftags: [].With tags attached,
revalidatePath()/revalidateTag()can match and evict prerender-seeded entries.Tests
tests/kv-cache-handler.test.ts— end-to-endrevalidatePath()through the active KV handler, including a prerender-seeded entry, plus a regression guard documenting that an entry seeded without tags is (correctly) not invalidated.tests/seed-cache.test.ts— asserts the seeded HTML/RSC contexts carry the expected path tags and thatrevalidatePath('/posts')evicts both seeded artifacts.tests/tpr-kv-keys.test.ts— asserts TPR-seeded entries carry path tags rather than[].Scope
This covers plain path invalidation. Rewrite-aware invalidation (canonical-vs-rewrite key mismatch — the second failing suite below) is a known follow-up.
Next.js reference tests
Fixes #1486