Skip to content

fix(vercel): handle ISR requests with passQuery: true#3539

Merged
pi0 merged 22 commits into
v2from
fix/vercel-isr-url
Dec 9, 2025
Merged

fix(vercel): handle ISR requests with passQuery: true#3539
pi0 merged 22 commits into
v2from
fix/vercel-isr-url

Conversation

@pi0

@pi0 pi0 commented Aug 19, 2025

Copy link
Copy Markdown
Member

Resolves #1880, #3594, #3651

Normally, with ISR matched routes, there is an (undocumented/legacy) x-now-route-matches header that contains the full pathname of the matched route. And Nitro vercel entry uses this header value to restore the original URL, but when passQuery isr route rule config is set, this header won't exist anymore, and the path is invalid (isr function name). We cannot always assume url query param is for ISR functions (it can be an actual query in normal routes!)

This PR changes the internal url query param to __isr_route and, if it exists, also attempts to rewrite without relying on legacy header but instead using a runtime protection check to make sure the requested route matches ISR route patterns.

We also make sure internal "__isr_route" is added to passQuery (if specified)


Beta testing: use nightly channel

@vercel

vercel Bot commented Aug 19, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
nitro.build Ready Ready Preview Comment Dec 9, 2025 8:50pm

@pkg-pr-new

pkg-pr-new Bot commented Oct 13, 2025

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/nitrojs/nitro/nitropack@3539

commit: 7753975

@pi0 pi0 changed the title fix(vercel): workaround for isr route rule with passQuery: true fix(vercel): handle isr requests with passQuery: true Oct 14, 2025
@mkucmus

mkucmus commented Nov 12, 2025

Copy link
Copy Markdown

hey @pi0, do you plan to release the fix in upcoming version maybe?

@fmoessle

Copy link
Copy Markdown
Contributor

Would love to see this merged. Using

  "resolutions": {
    "nitropack": "https://pkg.pr.new/nitrojs/nitro/nitropack@3539"
  }

works but is not ideal.

@fmoessle

Copy link
Copy Markdown
Contributor

The problem I face with https://pkg.pr.new/nitrojs/nitro/nitropack@3539: Building and starting the application with node-presets causes an issue with the public folder resolution in .output

Reproduction: https://github.com/fmoessle/nitro-vercel-isr/tree/test-3539-fix

pnpm build
pnpm preview

results in errors like this:

