From 9260e33802a95e4f7a5ba8338cc2d315fffe0123 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 4 Nov 2022 13:58:12 +0100 Subject: [PATCH 1/6] Rename RSC and Router headers Removes the `_` prefix from the RSC and Router headers as nginx strips `_` prefixed headers by default. --- .../client/components/app-router-headers.ts | 3 +++ .../next/client/components/app-router.tsx | 17 ++++++++----- packages/next/server/app-render.tsx | 25 ++++++++++++------- packages/next/server/base-server.ts | 20 +++++++++------ packages/next/server/internal-utils.ts | 5 ---- packages/next/server/web/adapter.ts | 13 +++++++--- test/e2e/app-dir/app/middleware.js | 4 +-- test/e2e/app-dir/index.test.ts | 4 +-- test/e2e/app-dir/rsc-basic.test.ts | 15 +++++------ test/e2e/switchable-runtime/index.test.ts | 4 +-- 10 files changed, 64 insertions(+), 46 deletions(-) create mode 100644 packages/next/client/components/app-router-headers.ts diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts new file mode 100644 index 000000000000..4f1153e41802 --- /dev/null +++ b/packages/next/client/components/app-router-headers.ts @@ -0,0 +1,3 @@ +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 diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx index a1ed740d1094..d29f680d5051 100644 --- a/packages/next/client/components/app-router.tsx +++ b/packages/next/client/components/app-router.tsx @@ -29,6 +29,11 @@ import { } from '../../shared/lib/hooks-client-context' import { useReducerWithReduxDevtools } from './use-reducer-with-devtools' import { ErrorBoundary, GlobalErrorComponent } from './error-boundary' +import { + NEXT_ROUTER_PREFETCH, + NEXT_ROUTER_STATE_TREE, + RSC, +} from './app-router-headers' function urlToUrlWithoutFlightMarker(url: string): URL { const urlWithoutFlightParameters = new URL(url, location.origin) @@ -53,18 +58,18 @@ export async function fetchServerResponse( prefetch?: true ): Promise<[FlightData: FlightData, canonicalUrlOverride: URL | undefined]> { const headers: { - __rsc__: '1' - __next_router_state_tree__: string - __next_router_prefetch__?: '1' + [RSC]: '1' + [NEXT_ROUTER_STATE_TREE]: string + [NEXT_ROUTER_PREFETCH]?: '1' } = { // Enable flight response - __rsc__: '1', + [RSC]: '1', // Provide the current router state - __next_router_state_tree__: JSON.stringify(flightRouterState), + [NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState), } if (prefetch) { // Enable prefetch response - headers.__next_router_prefetch__ = '1' + headers[NEXT_ROUTER_PREFETCH] = '1' } const res = await fetch(url.toString(), { diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 277420c014b2..ceb492784a2c 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -38,6 +38,11 @@ import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found' import { HeadManagerContext } from '../shared/lib/head-manager-context' import { Writable } from 'stream' import stringHash from 'next/dist/compiled/string-hash' +import { + NEXT_ROUTER_PREFETCH, + NEXT_ROUTER_STATE_TREE, + RSC, +} from '../client/components/app-router-headers' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -686,16 +691,16 @@ function getScriptNonceFromHeader(cspHeaderValue: string): string | undefined { return nonce } -const FLIGHT_PARAMETERS = [ - '__rsc__', - '__next_router_state_tree__', - '__next_router_prefetch__', +export const FLIGHT_PARAMETERS = [ + [RSC], + [NEXT_ROUTER_STATE_TREE], + [NEXT_ROUTER_PREFETCH], ] as const function headersWithoutFlight(headers: IncomingHttpHeaders) { const newHeaders = { ...headers } for (const param of FLIGHT_PARAMETERS) { - delete newHeaders[param] + delete newHeaders[param.toString().toLowerCase()] } return newHeaders } @@ -728,7 +733,7 @@ export async function renderToHTMLOrFlight( */ const isStaticGeneration = renderOpts.supportsDynamicHTML !== true && !renderOpts.isBot - const isFlight = req.headers.__rsc__ !== undefined + const isFlight = req.headers[RSC.toString().toLowerCase()] !== undefined const capturedErrors: Error[] = [] const allCapturedErrors: Error[] = [] @@ -786,7 +791,7 @@ export async function renderToHTMLOrFlight( // don't modify original query object query = Object.assign({}, query) - const isPrefetch = req.headers.__next_router_prefetch__ !== undefined + const isPrefetch = req.headers[NEXT_ROUTER_PREFETCH] !== undefined // TODO-APP: verify the tree is valid // TODO-APP: verify query param is single value (not an array) @@ -795,8 +800,10 @@ export async function renderToHTMLOrFlight( * Router state provided from the client-side router. Used to handle rendering from the common layout down. */ let providedFlightRouterState: FlightRouterState = isFlight - ? req.headers.__next_router_state_tree__ - ? JSON.parse(req.headers.__next_router_state_tree__ as string) + ? req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()] + ? JSON.parse( + req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()] as string + ) : undefined : undefined diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 91e01bda3d29..dd2a3aaa4c1e 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -74,6 +74,12 @@ import { getHostname } from '../shared/lib/get-hostname' import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' import { MiddlewareMatcher } from '../build/analysis/get-page-static-info' +import { + NEXT_ROUTER_PREFETCH, + NEXT_ROUTER_STATE_TREE, + RSC, +} from '../client/components/app-router-headers' +import { FLIGHT_PARAMETERS } from './app-render' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -1059,10 +1065,10 @@ export default abstract class Server { if (isAppPath) { res.setHeader( 'vary', - '__rsc__, __next_router_state_tree__, __next_router_prefetch__' + `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` ) - if (isSSG && req.headers['__rsc__']) { + if (isSSG && req.headers[RSC.toLowerCase()]) { if (!this.minimalMode) { isDataReq = true } @@ -1071,9 +1077,9 @@ export default abstract class Server { opts.runtime !== 'experimental-edge' || (this.serverOptions as any).webServerConfig ) { - delete req.headers['__rsc__'] - delete req.headers['__next_router_state_tree__'] - delete req.headers['__next_router_prefetch__'] + for (const param of FLIGHT_PARAMETERS) { + delete req.headers[param.toString().toLowerCase()] + } } } } @@ -1100,9 +1106,9 @@ export default abstract class Server { ) } - // Don't delete query.__rsc__ yet, it still needs to be used in renderToHTML later + // Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later const isFlightRequest = Boolean( - this.serverComponentManifest && req.headers.__rsc__ + this.serverComponentManifest && req.headers[RSC.toLowerCase()] ) // we need to ensure the status code if /404 is visited directly diff --git a/packages/next/server/internal-utils.ts b/packages/next/server/internal-utils.ts index 76f41fc0cecb..b51e327ac8b7 100644 --- a/packages/next/server/internal-utils.ts +++ b/packages/next/server/internal-utils.ts @@ -5,11 +5,6 @@ const INTERNAL_QUERY_NAMES = [ '__nextLocale', '__nextDefaultLocale', '__nextIsNotFound', - // RSC - '__rsc__', - // Routing - '__next_router_state_tree__', - '__next_router_prefetch__', ] as const const EXTENDED_INTERNAL_QUERY_NAMES = ['__nextDataReq'] as const diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index 49a8ec2b5288..062dca353ae0 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -10,6 +10,11 @@ import { waitUntilSymbol } from './spec-extension/fetch-event' import { NextURL } from './next-url' import { stripInternalSearchParams } from '../internal-utils' import { normalizeRscPath } from '../../shared/lib/router/utils/app-paths' +import { + NEXT_ROUTER_PREFETCH, + NEXT_ROUTER_STATE_TREE, + RSC, +} from '../../client/components/app-router-headers' class NextRequestHint extends NextRequest { sourcePage: string @@ -37,9 +42,9 @@ class NextRequestHint extends NextRequest { } const FLIGHT_PARAMETERS = [ - '__rsc__', - '__next_router_state_tree__', - '__next_router_prefetch__', + [RSC], + [NEXT_ROUTER_STATE_TREE], + [NEXT_ROUTER_PREFETCH], ] as const export async function adapter(params: { @@ -71,7 +76,7 @@ export async function adapter(params: { // Parameters should only be stripped for middleware if (!isEdgeRendering) { for (const param of FLIGHT_PARAMETERS) { - requestHeaders.delete(param) + requestHeaders.delete(param.toString().toLowerCase()) } } diff --git a/test/e2e/app-dir/app/middleware.js b/test/e2e/app-dir/app/middleware.js index 7ef85d902a36..2a53ed94317a 100644 --- a/test/e2e/app-dir/app/middleware.js +++ b/test/e2e/app-dir/app/middleware.js @@ -35,8 +35,8 @@ export function middleware(request) { ? 'rewrite' : 'redirect' - const internal = ['__rsc__', '__next_router_state_tree__'] - if (internal.some((name) => request.headers.has(name))) { + const internal = ['RSC', 'Next-Router-State-Tree'] + if (internal.some((name) => request.headers.has(name.toLowerCase()))) { return NextResponse[method](new URL('/internal/failure', request.url)) } diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index a87c46317ba7..d389c1e2d34c 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -84,7 +84,7 @@ describe('app dir', () => { {}, { headers: { - __rsc__: '1', + ['RSC'.toString()]: '1', }, } ) @@ -98,7 +98,7 @@ describe('app dir', () => { {}, { headers: { - __rsc__: '1', + ['RSC'.toString()]: '1', }, } ) diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index 9463fe3afb41..3b6827ed6f2c 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -88,9 +88,6 @@ describe('app dir - rsc basics', () => { '__nextLocale', '__nextDefaultLocale', '__nextIsNotFound', - '__rsc__', - '__next_router_state_tree__', - '__next_router_prefetch__', ] const hasNextInternalQuery = inlineFlightContents.some((content) => @@ -108,9 +105,9 @@ describe('app dir - rsc basics', () => { requestsCount++ return request.allHeaders().then((headers) => { if ( - headers.__rsc__ === '1' && - // Prefetches also include `__rsc__` - headers.__next_router_prefetch__ !== '1' + headers['RSC'.toLowerCase()] === '1' && + // Prefetches also include `RSC` + headers['Next-Router-Prefetch'.toLowerCase()] !== '1' ) { hasFlightRequest = true } @@ -194,8 +191,8 @@ describe('app dir - rsc basics', () => { page.on('request', (request) => { return request.allHeaders().then((headers) => { if ( - headers.__rsc__ === '1' && - headers.__next_router_prefetch__ !== '1' + headers['RSC'.toLowerCase()] === '1' && + headers['Next-Router-Prefetch'.toLowerCase()] !== '1' ) { hasFlightRequest = true } @@ -381,7 +378,7 @@ describe('app dir - rsc basics', () => { {}, { headers: { - __rsc__: '1', + ['RSC'.toString()]: '1', }, } ).then(async (response) => { diff --git a/test/e2e/switchable-runtime/index.test.ts b/test/e2e/switchable-runtime/index.test.ts index 59504dc20b3b..07b3a0f58d5f 100644 --- a/test/e2e/switchable-runtime/index.test.ts +++ b/test/e2e/switchable-runtime/index.test.ts @@ -120,7 +120,7 @@ describe('Switchable runtime', () => { beforePageLoad(page) { page.on('request', (request) => { return request.allHeaders().then((headers) => { - if (headers.__rsc__ === '1') { + if (headers['RSC'.toLowerCase()] === '1') { flightRequest = request.url() } }) @@ -680,7 +680,7 @@ describe('Switchable runtime', () => { beforePageLoad(page) { page.on('request', (request) => { request.allHeaders().then((headers) => { - if (headers.__rsc__ === '1') { + if (headers['RSC'.toLowerCase()] === '1') { flightRequest = request.url() } }) From 6f33635f53619f9a8adcc5fa09effb7ead99a21d Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 4 Nov 2022 21:33:17 +0100 Subject: [PATCH 2/6] Fix test --- test/e2e/app-dir/rsc-basic.test.ts | 39 ++++++------------------------ 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts index 3b6827ed6f2c..00fd0ef86304 100644 --- a/test/e2e/app-dir/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic.test.ts @@ -182,41 +182,18 @@ describe('app dir - rsc basics', () => { } }) - it('should refresh correctly with next/link', async () => { + it('should link correctly with next/link without mpa navigation to the page', async () => { // Select the button which is not hidden but rendered const selector = '#goto-next-link' - let hasFlightRequest = false - const browser = await webdriver(next.url, '/root', { - beforePageLoad(page) { - page.on('request', (request) => { - return request.allHeaders().then((headers) => { - if ( - headers['RSC'.toLowerCase()] === '1' && - headers['Next-Router-Prefetch'.toLowerCase()] !== '1' - ) { - hasFlightRequest = true - } - }) - }) - }, - }) + const browser = await webdriver(next.url, '/root', {}) - // wait for hydration - await new Promise((res) => setTimeout(res, 1000)) - if (isNextDev) { - expect(hasFlightRequest).toBe(false) - } - await browser.elementByCss(selector).click() + await browser.eval('window.didNotReloadPage = true') + await browser.elementByCss(selector).click().waitForElementByCss('#query') - // wait for re-hydration - if (isNextDev) { - await check( - () => (hasFlightRequest ? 'success' : hasFlightRequest), - 'success' - ) - } - const refreshText = await browser.elementByCss(selector).text() - expect(refreshText).toBe('next link') + expect(await browser.eval('window.didNotReloadPage')).toBe(true) + + const text = await browser.elementByCss('#query').text() + expect(text).toBe('query:0') }) it('should escape streaming data correctly', async () => { From 82b6e784bb5fd009fe84a336ec4eb0793c48b3a1 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 4 Nov 2022 21:51:56 +0100 Subject: [PATCH 3/6] Fix toLowerCase --- packages/next/server/app-render.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index ceb492784a2c..d5fd2404538d 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -733,7 +733,7 @@ export async function renderToHTMLOrFlight( */ const isStaticGeneration = renderOpts.supportsDynamicHTML !== true && !renderOpts.isBot - const isFlight = req.headers[RSC.toString().toLowerCase()] !== undefined + const isFlight = req.headers[RSC.toLowerCase()] !== undefined const capturedErrors: Error[] = [] const allCapturedErrors: Error[] = [] @@ -791,7 +791,8 @@ export async function renderToHTMLOrFlight( // don't modify original query object query = Object.assign({}, query) - const isPrefetch = req.headers[NEXT_ROUTER_PREFETCH] !== undefined + const isPrefetch = + req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] !== undefined // TODO-APP: verify the tree is valid // TODO-APP: verify query param is single value (not an array) From dbb8ed82b7cc39aa2fb7caec3ddee8d75d5eb6b5 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 7 Nov 2022 20:50:06 +0100 Subject: [PATCH 4/6] Add rsc/vary header to routes manifest --- packages/next/build/index.ts | 9 +++++++++ packages/next/client/components/app-router-headers.ts | 2 ++ packages/next/server/base-server.ts | 11 ++--------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 46802a753c1a..11e560b5fb9c 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -123,6 +123,7 @@ import { RemotePattern } from '../shared/lib/image-config' import { eventSwcPlugins } from '../telemetry/events/swc-plugins' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin' +import { RSC, RSC_VARY_HEADER } from '../client/components/app-router-headers' export type SsgRoute = { initialRevalidateSeconds: number | false @@ -755,6 +756,10 @@ export default async function build( defaultLocale: string localeDetection?: false } + rsc: { + header: typeof RSC + varyHeader: typeof RSC_VARY_HEADER + } } = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => { const sortedRoutes = getSortedRoutes([ ...pageKeys.pages, @@ -781,6 +786,10 @@ export default async function build( staticRoutes, dataRoutes: [], i18n: config.i18n || undefined, + rsc: { + header: RSC, + varyHeader: RSC_VARY_HEADER, + }, } }) diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts index 4f1153e41802..1eb2747dd1cd 100644 --- a/packages/next/client/components/app-router-headers.ts +++ b/packages/next/client/components/app-router-headers.ts @@ -1,3 +1,5 @@ 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 diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 6783448d67d6..1139b72994e8 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -74,11 +74,7 @@ import { getHostname } from '../shared/lib/get-hostname' import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' import { MiddlewareMatcher } from '../build/analysis/get-page-static-info' -import { - NEXT_ROUTER_PREFETCH, - NEXT_ROUTER_STATE_TREE, - RSC, -} from '../client/components/app-router-headers' +import { RSC, RSC_VARY_HEADER } from '../client/components/app-router-headers' import { FLIGHT_PARAMETERS } from './app-render' export type FindComponentsResult = { @@ -1059,10 +1055,7 @@ export default abstract class Server { (isSSG || hasServerProps) if (isAppPath) { - res.setHeader( - 'vary', - `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` - ) + res.setHeader('vary', RSC_VARY_HEADER) if (isSSG && req.headers[RSC.toLowerCase()]) { if (!this.minimalMode) { From a8d3403613ea77a14f135c32652de18b120074e2 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 7 Nov 2022 21:57:49 +0100 Subject: [PATCH 5/6] Fix manifest test --- test/integration/dynamic-routing/test/index.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 6866e9ba7699..e2a63c3cb409 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -1426,6 +1426,10 @@ function runTests({ dev }) { }, }, ], + rsc: { + header: 'RSC', + varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch', + }, }) }) From 9a3eac8925943bb8c621ab93f83fd4831f2c7b11 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 7 Nov 2022 21:59:15 +0100 Subject: [PATCH 6/6] Update another manifest --- test/integration/custom-routes/test/index.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 5384aba34aa8..3217bd514015 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -2215,6 +2215,10 @@ const runTests = (isDev = false) => { routeKeys: {}, }, ], + rsc: { + header: 'RSC', + varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch', + }, }) })