Skip to content

Commit

Permalink
#42398 fix(app-router): escape non iso FlightRouterState segment value
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcus-Rise committed Nov 8, 2022
1 parent 6e3d969 commit 3c77a34
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 11 deletions.
31 changes: 31 additions & 0 deletions 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]
}
23 changes: 12 additions & 11 deletions 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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions test/production/app-dir-prefetch-non-iso-url/index.test.ts
Expand Up @@ -3,6 +3,7 @@ import { NextInstance } from 'test/lib/next-modes/base'
import { join } from 'path'
import { BrowserInterface } from '../../lib/browsers/base'
import webdriver from 'next-webdriver'
import { waitFor } from 'next-test-utils'

describe('app-dir-prefetch-non-iso-url', () => {
let next: NextInstance
Expand All @@ -25,6 +26,9 @@ describe('app-dir-prefetch-non-iso-url', () => {
await browser.elementByCss('#to-iso').click()

const text = await browser.elementByCss('#page').text()

await waitFor(3000)

expect(text).toBe('/[slug]')
} finally {
if (browser) {
Expand All @@ -41,6 +45,9 @@ describe('app-dir-prefetch-non-iso-url', () => {
await browser.elementByCss('#to-non-iso').click()

const text = await browser.elementByCss('#page').text()

await waitFor(3000)

expect(text).toBe('/[slug]')
} finally {
if (browser) {
Expand Down
95 changes: 95 additions & 0 deletions test/unit/app-router-headers.test.ts
@@ -0,0 +1,95 @@
import { escapeFlightRouterState } from 'next/client/components/app-router-headers'
import { FlightRouterState } from 'next/server/app-render'

const state1: FlightRouterState = [
'',
{
children: [
['slug', 'свитер-urbain', 'd'],
{
children: ['', {}],
},
],
},
null,
'refetch',
]
const state1Escaped: FlightRouterState = [
'',
{
children: [
['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'],
{
children: ['', {}],
},
],
},
null,
'refetch',
]

const state2: FlightRouterState = [
'',
{
children: [
['slug', 'свитер-urbain', 'd'],
{
children: ['', {}],
},
],
},
null,
null,
true,
]
const state2Escaped: FlightRouterState = [
'',
{
children: [
['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'],
{
children: ['', {}],
},
],
},
null,
null,
true,
]

const state3: FlightRouterState = [
'',
{
children: [
['slug', 'свитер-urbain', 'd'],
{
children: ['', {}],
},
null,
'refetch',
],
},
]
const state3Escaped: FlightRouterState = [
'',
{
children: [
['slug', '%D1%81%D0%B2%D0%B8%D1%82%D0%B5%D1%80-urbain', 'd'],
{
children: ['', {}],
},
null,
'refetch',
],
},
]

describe('escapeFlightRouterState', () => {
it.each([
{ state: state1, escapedState: state1Escaped },
{ state: state2, escapedState: state2Escaped },
{ state: state3, escapedState: state3Escaped },
])('should escape non-iso segment value', ({ state, escapedState }) => {
expect(escapeFlightRouterState(state)).toMatchObject(escapedState)
})
})

0 comments on commit 3c77a34

Please sign in to comment.