Skip to content

fix(cache): attach path tags to prerender-seeded entries so revalidatePath invalidates them#1688

Open
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1486-revalidate-path
Open

fix(cache): attach path tags to prerender-seeded entries so revalidatePath invalidates them#1688
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1486-revalidate-path

Conversation

@james-elicx
Copy link
Copy Markdown
Member

Problem

Build-time prerender seeding (isrSetPrerenderedAppPage) and TPR upload were writing cache entries with tags: []. Tag-based invalidation could never reach those entries, so after a server action called revalidatePath('/foo'), vinext kept serving the stale prerendered HTML/RSC until the natural revalidateAt expiry. This matches the failure reported from the Next.js Deploy Suite (expect.not.toBe(...) — same value persists after revalidatePath()).

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 through seedHtml/seedRsc.
  • packages/vinext/src/server/isr-cache.tsisrSetPrerenderedAppPage accepts an optional tags array and forwards it into the cache handler context.
  • packages/vinext/src/cloudflare/tpr.ts — TPR KV pairs now carry the path tags instead of tags: [].

With tags attached, revalidatePath() / revalidateTag() can match and evict prerender-seeded entries.

Tests

  • tests/kv-cache-handler.test.ts — end-to-end revalidatePath() 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 that revalidatePath('/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

…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.
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1688

commit: 599c683

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_/foohandler.revalidateTag('_N_T_/foo')
  • buildAppPageCacheTags('/foo', []) produces ['/foo', '_N_T_/foo', '_N_T_/layout', '_N_T_/foo/layout', '_N_T_/foo/page']
  • _N_T_/foo is 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 revalidatePath through the KV handler with page/layout/bare variants
  • Sibling isolation (invalidating /blog/hello doesn'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.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 29, 2026

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:

  1. Correctness confirmed — Traced the tag matching logic end-to-end. buildAppPageCacheTags(pathname, []) produces the exact tag set that revalidatePath queries against, and it's the same function the runtime render path uses, ensuring parity between seeded and runtime-rendered entries.

  2. Non-blocking observation — Noted a minor behavioral discrepancy: isrSet always passes tags: tags ?? [] (explicit empty array) while the refactored isrSetPrerenderedAppPage omits the tags key entirely when empty. Safe for built-in handlers, but a subtle divergence in convention.

  3. Pre-existing TPR gap documentedbuildTprKVPairs feeds raw analytics paths into buildAppPageCacheTags without trailing-slash normalization. Not a regression from this PR, but worth noting.

  4. Test quality is strong — 93 tests passing, covering end-to-end revalidation through KV, sibling isolation, root path edge case, layout/page type variants, and a regression guard for the legacy no-tags behavior.

github run

@james-elicx james-elicx marked this pull request as ready for review May 29, 2026 08:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server action revalidatePath does not invalidate cached pages (including rewritten ones)

1 participant