Skip to content

Commit 70a71ab

Browse files
authored
Fix bad decoding for x-matched-path header (#78677)
This is a follow-up to #78326 which reverts the change that interferes with the added middleware rewrite test while also ensuring the encoding test from the `vercel/vercel` repo is still passing as expected. The issue with the encoding seems to be from the `x-matched-path` value being incorrectly decoded when using non-utf8 characters so this fixes the encoding. Fixes: https://github.com/vercel/next.js/actions/runs/14717733032/attempts/2 <details> <summary>test runs with patch</summary> ![CleanShot 2025-04-29 at 09 21 44@2x](https://github.com/user-attachments/assets/de5d66c5-5eb2-4e87-877e-875a7c937f35) ![CleanShot 2025-04-29 at 09 21 51@2x](https://github.com/user-attachments/assets/f1743f6c-d06c-4999-869a-9afaf1b69ab5) </details>
1 parent e3adfeb commit 70a71ab

File tree

3 files changed

+45
-4
lines changed

3 files changed

+45
-4
lines changed

packages/next/src/server/base-server.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ import {
177177
import { InvariantError } from '../shared/lib/invariant-error'
178178
import { decodeQueryPathParameter } from './lib/decode-query-path-parameter'
179179
import { getCacheHandlers } from './use-cache/handlers'
180+
import { fixMojibake } from './lib/fix-mojibake'
180181

181182
export type FindComponentsResult = {
182183
components: LoadComponentsReturnType
@@ -1096,7 +1097,7 @@ export default abstract class Server<
10961097
// x-matched-path is the source of truth, it tells what page
10971098
// should be rendered because we don't process rewrites in minimalMode
10981099
let { pathname: matchedPath } = new URL(
1099-
req.headers[MATCHED_PATH_HEADER] as string,
1100+
fixMojibake(req.headers[MATCHED_PATH_HEADER] as string),
11001101
'http://localhost'
11011102
)
11021103

@@ -1249,9 +1250,14 @@ export default abstract class Server<
12491250
if (pageIsDynamic) {
12501251
let params: ParsedUrlQuery | false = {}
12511252

1252-
// ensure we normalize the dynamic route params for encoding/
1253-
// default values
1254-
paramsResult = utils.normalizeDynamicRouteParams(queryParams, false)
1253+
// If we don't already have valid params, try to parse them from
1254+
// the query params.
1255+
if (!paramsResult.hasValidParams) {
1256+
paramsResult = utils.normalizeDynamicRouteParams(
1257+
queryParams,
1258+
false
1259+
)
1260+
}
12551261

12561262
// for prerendered ISR paths we attempt parsing the route
12571263
// params from the URL directly as route-matches may not
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { fixMojibake } from './fix-mojibake'
2+
3+
describe('Mojibake handling', () => {
4+
const validValues = [
5+
'hello-world',
6+
'hello world',
7+
encodeURIComponent('こんにちは'),
8+
]
9+
it.each(validValues)(
10+
'should maintain value when encoding is correct $1',
11+
(testValue) => {
12+
expect(fixMojibake(testValue)).toBe(testValue)
13+
}
14+
)
15+
16+
it('should fix invalid encoding', () => {
17+
expect(fixMojibake('/blog/ã\x81\x93ã\x82\x93ã\x81«ã\x81¡ã\x81¯')).toBe(
18+
'/blog/こんにちは'
19+
)
20+
})
21+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// x-matched-path header can be decoded incorrectly
2+
// and should only be utf8 characters so this fixes
3+
// incorrectly encoded values
4+
export function fixMojibake(input: string): string {
5+
// Convert each character's char code to a byte
6+
const bytes = new Uint8Array(input.length)
7+
for (let i = 0; i < input.length; i++) {
8+
bytes[i] = input.charCodeAt(i)
9+
}
10+
11+
// Decode the bytes as proper UTF-8
12+
const decoder = new TextDecoder('utf-8')
13+
return decoder.decode(bytes)
14+
}

0 commit comments

Comments
 (0)