Skip to content

fix: emit no-store Cache-Control for revalidate = 0 pages#777

Merged
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:fix/cache-control-revalidate-zero
Apr 4, 2026
Merged

fix: emit no-store Cache-Control for revalidate = 0 pages#777
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:fix/cache-control-revalidate-zero

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Summary

  • Pages with export const revalidate = 0 were not getting a Cache-Control header, allowing CDNs to cache them
  • 0 is falsy in JS — the RSC function used if (options.revalidateSeconds) and the HTML function used revalidateSeconds > 0, both silently skipping the zero case
  • Added explicit === 0 checks in both resolveAppPageRscResponsePolicy and resolveAppPageHtmlResponsePolicy, placed before any branch that would mishandle 0 as falsy
  • Also tightened the isForceStatic/isDynamicError condition in the HTML helper from (=== null || === 0) to === null, removing the now-unreachable dead branch
  • Next.js treats revalidate = 0 as equivalent to dynamic = "force-dynamic" for caching: emits Cache-Control: no-store, must-revalidate

Test plan

  • New unit tests for revalidateSeconds: 0 in both RSC and HTML policy functions, including isForceStatic: true + revalidateSeconds: 0 combination to verify priority ordering
  • All existing response policy tests still pass (15 total)
  • pnpm run check passes

Pages exporting revalidate = 0 should be treated as fully dynamic (never
cached), matching Next.js behavior. The previous code used truthiness
checks on revalidateSeconds, causing 0 to fall through with no
Cache-Control header — allowing CDNs to cache dynamic content.

Add explicit === 0 guards in both resolveAppPageRscResponsePolicy and
resolveAppPageHtmlResponsePolicy, placed before any branch that uses
!revalidateSeconds or revalidateSeconds > 0. Also tighten the
isForceStatic/isDynamicError condition in the HTML helper from
(=== null || === 0) to === null, since 0 is now handled earlier.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 4, 2026

Open in StackBlitz

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

commit: 48354be

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

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.

Clean, correct fix for a real caching bug. The JS falsiness of 0 silently skipped Cache-Control emission for revalidate = 0 pages — good catch.

The fix is well-placed in both functions, the tests cover the key cases including the isForceStatic + revalidateSeconds: 0 priority edge case, and the dead branch cleanup in the HTML helper is a nice follow-through.

Two minor suggestions (non-blocking):

  1. RSC function consistency: The condition on line 94 (!options.revalidateSeconds) and line 103 (if (options.revalidateSeconds)) still rely on truthiness checks. Now that 0 is handled above, they're safe — null is the only remaining falsy value — but this is the same class of truthiness-relying code that caused the original bug. Consider tightening to === null and !== null && > 0 for symmetry with the HTML helper, which you already cleaned up nicely.

  2. Test coverage: Consider adding an isDynamicError: true + revalidateSeconds: 0 test case alongside the existing isForceStatic: true + revalidateSeconds: 0 cases, since isDynamicError is the other flag in the condition the comments warn about.

// force-dynamic for caching purposes. Must be checked before the
// isForceStatic/isDynamicError branch below, which uses !revalidateSeconds
// and would incorrectly catch 0 as a falsy value.
if (options.revalidateSeconds === 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fix looks correct. One small follow-up: the truthiness check on line 94 (!options.revalidateSeconds) is now safe since 0 exits above, but it's the same pattern that caused the original bug. For resilience against future reordering, consider tightening to === null:

Suggested change
if (options.revalidateSeconds === 0) {
if (options.revalidateSeconds === 0) {
return { cacheControl: NO_STORE_CACHE_CONTROL };
}

(The suggestion above is just the existing code — the real change would be on line 94 below, changing !options.revalidateSeconds to options.revalidateSeconds === null. Non-blocking.)

};
}

if ((options.isForceStatic || options.isDynamicError) && options.revalidateSeconds === null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good cleanup — tightening from (=== null || === 0) to === null here is the right call now that 0 is handled above. This is exactly the kind of dead-branch removal that makes the precedence chain clearer.

}),
).toEqual({
cacheControl: "no-store, must-revalidate",
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice coverage of the isForceStatic priority case. Consider also adding isDynamicError: true + revalidateSeconds: 0 since that's the other flag in the condition the comment warns about:

// revalidate = 0 takes priority over isDynamicError too
expect(
  resolveAppPageRscResponsePolicy({
    dynamicUsedDuringBuild: false,
    isDynamicError: true,
    isForceDynamic: false,
    isForceStatic: false,
    isProduction: true,
    revalidateSeconds: 0,
  }),
).toEqual({
  cacheControl: "no-store, must-revalidate",
});

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 4, 2026

Review submitted successfully. Here's a summary of my review:

Approved — this is a clean, correct fix for a real caching bug where JS falsiness of 0 caused revalidate = 0 pages to silently skip Cache-Control header emission, allowing CDNs to cache pages that should never be cached.

Two non-blocking suggestions:

  1. RSC function consistency — The remaining truthiness checks (!options.revalidateSeconds on line 94, if (options.revalidateSeconds) on line 103) are now safe but are the same pattern that caused the original bug. Tightening to explicit === null / !== null && > 0 would match the HTML helper's style and be more resilient to future refactoring.

  2. Additional test case — Adding an isDynamicError: true + revalidateSeconds: 0 test alongside the existing isForceStatic priority tests would round out coverage of the condition the comments warn about.

github run

@james-elicx james-elicx merged commit d063320 into cloudflare:main Apr 4, 2026
26 checks passed
@NathanDrake2406 NathanDrake2406 deleted the fix/cache-control-revalidate-zero branch April 4, 2026 14:35
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.

2 participants