fix(app-router): preserve semicolons in redirect digests#2487
fix(app-router): preserve semicolons in redirect digests#2487NathanDrake2406 wants to merge 7 commits into
Conversation
commit: |
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
3d06008 to
1e389b5
Compare
NathanDrake2406
left a comment
There was a problem hiding this comment.
Small follow-up nit: the server comment still describes the redirect shape as NEXT_REDIRECT;<type>;<encodedUrl>;<status>, but the shared parser now intentionally accepts both vinext’s encoded three-part form and Next’s raw five-part form. Might be worth updating that wording so future readers don’t “fix” this back to a single encoding model.
88d8409 to
94c81b0
Compare
Redirect digests can carry raw URLs, and raw URLs can contain semicolons. Treating semicolon-delimited URL content as digest structure can truncate or misclassify the redirect target. Keep redirect digest parsing strict about the final status segment while preserving semicolons in the URL. The server parser stays self-contained so App Router startup does not import the navigation shim.
94c81b0 to
e9808ce
Compare
|
/bigbonk review for issues |
There was a problem hiding this comment.
Reviewed the parser against Next.js's authoritative isRedirectError (packages/next/src/client/components/redirect-error.ts), which uses digest.slice(2, -2).join(';') for the URL and digest.at(-2) for the status. Two things check out well:
- Status allowlist is correct.
303/307/308exactly matches Next.js'sRedirectStatusCodeenum (SeeOther=303, TemporaryRedirect=307, PermanentRedirect=308). I verified there is no valid Next.js redirect digest carrying any other status, so the tightening fromparseInt()is aligned with Next.js, not a regression. - Raw-URL parsing matches Next.js. I diffed the parser against Next.js's exact
slice(2,-2)algorithm across canonical digests, including URLs whose query/segment ends in a status-like number (e.g./redirect?to=308;307;and/a;308;308;). Results are identical.
Two smaller points below. Neither blocks the PR; the second is a cleanup.
Validation run locally: vp test run tests/next-error-digest.test.ts tests/error-boundary.test.ts (43 passed) and vp check on the three changed source files (clean).
| const statusMatch = rest.match(/;(303|307|308);?$/); | ||
| const target = statusMatch | ||
| ? rest.slice(0, -statusMatch[0].length) | ||
| : /;\d[^;]*;$/.test(rest) |
There was a problem hiding this comment.
The malformed-status fallback branch /;\d[^;]*;$/ is inconsistent, because it only strips the trailing ; when the final segment starts with a digit:
NEXT_REDIRECT;replace;/foo;307garbage;→ url/foo;307garbage(one trailing;stripped) — this is the case covered by the new test.NEXT_REDIRECT;replace;/foo;abc;→ url/foo;abc;(both semicolons kept, becauseabcdoesn't start with a digit).
So two equally-malformed digests are treated differently, and the digit-prefix rule has no counterpart in Next.js (isRedirectError treats both as non-redirects since Number(status) is NaN). Since neither shape is emitted by vinext or Next.js in practice, I'd lean toward dropping the special case entirely and just using rest for the non-status branch — it's simpler and the asymmetry goes away. If you keep it, worth a comment explaining why the digit prefix matters.
|
|
||
| import { parseRedirectDigest } from "../utils/redirect-digest.js"; | ||
|
|
||
| export { parseRedirectDigest, type RedirectDigest } from "../utils/redirect-digest.js"; |
There was a problem hiding this comment.
This re-export doesn't appear to be consumed anywhere — navigation.ts and navigation-server.ts re-export the other helpers from this module but not parseRedirectDigest/RedirectDigest, and nothing else imports them from here (consumers import from utils/redirect-digest.js or the server wrapper directly). If it's not needed as public shim surface, dropping it keeps the shim's exported API minimal:
| export { parseRedirectDigest, type RedirectDigest } from "../utils/redirect-digest.js"; |
|
Review posted successfully. Here's a summary of my review of PR #2487: Review SummaryVerdict: Solid PR, no blocking issues. The core parsing logic is correct and I verified it against Next.js's authoritative implementation. What I verified as correct
Two non-blocking points raised inline
Also confirmed the author already addressed the earlier self-review nit about the doc comment describing the digest shape. |
Overview
NEXT_REDIRECTdigests.303,307, and308in the final digest segment are treated as redirect status. Other semicolon-delimited content stays in the URL.packages/vinext/src/utils/redirect-digest.ts,packages/vinext/src/server/next-error-digest.ts,packages/vinext/src/shims/navigation-errors.tsWhy
Redirect digest parsing has to separate control metadata from the redirect target without assuming semicolons only appear as digest separators. Vinext formats redirect digests with raw URLs, so the URL portion can contain semicolons. This PR centralizes that parsing in a small neutral utility so the server and navigation shim share behavior without making hot server code import the public shim graph.
303,307, or308; omitted status still defaults to307.utils/redirect-digest.ts; server does not importvinext/shims/navigation-errors.What changed
307.307.parseInt()could accept prefixes like307garbage.303,307, or308.Maintainer review path
packages/vinext/src/utils/redirect-digest.tspackages/vinext/src/server/next-error-digest.tspackages/vinext/src/shims/navigation-errors.tstests/next-error-digest.test.tstests/error-boundary.test.tsValidation
vp test run tests/next-error-digest.test.ts tests/error-boundary.test.tsvp check packages/vinext/src/utils/redirect-digest.ts packages/vinext/src/server/next-error-digest.ts packages/vinext/src/shims/navigation-errors.ts tests/error-boundary.test.ts tests/next-error-digest.test.tsRisk / compatibility
307and existing push / replace decoding.Non-goals