Skip to content

Commit 632d4ab

Browse files
authored
backport: omit searchParam data from FlightRouterState before transport (#80734) (#81261)
Backports: - #80734
1 parent 4a27070 commit 632d4ab

File tree

4 files changed

+390
-4
lines changed

4 files changed

+390
-4
lines changed

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { findSourceMapURL } from '../../app-find-source-map-url'
3333
import { PrefetchKind } from './router-reducer-types'
3434
import {
3535
normalizeFlightData,
36+
prepareFlightRouterStateForRequest,
3637
type NormalizedFlightData,
3738
} from '../../flight-data-helpers'
3839
import { getAppBuildId } from '../../app-build-id'
@@ -126,8 +127,9 @@ export async function fetchServerResponse(
126127
// Enable flight response
127128
[RSC_HEADER]: '1',
128129
// Provide the current router state
129-
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(
130-
JSON.stringify(flightRouterState)
130+
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(
131+
flightRouterState,
132+
options.isHmrRefresh
131133
),
132134
}
133135

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { handleSegmentMismatch } from '../handle-segment-mismatch'
4545
import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments'
4646
import {
4747
normalizeFlightData,
48+
prepareFlightRouterStateForRequest,
4849
type NormalizedFlightData,
4950
} from '../../../flight-data-helpers'
5051
import { getRedirectError } from '../../redirect'
@@ -92,8 +93,8 @@ async function fetchServerAction(
9293
headers: {
9394
Accept: RSC_CONTENT_TYPE_HEADER,
9495
[ACTION_HEADER]: actionId,
95-
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(
96-
JSON.stringify(state.tree)
96+
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(
97+
state.tree
9798
),
9899
...(process.env.NEXT_DEPLOYMENT_ID
99100
? {
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { prepareFlightRouterStateForRequest } from './flight-data-helpers'
2+
import type { FlightRouterState } from '../server/app-render/types'
3+
4+
describe('prepareFlightRouterStateForRequest', () => {
5+
describe('HMR refresh handling', () => {
6+
it('should preserve complete state for HMR refresh requests', () => {
7+
const flightRouterState: FlightRouterState = [
8+
'__PAGE__?{"sensitive":"data"}',
9+
{},
10+
'/some/url',
11+
'refresh',
12+
true,
13+
]
14+
15+
const result = prepareFlightRouterStateForRequest(flightRouterState, true)
16+
const decoded = JSON.parse(decodeURIComponent(result))
17+
18+
expect(decoded).toEqual(flightRouterState)
19+
})
20+
})
21+
22+
describe('__PAGE__ segment handling', () => {
23+
it('should strip search params from __PAGE__ segments', () => {
24+
const flightRouterState: FlightRouterState = [
25+
'__PAGE__?{"param":"value","foo":"bar"}',
26+
{},
27+
]
28+
29+
const result = prepareFlightRouterStateForRequest(flightRouterState)
30+
const decoded = JSON.parse(decodeURIComponent(result))
31+
32+
expect(decoded[0]).toBe('__PAGE__')
33+
})
34+
35+
it('should preserve non-page segments', () => {
36+
const flightRouterState: FlightRouterState = ['regular-segment', {}]
37+
38+
const result = prepareFlightRouterStateForRequest(flightRouterState)
39+
const decoded = JSON.parse(decodeURIComponent(result))
40+
41+
expect(decoded[0]).toBe('regular-segment')
42+
})
43+
44+
it('should preserve dynamic segments', () => {
45+
const dynamicSegment: [string, string, 'd'] = ['slug', 'test-value', 'd']
46+
const flightRouterState: FlightRouterState = [dynamicSegment, {}]
47+
48+
const result = prepareFlightRouterStateForRequest(flightRouterState)
49+
const decoded = JSON.parse(decodeURIComponent(result))
50+
51+
expect(decoded[0]).toEqual(dynamicSegment)
52+
})
53+
})
54+
55+
describe('URL stripping', () => {
56+
it('should always set URL (index 2) to null', () => {
57+
const flightRouterState: FlightRouterState = [
58+
'segment',
59+
{},
60+
'/sensitive/url/path',
61+
null,
62+
]
63+
64+
const result = prepareFlightRouterStateForRequest(flightRouterState)
65+
const decoded = JSON.parse(decodeURIComponent(result))
66+
67+
expect(decoded[2]).toBeNull()
68+
})
69+
})
70+
71+
describe('refresh marker handling', () => {
72+
it('should preserve "refetch" marker', () => {
73+
const flightRouterState: FlightRouterState = [
74+
'segment',
75+
{},
76+
'/url',
77+
'refetch',
78+
]
79+
80+
const result = prepareFlightRouterStateForRequest(flightRouterState)
81+
const decoded = JSON.parse(decodeURIComponent(result))
82+
83+
expect(decoded[3]).toBe('refetch')
84+
})
85+
86+
it('should preserve "inside-shared-layout" marker', () => {
87+
const flightRouterState: FlightRouterState = [
88+
'segment',
89+
{},
90+
'/url',
91+
'inside-shared-layout',
92+
]
93+
94+
const result = prepareFlightRouterStateForRequest(flightRouterState)
95+
const decoded = JSON.parse(decodeURIComponent(result))
96+
97+
expect(decoded[3]).toBe('inside-shared-layout')
98+
})
99+
100+
it('should strip "refresh" marker (client-only)', () => {
101+
const flightRouterState: FlightRouterState = [
102+
'segment',
103+
{},
104+
'/url',
105+
'refresh',
106+
]
107+
108+
const result = prepareFlightRouterStateForRequest(flightRouterState)
109+
const decoded = JSON.parse(decodeURIComponent(result))
110+
111+
expect(decoded[3]).toBeNull()
112+
})
113+
114+
it('should strip null refresh marker', () => {
115+
const flightRouterState: FlightRouterState = ['segment', {}, '/url', null]
116+
117+
const result = prepareFlightRouterStateForRequest(flightRouterState)
118+
const decoded = JSON.parse(decodeURIComponent(result))
119+
120+
expect(decoded[3]).toBeNull()
121+
})
122+
})
123+
124+
describe('optional fields preservation', () => {
125+
it('should preserve isRootLayout when true', () => {
126+
const flightRouterState: FlightRouterState = [
127+
'segment',
128+
{},
129+
null,
130+
null,
131+
true,
132+
]
133+
134+
const result = prepareFlightRouterStateForRequest(flightRouterState)
135+
const decoded = JSON.parse(decodeURIComponent(result))
136+
137+
expect(decoded[4]).toBe(true)
138+
})
139+
140+
it('should preserve isRootLayout when false', () => {
141+
const flightRouterState: FlightRouterState = [
142+
'segment',
143+
{},
144+
null,
145+
null,
146+
false,
147+
]
148+
149+
const result = prepareFlightRouterStateForRequest(flightRouterState)
150+
const decoded = JSON.parse(decodeURIComponent(result))
151+
152+
expect(decoded[4]).toBe(false)
153+
})
154+
155+
it('should handle minimal FlightRouterState (only segment and parallelRoutes)', () => {
156+
const flightRouterState: FlightRouterState = ['segment', {}]
157+
158+
const result = prepareFlightRouterStateForRequest(flightRouterState)
159+
const decoded = JSON.parse(decodeURIComponent(result))
160+
161+
expect(decoded).toEqual([
162+
'segment',
163+
{},
164+
null, // URL
165+
null, // refresh marker
166+
])
167+
})
168+
})
169+
170+
describe('recursive processing of parallel routes', () => {
171+
it('should recursively process nested parallel routes', () => {
172+
const flightRouterState: FlightRouterState = [
173+
'parent',
174+
{
175+
children: [
176+
'__PAGE__?{"nested":"param"}',
177+
{},
178+
'/nested/url',
179+
'refresh',
180+
],
181+
modal: ['modal-segment', {}, '/modal/url', 'refetch'],
182+
},
183+
'/parent/url',
184+
'inside-shared-layout',
185+
]
186+
187+
const result = prepareFlightRouterStateForRequest(flightRouterState)
188+
const decoded = JSON.parse(decodeURIComponent(result))
189+
190+
expect(decoded).toEqual([
191+
'parent',
192+
{
193+
children: [
194+
'__PAGE__', // search params stripped
195+
{},
196+
null, // URL stripped
197+
null, // 'refresh' marker stripped
198+
],
199+
modal: [
200+
'modal-segment',
201+
{},
202+
null, // URL stripped
203+
'refetch', // server marker preserved
204+
],
205+
},
206+
null, // URL stripped
207+
'inside-shared-layout', // server marker preserved
208+
])
209+
})
210+
211+
it('should handle deeply nested parallel routes', () => {
212+
const flightRouterState: FlightRouterState = [
213+
'root',
214+
{
215+
children: [
216+
'level1',
217+
{
218+
children: [
219+
'__PAGE__?{"deep":"nesting"}',
220+
{},
221+
'/deep/url',
222+
'refetch',
223+
],
224+
},
225+
],
226+
},
227+
]
228+
229+
const result = prepareFlightRouterStateForRequest(flightRouterState)
230+
const decoded = JSON.parse(decodeURIComponent(result))
231+
232+
expect(decoded[1].children[1].children[0]).toBe('__PAGE__')
233+
expect(decoded[1].children[1].children[2]).toBeNull()
234+
expect(decoded[1].children[1].children[3]).toBe('refetch')
235+
})
236+
})
237+
238+
describe('real-world scenarios', () => {
239+
it('should handle complex FlightRouterState with all features', () => {
240+
const complexState: FlightRouterState = [
241+
'__PAGE__?{"userId":"123"}',
242+
{
243+
children: [
244+
'dashboard',
245+
{
246+
modal: [
247+
'__PAGE__?{"modalParam":"data"}',
248+
{},
249+
'/modal/path',
250+
'refresh',
251+
false,
252+
],
253+
},
254+
'/dashboard/url',
255+
'refetch',
256+
true,
257+
],
258+
sidebar: [['slug', 'user-123', 'd'], {}, '/sidebar/url', null],
259+
},
260+
'/main/url',
261+
'inside-shared-layout',
262+
true,
263+
]
264+
265+
const result = prepareFlightRouterStateForRequest(complexState)
266+
const decoded = JSON.parse(decodeURIComponent(result))
267+
268+
// Root level checks
269+
expect(decoded[0]).toBe('__PAGE__') // search params stripped
270+
expect(decoded[2]).toBeNull() // URL stripped
271+
expect(decoded[3]).toBe('inside-shared-layout') // server marker preserved
272+
expect(decoded[4]).toBe(true) // isRootLayout preserved
273+
274+
// Children route checks
275+
const childrenRoute = decoded[1].children
276+
expect(childrenRoute[2]).toBeNull() // URL stripped
277+
expect(childrenRoute[3]).toBe('refetch') // server marker preserved
278+
expect(childrenRoute[4]).toBe(true) // isRootLayout preserved
279+
280+
// Modal route checks
281+
const modalRoute = childrenRoute[1].modal
282+
expect(modalRoute[0]).toBe('__PAGE__') // search params stripped
283+
expect(modalRoute[2]).toBeNull() // URL stripped
284+
expect(modalRoute[3]).toBeNull() // 'refresh' marker stripped
285+
expect(modalRoute[4]).toBe(false) // isRootLayout preserved
286+
287+
// Sidebar route (dynamic segment) checks
288+
const sidebarRoute = decoded[1].sidebar
289+
expect(sidebarRoute[0]).toEqual(['slug', 'user-123', 'd']) // dynamic segment preserved
290+
expect(sidebarRoute[2]).toBeNull() // URL stripped
291+
expect(sidebarRoute[3]).toBeNull() // null marker remains null
292+
})
293+
})
294+
})

0 commit comments

Comments
 (0)