Skip to content

fix(vercel): harden ISR URL restoration against carrier variants - #4641

Open
nikolas-sapa wants to merge 1 commit into
nitrojs:v2from
nikolas-sapa:fix/4446-isr-url-restoration
Open

nikolas-sapa wants to merge 1 commit into
nitrojs:v2from
nikolas-sapa:fix/4446-isr-url-restoration

Conversation

@nikolas-sapa

Copy link
Copy Markdown

🔗 Linked issue

Resolves #4446

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

This implements the fix design prioritized in #4446 — full credit to the reporter for the analysis, the carrier-shape taxonomy and the fail-safe reasoning; the code follows their proposal (items 1 and 2).

The ISR fallback function recovered the original URL from a single carrier: the __isr_route group of x-now-route-matches, and — only when the header was absent entirely — the destination query. Two gaps remain:

  1. Header present without __isr_route (e.g. positional-only serialization like 1=schedule): the header branch ran and found nothing, and the if/else shadowed the query fallback — so the rewrite's identical query value was never consulted. The internal -isr route rendered a 404, which Vercel stores as a valid ISR outcome and replays with x-vercel-cache: HIT — cache poisoning.
  2. Bare invocation (no header, no query): neither branch rewrote, same cacheable-404 outcome.

Changes to presets/vercel/runtime/vercel.ts:

  • Consult the destination query whenever the header carrier yields no __isr_route — strictly additive, since the rewrite writes the identical value into the dest query. Every carrier shape that works today resolves through the exact same code path as before.
  • When no carrier resolves and the request targets an internal -isr path, respond 503 instead of rendering the internal route. As measured in the issue on a live deployment: 404/410 are stored by the ISR cache, 401/5xx are never stored and any existing good copy keeps serving (x-vercel-cache: STALE) — turning an unrecognized future carrier shape into a transient miss instead of permanent poisoning.

One trade-off, explicit in the issue: a user route literally named …-isr hit bare would now get a 503 from this function. That is the accepted cost of the fail-safe, and external requests to such paths land on separate cache keys anyway.

Testing. Four runtime tests against the built fixture function (the harness invokes functions/__fallback.func directly): header carrier (unchanged), query carrier (unchanged), header-without-__isr_route falling back to the query (fails on main with a 404 for the internal path), and bare invocation returning 503 for both the page and _payload.json variants. Reverting the runtime change turns exactly the two bug-pinning tests red while the two working-path tests stay green — pinning rather than describing. Full file: 55 passed. eslint / prettier / tsc --noEmit clean.

The v3 main equivalent (presets/vercel/runtime/isr.ts, via #3851) has the same gaps — happy to port this once the v2 approach is agreed.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

The ISR fallback function recovered the original URL from a single
carrier: the `__isr_route` group of the `x-now-route-matches` header,
and — only when the header was absent entirely — the destination query.
Two gaps remain in that contract:

1. A header serialization without the named group (e.g. positional-only
   groups) shadows the query fallback, which the rewrite populates with
   the identical value. The internal `-isr` route renders, and the 404
   is stored by Vercel as a valid ISR outcome — cache poisoning until
   expiration.
2. A bare invocation (no header, no query) renders the internal route
   the same way.

Consult the destination query whenever the header carrier yields no
`__isr_route` (strictly additive: working carrier shapes are
unaffected), and respond 503 when no carrier resolves for an internal
`-isr` path — a 5xx is never stored by the ISR cache, so the miss
stays transient and any cached copy keeps serving with
`x-vercel-cache: STALE`.

Design per the analysis in nitrojs#4446; both gaps reproduce against the
fixture build, and the new tests pin them.

Fixes nitrojs#4446
@nikolas-sapa
nikolas-sapa requested a review from pi0 as a code owner September 19, 2026 10:51
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7b3c4a63-60f5-4f66-bc15-ea0e02e265f2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@vercel

vercel Bot commented Sep 19, 2026

Copy link
Copy Markdown

@nikolas-sapa is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

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