Skip to content

fix: always mark pages that receive searchParams as dynamic#822

Draft
southpolesteve wants to merge 1 commit intomainfrom
fix/isr-always-mark-dynamic-with-searchparams
Draft

fix: always mark pages that receive searchParams as dynamic#822
southpolesteve wants to merge 1 commit intomainfrom
fix/isr-always-mark-dynamic-with-searchparams

Conversation

@southpolesteve
Copy link
Copy Markdown
Collaborator

Summary

Pages that read searchParams are now always marked dynamic, even when the first request has an empty query string.

Details

Previously, markDynamicUsage() was only called when the URL contained query parameters (hasSearchParams). A request to /page (no query) would render with searchParams = {}, not be marked dynamic, and get ISR-cached. A later request to /page?filter=violet would then receive the cached empty-query response.

The fix removes the hasSearchParams guard so markDynamicUsage() is called unconditionally whenever searchParams is provided to the page component. This matches Next.js behavior where accessing the searchParams prop signals dynamic rendering regardless of whether the URL has query parameters.

Tests

Adds a production integration test with a dedicated fixture page (/search-params-page) that reads searchParams:

  • first request with no query: not cached, renders filter=none
  • second request with ?filter=violet: not cached, renders filter=violet

Pages that read searchParams are request-dependent even when the first
request has an empty query string. Previously, markDynamicUsage() was
only called when the URL contained query parameters (hasSearchParams),
so an empty-query first request would be cached and replayed to later
requests with different query parameters.

Remove the hasSearchParams guard so markDynamicUsage() is called
unconditionally whenever searchParams is provided to the page. This
matches Next.js behavior where accessing the searchParams prop signals
dynamic rendering regardless of whether the URL has query params.

Adds a production integration test with a dedicated fixture page that
reads searchParams, verifying the empty-query response is not cached
and later query-specific requests see their own parameters.
@southpolesteve
Copy link
Copy Markdown
Collaborator Author

/bigbonk review this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 11, 2026

Open in StackBlitz

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

commit: 57efd1f

@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

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.

Good fix — the cache-poisoning bug when the first request has an empty query string is a real correctness issue, and the test exactly reproduces the scenario. The change is narrow and the reasoning in the updated comment block is clear.

One minor cleanup and one question:

  1. Dead code: hasSearchParams variable — After removing the if (hasSearchParams) guard, the let hasSearchParams = false (line 983) and hasSearchParams = true (line 986) inside the searchParams.forEach loop are now unused. They should be removed to avoid confusing readers into thinking it's still referenced somewhere.

  2. ISR background regen path — The renderFreshPageForCache callback at line 2200 calls buildPageElements(route, params, cleanPathname, undefined, new URLSearchParams(), ...). With this change, markDynamicUsage() will now fire during background regeneration too (since searchParams is new URLSearchParams(), which is truthy). This is probably fine because:

    • The regen path already has its own unified context scope
    • The consumeDynamicUsage() in scheduleAppPageRscCacheWrite will detect it and skip the cache write
    • But that means pages with searchParams + ISR revalidate will never actually cache — the ISR cache read at line 2164 will always miss, and the write will always be skipped

    This is the correct behavior for the bug fix (searchParams pages shouldn't be cached at all), but it does mean the ISR cache read/write machinery runs for nothing on every request to these pages. Not a correctness issue, just wasted work. Worth noting for a future optimization where you could skip the ISR cache read entirely when the route receives searchParams.

The test fixture and assertions look solid. The export const revalidate = 60 on the fixture page is a good choice — it ensures the ISR path is actually engaged, which is what makes the bug manifest.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 11, 2026

The inline comment API requires diff positions. The review-level comment I already posted covers this feedback adequately, so the review is complete.


Review posted on PR #822. Here's a summary of the feedback:

  1. Correctness: The fix is sound. Removing the hasSearchParams guard so markDynamicUsage() fires unconditionally whenever searchParams is provided correctly prevents cache poisoning when the first request has an empty query string.

  2. Dead code cleanup: The hasSearchParams variable (declared at line 983, set at line 986) is now unused and should be removed.

  3. Performance observation: Pages with searchParams + ISR revalidate will now always skip caching (correct behavior), but the ISR cache read/write machinery still runs on every request. Not a bug, but a future optimization opportunity — the ISR cache read could be skipped entirely when the route receives searchParams.

github run

Comment on lines +1040 to +1045
// We cannot use a Proxy here because React's RSC debug serializer accesses
// properties on all props ($$typeof, etc.), causing false positives.
// Instead, we unconditionally mark dynamic: any page that receives
// searchParams is request-dependent, even when the first request has an
// empty query string. Without this, the empty-query render would be cached
// and replayed to later requests with different query parameters.
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.

Would it be possible to filter out those internals in a proxy instead? If so, we could use the proxy approach and align behaviour with Next.js.

This change as-is would mark every route we give search params to as dynamic, regardles of whether they use search params or not.

@james-elicx james-elicx marked this pull request as draft April 12, 2026 09:47
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