Skip to content

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

Merged
pi0 merged 7 commits into
mainfrom
fix/vercel-isr-query
Dec 10, 2025
Merged

fix(vercel): handle ISR requests with passQuery: true#3851
pi0 merged 7 commits into
mainfrom
fix/vercel-isr-query

Conversation

@pi0

@pi0 pi0 commented Dec 9, 2025

Copy link
Copy Markdown
Member

Similar fix of #3539 for v3

TODO:

  • Route rule runtime validation
  • E2E testing

@vercel

vercel Bot commented Dec 9, 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 10, 2025 9:05am

@coderabbitai

coderabbitai Bot commented Dec 9, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a named ISR URL query parameter and a helper to extract it, updates Vercel runtime files to use that helper and call getRouteRules before rewriting ISR requests, adjusts ISR route generation and tests to use the new parameter, and makes getRouteRules publicly exported (re-exported from src/runtime/app.ts).

Changes

Cohort / File(s) Change Summary
ISR extraction helper
src/presets/vercel/runtime/isr.ts
Adds ISR_URL_PARAM ("__isr_route") and exported `isrRouteRewrite(reqUrl: string, xNowRouteMatches: string
Vercel runtime integrations
src/presets/vercel/runtime/vercel.node.ts, src/presets/vercel/runtime/vercel.web.ts
Replace inline ISR parsing with isrRouteRewrite(req.url, xNowRouteMatches); call getRouteRules("", isrURL[0]) and, if routeRules?.isr is truthy, rewrite the request URL to the decoded ISR pathname plus optional query. Added imports for getRouteRules and isrRouteRewrite.
Route generation utils
src/presets/vercel/utils.ts
Replace hard-coded $url capture with a named capture using ISR_URL_PARAM, update destination query to use ISR_URL_PARAM, and ensure __isr_route is included in prerender allowQuery arrays.
Runtime export
src/runtime/internal/app.ts, src/runtime/app.ts
Made getRouteRules exported from src/runtime/internal/app.ts and re-exported it from src/runtime/app.ts.
Tests
test/presets/vercel.test.ts
Update expectations to use __isr_route in route-src capture groups, dest query strings, and include __isr_route in prerender allowedQuery.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay attention to URL decoding/encoding edge cases in isrRouteRewrite.
  • Verify imports/exports for getRouteRules compile across runtimes and bundlers.
  • Validate the regex named-capture usage and resulting route strings in utils.ts match test expectations.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format with 'fix' prefix, clear scope 'vercel', and descriptive message about handling ISR requests with passQuery flag.
Description check ✅ Passed The pull request description references a similar fix (#3539) for v3 and indicates completed TODOs, directly relating to the changeset which refactors ISR URL handling and route rule validation.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f3d3d4 and 8a4f67e.

📒 Files selected for processing (1)
  • test/presets/vercel.test.ts (2 hunks)

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.

@pkg-pr-new

pkg-pr-new Bot commented Dec 9, 2025

Copy link
Copy Markdown

Open in StackBlitz

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

commit: b9765ab

@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 (4)
src/presets/vercel/runtime/vercel.node.ts (1)

5-22: Node runtime ISR rewrite correctly centralized via isrRouteRewrite

The node handler’s ISR handling now mirrors the web runtime and cleanly delegates URL extraction to isrRouteRewrite, which is a good consolidation of logic. The req.url! assertion and use of the x-now-route-matches header align with prior behavior, so no functional regressions are apparent here.

If you want to tighten the types a bit, you could consider passing null explicitly when the header is missing instead of casting to string, but that’s purely cosmetic:

-  const isrURL = isrRouteRewrite(
-    req.url!,
-    req.headers["x-now-route-matches"] as string
-  );
+  const isrURL = isrRouteRewrite(
+    req.url!,
+    (req.headers["x-now-route-matches"] as string | undefined) ?? null
+  );
src/presets/vercel/utils.ts (2)

13-239: ISR route rule generation looks consistent; double‑check behavior for non‑/** ISR routes

Using ISR_URL_PARAM for both the named capture and the dest query is a nice improvement and keeps the runtime and build config in sync. The transformations

  • root / rule:
    • src: (?<__isr_route>/)
    • dest: /index-isr?__isr_route=$__isr_route
  • catch‑all ISR rules:
    • src via key.replace(/^(.*)\/\*\*/, '(?<__isr_route>$1/.*)')
    • dest via ...?__isr_route=$__isr_route

are all coherent and match the expectations encoded in the updated tests.

One corner case worth verifying: if someone defines routeRules["/foo"]!.isr without a /** suffix, src will stay as "/foo" (no named capture), but dest will still reference $__isr_route. That relies on Vercel’s routing engine to either (a) provide a __isr_route entry in x-now-route-matches even without an explicit named group, or (b) treat $__isr_route in dest in a forgiving way. If this scenario is not supported or guaranteed, it may be safer to either:

  • require /** for ISR rules that depend on __isr_route, or
  • synthesize a named group for non‑/** ISR rules as well.

Please confirm this behavior against the Vercel Build Output API docs / runtime behavior for non‑/** ISR routes that still use $__isr_route in dest.


482-487: Ensuring __isr_route is in allowQuery is good; consider avoiding in‑place mutation

The allowQuery augmentation correctly guarantees that __isr_route is forwarded when a user has explicitly set allowQuery, which is important for ISR revalidation flows.

Right now this mutates the allowQuery array in place, which is likely the same array referenced by the original route rule config. That’s probably fine given Nitro already mutates route rules elsewhere, but if you want to keep user config more obviously immutable, you could reassign a cloned array instead:

-  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 = [
+        ...prerenderConfig.allowQuery,
+        ISR_URL_PARAM,
+      ];
+    }
+  }
src/presets/vercel/runtime/isr.ts (1)

1-23: Centralized ISR URL extraction is solid; avoid trailing ? when no extra query remains

The ISR_URL_PARAM constant and isrRouteRewrite helper nicely centralize ISR URL extraction logic for both runtimes, and the query‑string fallback correctly strips __isr_route while preserving other query params.

There is a small edge case in the query‑based branch: if __isr_route is the only query parameter, params.toString() returns an empty string, so the function returns a URL like "/some/path?" instead of "/some/path". Functionally it’s usually fine, but it’s easy to avoid and keeps URLs cleaner:

-      const params = new URLSearchParams(reqUrl.slice(queryIndex + 1));
-      const isrURL = params.get(ISR_URL_PARAM);
-      if (isrURL) {
-        params.delete(ISR_URL_PARAM);
-        return decodeURIComponent(isrURL) + "?" + params.toString();
-      }
+      const params = new URLSearchParams(reqUrl.slice(queryIndex + 1));
+      const isrURL = params.get(ISR_URL_PARAM);
+      if (isrURL) {
+        params.delete(ISR_URL_PARAM);
+        const restQuery = params.toString();
+        const decoded = decodeURIComponent(isrURL);
+        return restQuery ? `${decoded}?${restQuery}` : decoded;
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd9c291 and 9cbd438.

📒 Files selected for processing (5)
  • src/presets/vercel/runtime/isr.ts (1 hunks)
  • src/presets/vercel/runtime/vercel.node.ts (2 hunks)
  • src/presets/vercel/runtime/vercel.web.ts (2 hunks)
  • src/presets/vercel/utils.ts (4 hunks)
  • test/presets/vercel.test.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/presets/vercel/utils.ts (1)
src/presets/vercel/runtime/isr.ts (1)
  • ISR_URL_PARAM (1-1)
src/presets/vercel/runtime/vercel.node.ts (1)
src/presets/vercel/runtime/isr.ts (1)
  • isrRouteRewrite (3-23)
src/presets/vercel/runtime/vercel.web.ts (1)
src/presets/vercel/runtime/isr.ts (1)
  • isrRouteRewrite (3-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: publish-pkg-pr-new
🔇 Additional comments (2)
test/presets/vercel.test.ts (1)

121-155: Tests correctly capture new __isr_route behavior and prerender allowQuery

The updated snapshots for ISR routes and prerender configs line up with the new ISR_URL_PARAM ("__isr_route") and the helper logic in isrRouteRewrite. In particular:

  • Route rules now consistently use (?<__isr_route>...) in src and ?__isr_route=$__isr_route in dest.
  • The prerender-config expectation includes "__isr_route" in allowQuery, validating the new writePrerenderConfig behavior.

This provides good coverage for the new ISR URL handling across both routing and prerender metadata.

Also applies to: 366-369

src/presets/vercel/runtime/vercel.web.ts (1)

5-21: Web runtime ISR rewrite correctly delegates to shared helper

The web runtime now cleanly delegates ISR URL extraction to isrRouteRewrite, and rewrites the request by constructing a new Request with new URL(isrURL, req.url).href. This:

  • Keeps ISR routing behavior in sync with the node runtime.
  • Allows the helper to handle both x-now-route-matches–based extraction and the query‑string fallback in one place.
  • Ensures that, when the helper includes remaining query params in the returned URL, the rewritten Request preserves them.

The change looks sound and improves maintainability of ISR handling across runtimes.

@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 (1)
src/presets/vercel/runtime/vercel.node.ts (1)

15-24: ISR rewrite logic is sound; consider a small readability tweak

The flow—derive an ISR target via isrRouteRewrite, check getRouteRules("", pathname) for routeRules?.isr, and only then rewrite req.url—is correct and safely no‑ops when there is no applicable ISR rule.

For readability and to avoid magic tuple indices, you could optionally destructure the result:

-  const isrURL = isrRouteRewrite(
-    req.url!,
-    req.headers["x-now-route-matches"] as string
-  );
-  if (isrURL) {
-    const { routeRules } = getRouteRules("", isrURL[0]);
-    if (routeRules?.isr) {
-      req.url = isrURL[0] + (isrURL[1] ? `?${isrURL[1]}` : "");
-    }
-  }
+  const isrURL = isrRouteRewrite(
+    req.url!,
+    req.headers["x-now-route-matches"] as string
+  );
+  if (isrURL) {
+    const [pathname, search] = isrURL;
+    const { routeRules } = getRouteRules("", pathname);
+    if (routeRules?.isr) {
+      req.url = pathname + (search ? `?${search}` : "");
+    }
+  }

Behavior stays identical; the block becomes a bit clearer to future readers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7683b3f and 5f3d3d4.

📒 Files selected for processing (3)
  • src/presets/vercel/runtime/isr.ts (1 hunks)
  • src/presets/vercel/runtime/vercel.node.ts (2 hunks)
  • src/presets/vercel/runtime/vercel.web.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/presets/vercel/runtime/vercel.web.ts
  • src/presets/vercel/runtime/isr.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/presets/vercel/runtime/vercel.node.ts (2)
src/presets/vercel/runtime/isr.ts (1)
  • isrRouteRewrite (3-23)
src/runtime/internal/app.ts (1)
  • getRouteRules (205-255)
🔇 Additional comments (1)
src/presets/vercel/runtime/vercel.node.ts (1)

4-5: Import wiring for ISR route rules looks correct

Bringing in getRouteRules from nitro/app and isrRouteRewrite from the shared ISR helper keeps this adapter thin and aligned with the new centralized ISR logic. No issues here.

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.

1 participant