request error] [unhandled] [GET] http://[::]:3000/favicon.ico
 H3Error: ENOENT: no such file or directory, open 'XXX/florian/projects/test/nitro-vercel-isr/.output/server/chunks/public/favicon.ico'
    at async open (node:internal/fs/promises:634:25)
    ... 2 lines matching cause stack trace ...
    at async Server.toNodeHandle (file:///XXX/.output/server/chunks/nitro/nitro.mjs:1917:7) {
  cause: Error: ENOENT: no such file or directory, open 'XXX/.output/server/chunks/public/favicon.ico'
      at async open (node:internal/fs/promises:634:25)
      at async Object.readFile (node:internal/fs/promises:1238:14)
      at async Object.handler (file:///XXX/.output/server/chunks/nitro/nitro.mjs:1646:19)
      at async Server.toNodeHandle (file:///XXX/.output/server/chunks/nitro/nitro.mjs:1917:7) {
    errno: -2,
    code: 'ENOENT',
    syscall: 'open',
    path: 'XXX/.output/server/chunks/public/favicon.ico'
  },
  statusCode: 500,
  fatal: false,
  unhandled: true,
  statusMessage: undefined,
  data: undefined
}

There is no public folder in .output/server/chunks

@coderabbitai

coderabbitai Bot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR introduces a new constant ISR_URL_PARAM with value "__isr_route" to standardize Incremental Static Regeneration parameter handling across the Vercel preset. The constant replaces hardcoded parameter references in route matching, URL query handling, and test fixtures for consistent ISR routing behavior.

Changes

Cohort / File(s) Summary
New constant definition
src/presets/vercel/runtime/consts.ts
Introduces ISR_URL_PARAM constant with value "__isr_route" for centralized ISR parameter naming.
Runtime ISR flow updates
src/presets/vercel/runtime/vercel.ts
Refactors ISR detection and routing logic to extract ISR_URL_PARAM from headers or query parameters; adds route rules lookup via getRouteRulesForPath() and implements withQuery() for preserving non-ISR query parameters.
Utility and configuration
src/presets/vercel/utils.ts
Replaces hardcoded "url" parameter with ISR_URL_PARAM constant in route regex captures, query destinations, and prerender configuration allowlist.
Test updates
test/presets/vercel.test.ts
Updates ISR route assertions to use __isr_route in destination query strings and source regex named capture groups; adds "__isr_route" to prerender config allowQuery array.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • src/presets/vercel/runtime/vercel.ts: Carefully verify the two-path ISR detection flow (header-based vs. passQuery-based) correctly handles URL parameter extraction and route rule resolution.
  • src/presets/vercel/utils.ts: Confirm all hardcoded "url" parameter references have been consistently replaced with ISR_URL_PARAM across regex patterns and prerender config generation.
  • Cross-file consistency: Ensure the constant definition, usage in runtime logic, utility transformations, and test expectations are properly aligned throughout the preset.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description is well-structured, relates to the changeset, references multiple linked issues, explains the problem and solution clearly.
Linked Issues check ✅ Passed The PR addresses issue #1880 (and others) by introducing __isr_route parameter to handle ISR requests with passQuery, ensuring query parameters are preserved in Vercel's caching behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing ISR request handling with passQuery support; changes to consts, runtime, utils, and tests are cohesively focused on this objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title follows conventional commits format with type 'fix', scope 'vercel', and subject describing the change.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/vercel-isr-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pi0

pi0 commented Dec 9, 2025

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/presets/vercel/runtime/vercel.ts (2)

15-23: Variable naming: isrRoute is misleading.

The variable isrRoute actually holds the x-now-route-matches header value (a query string), not a route. Consider renaming to routeMatchesHeader or isrHeader for clarity.

-  const isrRoute = req.headers["x-now-route-matches"] as string;
-  if (isrRoute) {
-    const { [ISR_URL_PARAM]: url } = parseQuery(isrRoute);
+  const routeMatchesHeader = req.headers["x-now-route-matches"] as string;
+  if (routeMatchesHeader) {
+    const { [ISR_URL_PARAM]: url } = parseQuery(routeMatchesHeader);

17-22: Consider extracting duplicate ISR validation logic.

Lines 17-22 and 32-39 share nearly identical validation logic: extract URL from ISR parameter, check if it's a string, get route rules, verify ISR is enabled, then update req.url. This duplication increases maintenance burden.

Consider extracting a helper function:

function trySetIsrUrl(
  req: any,
  url: unknown,
  preserveParams?: Record<string, any>
): void {
  if (url && typeof url === "string") {
    const routeRules = getRouteRulesForPath(url) as NitroRouteRules;
    if (routeRules.isr) {
      req.url = preserveParams ? withQuery(url, preserveParams) : url;
    }
  }
}

Then simplify the main logic:

const routeMatchesHeader = req.headers["x-now-route-matches"] as string;
if (routeMatchesHeader) {
  const { [ISR_URL_PARAM]: url } = parseQuery(routeMatchesHeader);
  trySetIsrUrl(req, url);
} else {
  const queryIndex = req.url!.indexOf("?");
  const urlQueryIndex =
    queryIndex === -1
      ? -1
      : req.url!.indexOf(`${ISR_URL_PARAM}=`, queryIndex);
  if (urlQueryIndex !== -1) {
    const { [ISR_URL_PARAM]: url, ...params } = parseQuery(
      req.url!.slice(queryIndex)
    );
    trySetIsrUrl(req, url, params);
  }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5caca6c and 7753975.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • src/presets/vercel/runtime/consts.ts (1 hunks)
  • src/presets/vercel/runtime/vercel.ts (1 hunks)
  • src/presets/vercel/utils.ts (4 hunks)
  • test/presets/vercel.test.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/presets/vercel/utils.ts (1)
src/presets/vercel/runtime/consts.ts (1)
  • ISR_URL_PARAM (1-1)
src/presets/vercel/runtime/vercel.ts (3)
src/runtime/internal/app.ts (1)
  • nitroApp (220-220)
src/presets/netlify/legacy/runtime/netlify.ts (1)
  • handler (7-28)
src/presets/vercel/runtime/consts.ts (1)
  • ISR_URL_PARAM (1-1)
🔇 Additional comments (10)
src/presets/vercel/runtime/consts.ts (1)

1-1: LGTM! Centralized constant for ISR parameter.

The constant effectively standardizes the ISR URL parameter across the codebase, replacing hardcoded strings and improving maintainability.

src/presets/vercel/utils.ts (4)

14-14: LGTM! Import of centralized ISR parameter constant.


237-239: LGTM! Consistent use of ISR_URL_PARAM in route configuration.

The named capture group and destination query string correctly reference the centralized constant.


246-264: LGTM! ISR route handling uses centralized constant.

The routing logic properly applies ISR_URL_PARAM across both source patterns and destination URLs for ISR-enabled and ISR-disabled routes.


475-480: Guard against undefined allowQuery before calling .includes().

The code checks if prerenderConfig.allowQuery exists before calling .includes(), but it doesn't guard against allowQuery being undefined when pushing the ISR parameter. If allowQuery is undefined, line 477 will throw a TypeError.

Apply this diff to properly initialize or guard the array:

-  if (
-    prerenderConfig.allowQuery &&
-    !prerenderConfig.allowQuery.includes(ISR_URL_PARAM)
-  ) {
-    prerenderConfig.allowQuery.push(ISR_URL_PARAM);
-  }
+  if (prerenderConfig.allowQuery) {
+    if (!prerenderConfig.allowQuery.includes(ISR_URL_PARAM)) {
+      prerenderConfig.allowQuery.push(ISR_URL_PARAM);
+    }
+  } else if (prerenderConfig.passQuery) {
+    prerenderConfig.allowQuery = [ISR_URL_PARAM];
+  }

Alternatively, if allowQuery should always include ISR_URL_PARAM when ISR is configured:

-  if (
-    prerenderConfig.allowQuery &&
-    !prerenderConfig.allowQuery.includes(ISR_URL_PARAM)
-  ) {
-    prerenderConfig.allowQuery.push(ISR_URL_PARAM);
-  }
+  if (!prerenderConfig.allowQuery) {
+    prerenderConfig.allowQuery = [];
+  }
+  if (!prerenderConfig.allowQuery.includes(ISR_URL_PARAM)) {
+    prerenderConfig.allowQuery.push(ISR_URL_PARAM);
+  }

Likely an incorrect or invalid review comment.

test/presets/vercel.test.ts (2)

116-150: LGTM! Test fixtures updated to reflect new ISR parameter.

The test expectations correctly verify that ISR routes use __isr_route in both source capture groups and destination query strings, aligning with the centralized constant.


463-463: LGTM! Prerender config expectation updated.

The test correctly verifies that __isr_route is included in the allowQuery array alongside user-facing query parameters.

src/presets/vercel/runtime/vercel.ts (3)

3-8: LGTM! Necessary imports for ISR-aware routing.

The imports correctly bring in route rules lookup, query manipulation utilities, and the centralized ISR parameter constant.


38-38: LGTM! Preserves non-ISR query parameters.

The use of withQuery(url, params) correctly preserves user-facing query parameters when reconstructing the URL for the passQuery flow, which addresses the issue described in #1880.


26-33: The non-null assertions are safe but redundant.

In Node.js HTTP request handlers, req.url is always defined by the HTTP specification and guaranteed to be set when the request object is created. The h3 framework maintains this contract. The non-null assertions on lines 26, 30, and 33 are technically unnecessary since req.url will always have a value in this context. Consider removing the ! operators for cleaner code, though the current implementation is functionally correct.

@pi0

pi0 commented Dec 9, 2025

Copy link
Copy Markdown
Member Author

@fmoessle I couldn't reproduce error on your branch... This PR is also only affecting vercel preset not sure how can it break node-server. If you continue to having them on nightly or next release please feel free to raise an issue with more steps for reproduction.

@pi0 pi0 changed the title fix(vercel): handle isr requests with passQuery: true fix(vercel): handle ISR requests with passQuery: true Dec 9, 2025
@pi0
pi0 merged commit 16f4167 into v2 Dec 9, 2025
8 checks passed
@pi0
pi0 deleted the fix/vercel-isr-url branch December 9, 2025 21:22
@ps-20x

ps-20x commented Dec 15, 2025

Copy link
Copy Markdown

Would love to see this merged. Using

  "resolutions": {
    "nitropack": "https://pkg.pr.new/nitrojs/nitro/nitropack@3539"
  }

works but is not ideal.

As this branch was now merged and deleted and as far as I can see there is no new release in nitro v2. What's the proposed way now? Should it now target the commit?

@pi0

pi0 commented Dec 15, 2025

Copy link
Copy Markdown
Member Author

You can use nightly channel: https://nitro.build/guide/nightly

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.

5 participants