Skip to content

fix: resolve revalidate from cacheControl for null (notFound) page values - #233

Open
aevarOrigo wants to merge 1 commit into
fortedigital:masterfrom
aevarOrigo:fix/pages-router-notfound-revalidate
Open

fix: resolve revalidate from cacheControl for null (notFound) page values#233
aevarOrigo wants to merge 1 commit into
fortedigital:masterfrom
aevarOrigo:fix/pages-router-notfound-revalidate

Conversation

@aevarOrigo

Copy link
Copy Markdown

Summary

resolveRevalidateValue() only reads ctx.cacheControl.revalidate when the cached value's kind is "APP_PAGE" or "PAGES". But when a Pages Router getStaticProps/getServerSideProps returns notFound: true, there's no page content to cache, so Next.js's response cache calls CacheHandler#set() with incrementalCacheValue set to null — it has no kind at all.

That means the else if never matches for notFound: true responses, revalidate falls through to ctx.revalidate, which the pages-router response cache never actually populates (response-cache/index.js only ever passes { cacheControl, isRoutePPREnabled, isFallback } into incrementalCache.set()), and the entry silently gets defaultStaleAge instead of the revalidate value the page actually returned alongside notFound: true.

Repro

// pages/[stationId].tsx
export const getStaticProps = async (context) => {
  if (invalid) {
    return { notFound: true, revalidate: 10 };
  }
  // ...
  return { props: {...}, revalidate: 30 };
};
  • Success path: cached value has kind: "PAGES"ctx.cacheControl.revalidate (30) is read correctly.
  • notFound: true path: cached value is null → falls through to ctx.revalidate (undefined) → defaultStaleAge is used instead of 10, regardless of what the page returned.

With the default defaultStaleAge of 31536000 (1 year), every notFound: true response ends up with a ~1 year stale age in the underlying store no matter what revalidate the page specifies.

Fix

Also read ctx.cacheControl.revalidate when the incremental cache value is null/undefined, since that's exactly where the intended revalidate time already lives for notFound: true responses.

Test plan

  • Added resolveRevalidateValue.test.ts covering FETCH, APP_PAGE, PAGES, null (the fix), undefined, unknown kind, and the no-value/no-ctx fallback cases.
  • pnpm test in packages/nextjs-cache-handler — all 34 tests pass.
  • pnpm build (tsup --dts-resolve) — builds cleanly, no type errors.

Happy to adjust the approach if you'd prefer handling this differently (e.g. inside CacheHandler#set() instead of resolveRevalidateValue()).

…lues

getStaticProps/getServerSideProps returning `notFound: true` has no page
content to cache, so Next.js's pages-router response cache calls
CacheHandler#set() with `incrementalCacheValue` set to `null` (no `kind`
property). resolveRevalidateValue() only read `ctx.cacheControl.revalidate`
when the value's `kind` was "APP_PAGE" or "PAGES", so null values fell
through to the unpopulated `ctx.revalidate` and silently defaulted to
`defaultStaleAge` (1 year) regardless of the `revalidate` the page actually
returned alongside `notFound: true`.

Read ctx.cacheControl.revalidate for null/undefined values too, since that's
exactly where the intended revalidate time already lives.
@AyronK

AyronK commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

This PR conflicts with #232.

// `ctx.cacheControl.revalidate` in that case, so read it here too instead of
// silently falling through to the unpopulated `ctx.revalidate` below and
// defaulting to `defaultStaleAge`.
cachedPageValue == null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't agree that cachedPageValue == null is the right call here, it's not even === comparison.

If anything I'd go with return revalidate ?? responseCacheCtx.cacheControl?.revalidate ?? ctx.revalidate;

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