Files
cloudflare__vinext/tests/next-error-digest.test.ts
Nathan Nguyen 7e721b3e53 fix(app-router): preserve semicolons in redirect digests (#2487)
* fix(app-router): preserve raw redirect digest URLs

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.

* fix: simplify malformed redirect digest fallback

* fix: simplify malformed redirect digest fallback

* test: cover malformed redirect digest tails consistently

* fix: restore unstable rethrow cause handling

* fix: guard decodeURIComponent throw and comment status allowlist

- Wrap decodeURIComponent in try/catch for malformed lone % in raw URLs
- Add clarifying comment that only canonical 303/307/308 are extracted
- Both address ask-bonk review feedback from PR #2487

* fix(app-router): preserve raw redirect digest URLs

* fix(app-router): support empty redirect destinations

* docs(app-router): explain redirect digest formats

* docs(app-router): clarify redirect digest parsing

* test(app-router): cover redirect digest re-emission

* docs(app-router): document redirect parser invariants

---------

Co-authored-by: James <james@eli.cx>
2026-07-08 10:55:33 +01:00

101 lines
3.1 KiB
TypeScript

import { describe, expect, it } from "vite-plus/test";
import { parseNextRedirectDigest } from "../packages/vinext/src/server/next-error-digest.js";
describe("next error digest parsing", () => {
// Mirrors Next.js redirect type semantics from:
// https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts
it("preserves an omitted redirect type so callers can apply context-sensitive defaults", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;;%2Fdashboard;307")).toEqual({
status: 307,
type: null,
url: "/dashboard",
});
});
it("preserves explicit redirect types", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;%2Flogin;308")).toEqual({
status: 308,
type: "replace",
url: "/login",
});
expect(parseNextRedirectDigest("NEXT_REDIRECT;push;%2Fprofile;307")).toEqual({
status: 307,
type: "push",
url: "/profile",
});
});
it("preserves non-empty redirect type segments as raw digest data", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;custom;%2Fprofile;307")).toEqual({
status: 307,
type: "custom",
url: "/profile",
});
});
it("preserves semicolons inside redirect URLs", () => {
expect(
parseNextRedirectDigest(
"NEXT_REDIRECT;replace;javascript:window.location.assign('/boom');;307;",
),
).toEqual({
status: 307,
type: "replace",
url: "javascript:window.location.assign('/boom');",
});
});
it("preserves percent escapes and literal percent signs in Next-style raw URLs", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;/docs%2Fguide%3Bpart;307;")).toEqual({
status: 307,
type: "replace",
url: "/docs%2Fguide%3Bpart",
});
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;/discount/100%;307;")).toEqual({
status: 307,
type: "replace",
url: "/discount/100%",
});
});
it("decodes vinext's encoded redirect URLs", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;%2Fdocs%252Fguide%253Bpart;307")).toEqual(
{
status: 307,
type: "replace",
url: "/docs%2Fguide%3Bpart",
},
);
});
it("accepts empty redirect URLs", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;;307;")).toEqual({
status: 307,
type: "replace",
url: "",
});
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;;307")).toEqual({
status: 307,
type: "replace",
url: "",
});
});
it("preserves semicolons inside redirect URLs when status is omitted", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;%2Fdocs%3Bpart")).toEqual({
status: 307,
type: "replace",
url: "/docs;part",
});
});
it("rejects malformed Next-style trailing status segments", () => {
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;/foo;307garbage;")).toBeNull();
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;/foo;abc;")).toBeNull();
expect(parseNextRedirectDigest("NEXT_REDIRECT;replace;/foo;")).toBeNull();
});
});