diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts index 1eb2747dd1cd22b..aef43864e0a70ed 100644 --- a/packages/next/client/components/app-router-headers.ts +++ b/packages/next/client/components/app-router-headers.ts @@ -1,5 +1,36 @@ +import { + DynamicParamTypesShort, + FlightRouterState, +} from '../../server/app-render' + export const RSC = 'RSC' as const export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const export const RSC_VARY_HEADER = `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const + +export const escapeFlightRouterState = ( + state: FlightRouterState +): FlightRouterState => { + const [segment, parallelRoutes, ...restState] = state + const escapedParallelRoutes: typeof parallelRoutes = Object.create({}) + let escapedSegment: typeof segment = segment + + if (typeof segment !== 'string') { + const [param, value, type] = segment + const escapedSegmentValue = encodeURIComponent(value) + escapedSegment = [ + param, + escapedSegmentValue, + type as DynamicParamTypesShort, + ] + } + + Object.keys(parallelRoutes).forEach((key) => { + const childState = parallelRoutes[key] + + escapedParallelRoutes[key] = escapeFlightRouterState(childState) + }) + + return [escapedSegment, escapedParallelRoutes, ...restState] +} diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx index d29f680d5051c63..5788d81e507f952 100644 --- a/packages/next/client/components/app-router.tsx +++ b/packages/next/client/components/app-router.tsx @@ -1,18 +1,18 @@ 'use client' import type { ReactNode } from 'react' -import React, { useEffect, useMemo, useCallback } from 'react' +import React, { useCallback, useEffect, useMemo } from 'react' import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack/client' +import type { + AppRouterInstance, + CacheNode, +} from '../../shared/lib/app-router-context' import { AppRouterContext, - LayoutRouterContext, GlobalLayoutRouterContext, + LayoutRouterContext, } from '../../shared/lib/app-router-context' -import type { - CacheNode, - AppRouterInstance, -} from '../../shared/lib/app-router-context' -import type { FlightRouterState, FlightData } from '../../server/app-render' +import type { FlightData, FlightRouterState } from '../../server/app-render' import { ACTION_NAVIGATE, ACTION_PREFETCH, @@ -22,14 +22,13 @@ import { reducer, } from './reducer' import { - SearchParamsContext, - // ParamsContext, PathnameContext, - // LayoutSegmentsContext, + SearchParamsContext, } from '../../shared/lib/hooks-client-context' import { useReducerWithReduxDevtools } from './use-reducer-with-devtools' import { ErrorBoundary, GlobalErrorComponent } from './error-boundary' import { + escapeFlightRouterState, NEXT_ROUTER_PREFETCH, NEXT_ROUTER_STATE_TREE, RSC, @@ -65,7 +64,9 @@ export async function fetchServerResponse( // Enable flight response [RSC]: '1', // Provide the current router state - [NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState), + [NEXT_ROUTER_STATE_TREE]: JSON.stringify( + escapeFlightRouterState(flightRouterState) + ), } if (prefetch) { // Enable prefetch response diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index d5fd2404538d9b1..8f122e0790cda26 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -855,7 +855,13 @@ export async function renderToHTMLOrFlight( } const key = segmentParam.param - const value = pathParams[key] + let value = pathParams[key] + + if (Array.isArray(value)) { + value = value.map((i) => encodeURIComponent(i)) + } else if (typeof value === 'string') { + value = encodeURIComponent(value) + } if (!value) { // Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard` diff --git a/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js b/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js new file mode 100644 index 000000000000000..90ee69c678574b3 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/[slug]/page.js @@ -0,0 +1,12 @@ +export default function Slug(props) { + return ( + <> +
/[slug]
+{JSON.stringify(props)}
+ > + ) +} + +export function generateStaticParams() { + return [{ slug: 'iso-url' }, { slug: 'кириллица' }] +} diff --git a/test/production/app-dir-prefetch-non-iso-url/app/layout.js b/test/production/app-dir-prefetch-non-iso-url/app/layout.js new file mode 100644 index 000000000000000..750eb927b198012 --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/layout.js @@ -0,0 +1,7 @@ +export default function Layout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir-prefetch-non-iso-url/app/page.js b/test/production/app-dir-prefetch-non-iso-url/app/page.js new file mode 100644 index 000000000000000..786b1ea934a51de --- /dev/null +++ b/test/production/app-dir-prefetch-non-iso-url/app/page.js @@ -0,0 +1,18 @@ +import Link from 'next/link' + +export default function Page(props) { + return ( + <> +index
+{JSON.stringify(props)}
+ + /iso-url + +