Skip to content

Commit e5507ff

Browse files
committed
omit searchParam data from FlightRouterState before transport (#80734)
Certain types of data gets stashed in the client-side `FlightRouterState`, such as the "refresh" marker alongside a URL that the client can callback to when it needs to revalidate segments that were "activated" on previously visited URLs. Additionally, query parameter data gets encoded in the `__PAGE__` segment key, but isn't actually needed by the server: `__PAGE__` segments would always be the leaf, so it wouldn't cause any unexpected behavior in the diffing logic. With large query params, this can lead to large amount of data needing to be sent to the server, only for it to not actually be used. There's a larger refactor around the server/client transport being handled in the `experimental.clientSegmentCache` flag, so this is a stop-gap until that behavior is stabilized. Closes NEXT-4521
1 parent 741d095 commit e5507ff

File tree

4 files changed

+417
-4
lines changed

4 files changed

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

0 commit comments

Comments
 (0)