nextjs - ✅(Solved) Fix Middleware rewrite: internal request URL re-serialized incorrectly so query param value containing & is split [2 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#89879Fetched 2026-04-08 00:21:11
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
0
Timeline (top)
cross-referenced ×2commented ×1issue_type_added ×1labeled ×1

Error Message

ERROR: rewriteUrl has secret

Fix Action

Fixed

PR fix notes

PR #89883: fix: improve query parameter encoding in middleware rewrites

Description (problem / solution / changelog)

This PR addresses issue #89879 where query parameter values containing & characters were being incorrectly split during middleware rewrites.

Changes

  1. Simplified stringifyQuery() in server-route-utils.ts to always encode query parameters using standard querystring.stringify(), removing conditional logic that bypassed encoding for certain parameters.

  2. Added query re-serialization in resolve-routes.ts after parsing middleware rewrite URLs to ensure proper encoding is preserved.

  3. Added e2e test case to verify query params with & characters are preserved through middleware rewrites.

Known Limitation

Additional query params added by middleware to URL values may still be lost due to formatUrl preferring query object over search property. This requires deeper investigation into the request handling pipeline.

Test Results

✓ should preserve & in query param values through middleware rewrite
✓ should handle URL with multiple query params containing &

Fixes #89879

Changed files

  • packages/next/src/server/lib/router-utils/resolve-routes.ts (modified, +7/-0)
  • packages/next/src/server/server-route-utils.ts (modified, +4/-26)
  • test/e2e/middleware-rewrite-query-encoding/middleware-rewrite-query-encoding.test.ts (added, +61/-0)
  • test/e2e/middleware-rewrite-query-encoding/middleware.js (added, +33/-0)
  • test/e2e/middleware-rewrite-query-encoding/pages/api/image-proxy.ts (added, +9/-0)
  • test/e2e/middleware-rewrite-query-encoding/pages/index.tsx (added, +3/-0)

PR #89884: fix: improve query parameter encoding in middleware rewrites

Description (problem / solution / changelog)

This PR addresses issue #89879 where query parameter values containing & characters were being incorrectly split during middleware rewrites.

Changes

  1. Simplified stringifyQuery() in server-route-utils.ts to always encode query parameters using standard querystring.stringify(), removing conditional logic that bypassed encoding for certain parameters.

  2. Added query re-serialization in resolve-routes.ts after parsing middleware rewrite URLs to ensure proper encoding is preserved.

  3. Added e2e test case to verify query params with & characters are preserved through middleware rewrites.

Known Limitation

Additional query params added by middleware to URL values may still be lost due to formatUrl preferring query object over search property. This requires deeper investigation into the request handling pipeline.

Test Results

✓ should preserve & in query param values through middleware rewrite
✓ should handle URL with multiple query params containing &

Fixes #89879

Changed files

  • packages/next/src/server/lib/router-utils/resolve-routes.ts (modified, +7/-0)
  • packages/next/src/server/server-route-utils.ts (modified, +4/-26)
  • test/e2e/middleware-rewrite-query-encoding/middleware-rewrite-query-encoding.test.ts (added, +61/-0)
  • test/e2e/middleware-rewrite-query-encoding/middleware.js (added, +33/-0)
  • test/e2e/middleware-rewrite-query-encoding/pages/api/image-proxy.ts (added, +9/-0)
  • test/e2e/middleware-rewrite-query-encoding/pages/index.tsx (added, +3/-0)

Code Example

try {
    const rewriteUrl = new URL(urlPathAndBeyond(request.nextUrl), serverURL);
    console.log(
        `rewriteUrl before url search param is set: ${inspect(rewriteUrl)}`
    );
    const imageUrl = new URL(request.nextUrl.searchParams.get("url")!);
    console.log(`imageUrl: ${inspect(imageUrl)}`);
    if (imageUrl.searchParams.has("secret")) {
        return NextResponse.next();
    }
    imageUrl.searchParams.set("secret", imageSecret);
    rewriteUrl.searchParams.set("url", imageUrl.toString());
    // sanity check: rewriteUrl should not have secret
    if (rewriteUrl.searchParams.has("secret")) {
        throw new Error("rewriteUrl has secret");
    }
    console.log(`rewriting to ${inspect(rewriteUrl)}`);
    return NextResponse.rewrite(rewriteUrl);
} catch (error) {
    console.error(error);
}

---

rewriteUrl before url search param is set: URL {
  href: 'http://localhost:3000/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fmedia%2Ffile%2Ftest.png%3F2026-02-11T12%3A20%3A48.699Z&w=3840&q=75',
  ...
  searchParams: URLSearchParams {
    'url' => 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z',
    'w' => '3840',
    'q' => '75' },
}

imageUrl: URL {
  href: 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z',
  ...
}

rewriting to URL {
  href: 'http://localhost:3000/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fmedia%2Ffile%2Ftest.png%3F2026-02-11T12%253A20%253A48.699Z%3D%26secret%3Dsuper-secret-image-secret&w=3840&q=75',
  ...
  searchParams: URLSearchParams {
    'url' => 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z=&secret=super-secret-image-secret',
    'w' => '3840',
    'q' => '75' },
}

---

rewriteUrl before url search param is set: URL {
  href: 'http://localhost:3000/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fmedia%2Ffile%2Ftest.png%3F2026-02-11T12%3A20%3A48.699Z%3D&secret=super-secret-image-secret&w=3840&q=75',
  ...
  searchParams: URLSearchParams {
    'url' => 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z=',
    'secret' => 'super-secret-image-secret',
    'w' => '3840',
    'q' => '75' },
}

imageUrl: URL {
  href: 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z=',
  ...
}

---

ERROR: rewriteUrl has secret

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:40 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6000
  Available memory (MB): 32768
  Available CPU cores: 10
Binaries:
  Node: 24.8.0
  npm: 11.6.0
  Yarn: N/A
  pnpm: 10.12.1
Relevant Packages:
  next: 16.1.6 // Latest available version is detected (16.1.6).
  eslint-config-next: N/A
  react: 19.1.0
  react-dom: 19.1.0
  typescript: 5.9.3
Next.js Config:
  output: standalone
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/DanielGiljam/nextjs-internal-request-url-re-serialization-issue

To Reproduce

Middleware rewrites /_next/image and injects a secret query param into the image URL that is passed as the url param. The image URL can already have a query string (e.g. a cache-busting timestamp), so after adding secret, the value of url looks like: http://localhost:3000/api/media/file/...?2026-02-11T12:20:48.699Z=&secret=super-secret-image-secret i.e. it contains a literal "&".

Relevant middleware code:

try {
    const rewriteUrl = new URL(urlPathAndBeyond(request.nextUrl), serverURL);
    console.log(
        `rewriteUrl before url search param is set: ${inspect(rewriteUrl)}`
    );
    const imageUrl = new URL(request.nextUrl.searchParams.get("url")!);
    console.log(`imageUrl: ${inspect(imageUrl)}`);
    if (imageUrl.searchParams.has("secret")) {
        return NextResponse.next();
    }
    imageUrl.searchParams.set("secret", imageSecret);
    rewriteUrl.searchParams.set("url", imageUrl.toString());
    // sanity check: rewriteUrl should not have secret
    if (rewriteUrl.searchParams.has("secret")) {
        throw new Error("rewriteUrl has secret");
    }
    console.log(`rewriting to ${inspect(rewriteUrl)}`);
    return NextResponse.rewrite(rewriteUrl);
} catch (error) {
    console.error(error);
}

First request (correct)

Incoming: /_next/image?url=...&w=3840&q=75 (no secret). We set url to the image URL including ?timestamp=&secret=.... After rewriteUrl.searchParams.set("url", imageUrl.toString()), the serialized rewrite URL is correct (e.g. "&" inside url is "%26"):

rewriteUrl before url search param is set: URL {
  href: 'http://localhost:3000/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fmedia%2Ffile%2Ftest.png%3F2026-02-11T12%3A20%3A48.699Z&w=3840&q=75',
  ...
  searchParams: URLSearchParams {
    'url' => 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z',
    'w' => '3840',
    'q' => '75' },
}

imageUrl: URL {
  href: 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z',
  ...
}

rewriting to URL {
  href: 'http://localhost:3000/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fmedia%2Ffile%2Ftest.png%3F2026-02-11T12%253A20%253A48.699Z%3D%26secret%3Dsuper-secret-image-secret&w=3840&q=75',
  ...
  searchParams: URLSearchParams {
    'url' => 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z=&secret=super-secret-image-secret',
    'w' => '3840',
    'q' => '75' },
}

So on the first run, rewriteUrl has no top-level secret, only inside url.

Second request (internal rewrite request – bug)

The internal request that Next.js sends for the rewrite hits the same middleware. Its URL is parsed so that the url param value is truncated at the first "&", and secret appears as a top-level param:

rewriteUrl before url search param is set: URL {
  href: 'http://localhost:3000/_next/image?url=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fmedia%2Ffile%2Ftest.png%3F2026-02-11T12%3A20%3A48.699Z%3D&secret=super-secret-image-secret&w=3840&q=75',
  ...
  searchParams: URLSearchParams {
    'url' => 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z=',
    'secret' => 'super-secret-image-secret',
    'w' => '3840',
    'q' => '75' },
}

imageUrl: URL {
  href: 'http://localhost:3000/api/media/file/test.png?2026-02-11T12:20:48.699Z=',
  ...
}

So on the internal request, url is cut at "&" and secret is leaked to the top level. The sanity check rewriteUrl.searchParams.has("secret") then throws:

ERROR: rewriteUrl has secret

Current vs. Expected behavior

Current

The internal request URL appears to be built from decoded search params and re-serialized without encoding "&" inside the url param value, so the value is split and secret becomes a separate top-level query parameter.

Expected

The internal request for the rewrite should use a query string that preserves the url param value (with "&" encoded as "%26"), so that parsing it again yields a single url param whose value is the full image URL including &secret=....

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:40 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6000
  Available memory (MB): 32768
  Available CPU cores: 10
Binaries:
  Node: 24.8.0
  npm: 11.6.0
  Yarn: N/A
  pnpm: 10.12.1
Relevant Packages:
  next: 16.1.6 // Latest available version is detected (16.1.6).
  eslint-config-next: N/A
  react: 19.1.0
  react-dom: 19.1.0
  typescript: 5.9.3
Next.js Config:
  output: standalone

Which area(s) are affected? (Select all that apply)

Middleware

Which stage(s) are affected? (Select all that apply)

next dev (local), next start (local), Other (Deployed)

Additional context

When middleware (proxy.ts uses NextResponse.rewrite(rewriteUrl) with a URL whose search param value contains a literal "&", the internal request that Next.js issues for the rewrite is built in a way that does not encode that "&". The value is therefore split at "&", so the next middleware run sees extra top-level params and a truncated param.

extent analysis

Fix: Encode the inner url value before putting it into the outer rewrite URL

The problem is caused by Next.js re‑serialising the outer URL after it has already decoded the inner query string.
When the inner URL contains an un‑escaped & (the secret param you add), the outer query string is split and the secret “leaks” to the top level.

The reliable workaround is to URL‑encode the whole inner URL (or

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - ✅(Solved) Fix Middleware rewrite: internal request URL re-serialized incorrectly so query param value containing & is split [2 pull requests, 2 comments, 2 participants]