Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unique search query for RSC requests to be cacable on CDN #50970

Merged
merged 6 commits into from Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/next/src/client/components/app-router-headers.ts
Expand Up @@ -14,3 +14,5 @@ export const FLIGHT_PARAMETERS = [
[NEXT_ROUTER_STATE_TREE],
[NEXT_ROUTER_PREFETCH],
] as const

export const NEXT_RSC_UNION_QUERY = '_rsc' as const
3 changes: 2 additions & 1 deletion packages/next/src/client/components/app-router.tsx
Expand Up @@ -49,6 +49,7 @@ import { RedirectBoundary } from './redirect-boundary'
import { NotFoundBoundary } from './not-found-boundary'
import { findHeadInCache } from './router-reducer/reducers/find-head-in-cache'
import { createInfinitePromise } from './infinite-promise'
import { NEXT_RSC_UNION_QUERY } from './app-router-headers'

const isServer = typeof window === 'undefined'

Expand All @@ -65,7 +66,7 @@ export function getServerActionDispatcher() {

export function urlToUrlWithoutFlightMarker(url: string): URL {
const urlWithoutFlightParameters = new URL(url, location.origin)
// TODO-APP: handle .rsc for static export case
urlWithoutFlightParameters.searchParams.delete(NEXT_RSC_UNION_QUERY)
return urlWithoutFlightParameters
}

Expand Down
Expand Up @@ -10,13 +10,15 @@ import type {
import {
NEXT_ROUTER_PREFETCH,
NEXT_ROUTER_STATE_TREE,
NEXT_RSC_UNION_QUERY,
NEXT_URL,
RSC,
RSC_CONTENT_TYPE_HEADER,
} from '../app-router-headers'
import { urlToUrlWithoutFlightMarker } from '../app-router'
import { callServer } from '../../app-call-server'
import { PrefetchKind } from './router-reducer-types'
import { hexHash } from '../../../shared/lib/hash'

/**
* Fetch the flight data for the provided url. Takes in the current router state to decide what to render server-side.
Expand Down Expand Up @@ -56,18 +58,28 @@ export async function fetchServerResponse(
headers[NEXT_URL] = nextUrl
}

const uniqueCacheQuery = hexHash(
[
headers[NEXT_ROUTER_PREFETCH] || '0',
headers[NEXT_ROUTER_STATE_TREE],
].join(',')
)

try {
let fetchUrl = url
let fetchUrl = new URL(url)
if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
fetchUrl = new URL(url) // clone
if (fetchUrl.pathname.endsWith('/')) {
fetchUrl.pathname += 'index.txt'
} else {
fetchUrl.pathname += '.txt'
}
}
}

// Add unique cache query to avoid caching conflicts on CDN which don't respect to Vary header
fetchUrl.searchParams.set(NEXT_RSC_UNION_QUERY, uniqueCacheQuery)

const res = await fetch(fetchUrl, {
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
credentials: 'same-origin',
Expand Down
Expand Up @@ -8,6 +8,7 @@ import {
} from '../router-reducer-types'
import { createRecordFromThenable } from '../create-record-from-thenable'
import { prunePrefetchCache } from './prune-prefetch-cache'
import { NEXT_RSC_UNION_QUERY } from '../../app-router-headers'

export function prefetchReducer(
state: ReadonlyReducerState,
Expand All @@ -17,6 +18,8 @@ export function prefetchReducer(
prunePrefetchCache(state.prefetchCache)

const { url } = action
url.searchParams.delete(NEXT_RSC_UNION_QUERY)

const href = createHrefFromUrl(
url,
// Ensures the hash is not part of the cache key as it does not affect fetching the server
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/base-server.ts
Expand Up @@ -81,6 +81,7 @@ import {
RSC,
RSC_VARY_HEADER,
FLIGHT_PARAMETERS,
NEXT_RSC_UNION_QUERY,
} from '../client/components/app-router-headers'
import {
MatchOptions,
Expand Down Expand Up @@ -2206,6 +2207,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const { res, query, pathname } = ctx
let page = pathname
const bubbleNoFallback = !!query._nextBubbleNoFallback
delete query[NEXT_RSC_UNION_QUERY]
delete query._nextBubbleNoFallback

const options: MatchOptions = {
Expand Down
24 changes: 14 additions & 10 deletions packages/next/src/server/internal-utils.ts
@@ -1,3 +1,4 @@
import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers'
import type { NextParsedUrlQuery } from './request-meta'

const INTERNAL_QUERY_NAMES = [
Expand All @@ -6,29 +7,32 @@ const INTERNAL_QUERY_NAMES = [
'__nextInferredLocaleFromDefault',
'__nextDefaultLocale',
'__nextIsNotFound',
NEXT_RSC_UNION_QUERY,
] as const

const EXTENDED_INTERNAL_QUERY_NAMES = ['__nextDataReq'] as const
const EDGE_EXTENDED_INTERNAL_QUERY_NAMES = ['__nextDataReq'] as const

export function stripInternalQueries(query: NextParsedUrlQuery) {
for (const name of INTERNAL_QUERY_NAMES) {
delete query[name]
}
}

export function stripInternalSearchParams(
searchParams: URLSearchParams,
extended?: boolean
) {
export function stripInternalSearchParams<T extends string | URL>(
url: T,
isEdge: boolean
): T {
const isStringUrl = typeof url === 'string'
const instance = isStringUrl ? new URL(url) : (url as URL)
for (const name of INTERNAL_QUERY_NAMES) {
searchParams.delete(name)
instance.searchParams.delete(name)
}

if (extended) {
for (const name of EXTENDED_INTERNAL_QUERY_NAMES) {
searchParams.delete(name)
if (isEdge) {
for (const name of EDGE_EXTENDED_INTERNAL_QUERY_NAMES) {
instance.searchParams.delete(name)
}
}

return searchParams
return (isStringUrl ? instance.toString() : instance) as T
}
2 changes: 2 additions & 0 deletions packages/next/src/server/next-server.ts
Expand Up @@ -110,6 +110,7 @@ import { invokeRequest } from './lib/server-ipc/invoke-request'
import { filterReqHeaders } from './lib/server-ipc/utils'
import { createRequestResponseMocks } from './lib/mock-request'
import chalk from 'next/dist/compiled/chalk'
import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers'

export * from './base-server'

Expand Down Expand Up @@ -1560,6 +1561,7 @@ export default class NextNodeServer extends BaseServer {
return { finished: true }
}
delete query._nextBubbleNoFallback
delete query[NEXT_RSC_UNION_QUERY]

const handledAsEdgeFunction = await this.runEdgeFunction({
req,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/request-meta.ts
Expand Up @@ -5,6 +5,7 @@ import type { UrlWithParsedQuery } from 'url'
import type { BaseNextRequest } from './base-http'
import type { CloneableBody } from './body-streams'
import { RouteMatch } from './future/route-matches/route-match'
import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers'

// FIXME: (wyattjoh) this is a temporary solution to allow us to pass data between bundled modules
export const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta')
Expand Down Expand Up @@ -97,6 +98,7 @@ type NextQueryMetadata = {
_nextBubbleNoFallback?: '1'
__nextDataReq?: '1'
__nextCustomErrorRender?: '1'
[NEXT_RSC_UNION_QUERY]?: string
}

export type NextParsedUrlQuery = ParsedUrlQuery &
Expand Down
10 changes: 5 additions & 5 deletions packages/next/src/server/web/adapter.ts
Expand Up @@ -117,14 +117,14 @@ export async function adapter(
}
}

// Strip internal query parameters off the request.
stripInternalSearchParams(requestUrl.searchParams, true)
const normalizeUrl = process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE
? new URL(params.request.url)
: requestUrl

const request = new NextRequestHint({
page: params.page,
input: process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE
? params.request.url
: String(requestUrl),
// Strip internal query parameters off the request.
input: stripInternalSearchParams(normalizeUrl, true).toString(),
init: {
body: params.request.body,
geo: params.request.geo,
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/server/web/sandbox/sandbox.ts
Expand Up @@ -4,6 +4,7 @@ import { getModuleContext } from './context'
import { EdgeFunctionDefinition } from '../../../build/webpack/plugins/middleware-plugin'
import { requestToBodyStream } from '../../body-streams'
import type { EdgeRuntime } from 'next/dist/compiled/edge-runtime'
import { NEXT_RSC_UNION_QUERY } from '../../../client/components/app-router-headers'

export const ErrorSource = Symbol('SandboxError')

Expand Down Expand Up @@ -96,6 +97,10 @@ export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
: undefined

const KUint8Array = runtime.evaluate('Uint8Array')
const urlInstance = new URL(params.request.url)
urlInstance.searchParams.delete(NEXT_RSC_UNION_QUERY)

params.request.url = urlInstance.toString()

try {
const result = await edgeFunction({
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/shared/lib/hash.ts
Expand Up @@ -7,3 +7,7 @@ export function djb2Hash(str: string) {
}
return Math.abs(hash)
}

export function hexHash(str: string) {
return djb2Hash(str).toString(16).slice(0, 7)
}
29 changes: 25 additions & 4 deletions test/e2e/app-dir/app-prefetch/prefetching.test.ts
@@ -1,6 +1,9 @@
import { createNextDescribe } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'

// @ts-ignore
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'

const browserConfigWithFixedTime = {
beforePageLoad: (page) => {
page.addInitScript(() => {
Expand Down Expand Up @@ -38,6 +41,10 @@ createNextDescribe(
return
}

it('NEXT_RSC_UNION_QUERY query name is _rsc', async () => {
expect(NEXT_RSC_UNION_QUERY).toBe('_rsc')
})

it('should show layout eagerly when prefetched with loading one level down', async () => {
const browser = await next.browser('/', browserConfigWithFixedTime)
// Ensure the page is prefetched
Expand Down Expand Up @@ -85,8 +92,12 @@ createNextDescribe(
await browser.eval(
'window.nd.router.prefetch("/static-page", {kind: "auto"})'
)

await check(() => {
return requests.some((req) => req.includes('static-page'))
return requests.some(
(req) =>
req.includes('static-page') && !req.includes(NEXT_RSC_UNION_QUERY)
)
? 'success'
: JSON.stringify(requests)
}, 'success')
Expand Down Expand Up @@ -114,7 +125,10 @@ createNextDescribe(
`window.nd.router.prefetch("/static-page", {kind: "auto"})`
)
await check(() => {
return requests.some((req) => req.includes('static-page'))
return requests.some(
(req) =>
req.includes('static-page') && !req.includes(NEXT_RSC_UNION_QUERY)
)
? 'success'
: JSON.stringify(requests)
}, 'success')
Expand All @@ -136,7 +150,10 @@ createNextDescribe(
.waitForElementByCss('#static-page')

expect(
requests.filter((request) => request === '/static-page').length
requests.filter(
(request) =>
request === '/static-page' || request.includes(NEXT_RSC_UNION_QUERY)
).length
).toBe(1)
})

Expand All @@ -159,7 +176,11 @@ createNextDescribe(
for (let i = 0; i < 5; i++) {
await waitFor(500)
expect(
requests.filter((request) => request === '/static-page').length
requests.filter(
(request) =>
request === '/static-page' ||
request.includes(NEXT_RSC_UNION_QUERY)
).length
).toBe(0)
}
})
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/app-dir/navigation/app/assertion/page/page.js
@@ -0,0 +1,9 @@
import { strict as assert } from 'node:assert'
// @ts-ignore
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'

export default function Page({ searchParams }) {
assert(searchParams[NEXT_RSC_UNION_QUERY] === undefined)

return <p>no rsc query page</p>
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/navigation/app/assertion/route/route.js
@@ -0,0 +1,8 @@
import { strict as assert } from 'node:assert'
// @ts-ignore
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'

export function GET(request) {
assert(request.nextUrl.searchParams.get(NEXT_RSC_UNION_QUERY) === null)
return new Response('no rsc query route')
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/navigation/middleware.js
@@ -1,11 +1,24 @@
// @ts-check
import { NextResponse } from 'next/server'
// @ts-ignore
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'

if (NEXT_RSC_UNION_QUERY !== '_rsc') {
throw new Error(`NEXT_RSC_UNION_QUERY should be _rsc`)
}

/**
* @param {import('next/server').NextRequest} request
* @returns {NextResponse | undefined}
*/
export function middleware(request) {
const rscQuery = request.nextUrl.searchParams.get(NEXT_RSC_UNION_QUERY)

// Test that the RSC query is not present in the middleware
if (rscQuery) {
throw new Error('RSC query should not be present in the middleware')
}

if (request.nextUrl.pathname === '/redirect-middleware-to-dashboard') {
return NextResponse.redirect(new URL('/redirect-dest', request.url))
}
Expand Down