diff --git a/packages/next/src/client/components/router-reducer/set-cache-busting-search-param.ts b/packages/next/src/client/components/router-reducer/set-cache-busting-search-param.ts index a7db3f204af41..a57b9c1b183f7 100644 --- a/packages/next/src/client/components/router-reducer/set-cache-busting-search-param.ts +++ b/packages/next/src/client/components/router-reducer/set-cache-busting-search-param.ts @@ -36,12 +36,29 @@ export const setCacheBustingSearchParam = ( headers[NEXT_ROUTER_STATE_TREE_HEADER], headers[NEXT_URL] ) - if (uniqueCacheKey === null) { - // None of our custom request headers are present. We don't need to set a - // cache-busting search param. - return - } + setCacheBustingSearchParamWithHash(url, uniqueCacheKey) +} +/** + * Sets a cache-busting search parameter on a URL using a provided hash value. + * + * This function performs the same logic as `setCacheBustingSearchParam` but accepts + * a pre-computed hash instead of computing it from headers. + * + * Example: + * URL before: https://example.com/path?query=1 + * hash: "abc123" + * URL after: https://example.com/path?query=1&_rsc=abc123 + * + * If the hash is null, we will set `_rsc` search param without a value. + * Like this: https://example.com/path?query=1&_rsc + * + * Note: This function mutates the input URL directly and does not return anything. + */ +export const setCacheBustingSearchParamWithHash = ( + url: URL, + hash: string +): void => { /** * Note that we intentionally do not use `url.searchParams.set` here: * @@ -64,6 +81,10 @@ export const setCacheBustingSearchParam = ( .split('&') .filter((pair) => pair && !pair.startsWith(`${NEXT_RSC_UNION_QUERY}=`)) - pairs.push(`${NEXT_RSC_UNION_QUERY}=${uniqueCacheKey}`) + if (hash.length > 0) { + pairs.push(`${NEXT_RSC_UNION_QUERY}=${hash}`) + } else { + pairs.push(`${NEXT_RSC_UNION_QUERY}`) + } url.search = pairs.length ? `?${pairs.join('&')}` : '' } diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index ecc16b7c1b8ed..b8c3079d3b381 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -11,6 +11,9 @@ import { NEXT_ROUTER_STATE_TREE_HEADER, ACTION_HEADER, NEXT_ACTION_NOT_FOUND_HEADER, + NEXT_ROUTER_PREFETCH_HEADER, + NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, + NEXT_URL, } from '../../client/components/app-router-headers' import { getAccessFallbackHTTPStatus, @@ -54,6 +57,7 @@ import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.exte import { InvariantError } from '../../shared/lib/invariant-error' import { executeRevalidates } from '../revalidation-utils' import { getRequestMeta } from '../request-meta' +import { setCacheBustingSearchParam } from '../../client/components/router-reducer/set-cache-busting-search-param' function formDataFromSearchQueryString(query: string) { const searchParams = new URLSearchParams(query) @@ -339,6 +343,20 @@ async function createRedirectRenderResult( forwardedHeaders.delete(ACTION_HEADER) try { + setCacheBustingSearchParam(fetchUrl, { + [NEXT_ROUTER_PREFETCH_HEADER]: forwardedHeaders.get( + NEXT_ROUTER_PREFETCH_HEADER + ) + ? ('1' as const) + : undefined, + [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: + forwardedHeaders.get(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER) ?? + undefined, + [NEXT_ROUTER_STATE_TREE_HEADER]: + forwardedHeaders.get(NEXT_ROUTER_STATE_TREE_HEADER) ?? undefined, + [NEXT_URL]: forwardedHeaders.get(NEXT_URL) ?? undefined, + }) + const response = await fetch(fetchUrl, { method: 'GET', headers: forwardedHeaders, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 821179b23c0d8..5611478f0caef 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -171,6 +171,7 @@ import { getCacheHandlers } from './use-cache/handlers' import { fixMojibake } from './lib/fix-mojibake' import { computeCacheBustingSearchParam } from '../shared/lib/router/utils/cache-busting-search-param' import { RedirectStatusCode } from '../client/components/redirect-status-code' +import { setCacheBustingSearchParamWithHash } from '../client/components/router-reducer/set-cache-busting-search-param' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -2055,6 +2056,8 @@ export default abstract class Server< const isPossibleServerAction = getIsPossibleServerAction(req) const hasGetInitialProps = !!components.Component?.getInitialProps let isSSG = !!components.getStaticProps + // NOTE: Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later + const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false // Not all CDNs respect the Vary header when caching. We must assume that // only the URL is used to vary the responses. The Next client computes a @@ -2062,20 +2065,10 @@ export default abstract class Server< // responding to a request, we must verify that the hash matches the // expected value. Neglecting to do this properly can lead to cache // poisoning attacks on certain CDNs. - // TODO: This is verification only runs during per-segment prefetch - // requests, since those are the only ones that both vary on a custom - // header and are cacheable. But for safety, we should run this - // verification for all requests, once we confirm the behavior is correct. - // Will need to update our test suite, since there are a handlful of unit - // tests that send fetch requests with custom headers but without a - // corresponding cache-busting search param. - // TODO: Consider not using custom request headers at all, and instead fully - // encode everything into the search param. if ( !this.minimalMode && this.nextConfig.experimental.validateRSCRequestHeaders && - this.isAppSegmentPrefetchEnabled && - getRequestMeta(req, 'segmentPrefetchRSCRequest') + isRSCRequest ) { const headers = req.headers const expectedHash = computeCacheBustingSearchParam( @@ -2084,12 +2077,22 @@ export default abstract class Server< headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()], headers[NEXT_URL.toLowerCase()] ) - const actualHash = getRequestMeta(req, 'cacheBustingSearchParam') ?? null + const actualHash = + getRequestMeta(req, 'cacheBustingSearchParam') ?? + new URL(req.url || '', 'http://localhost').searchParams.get( + NEXT_RSC_UNION_QUERY + ) + if (expectedHash !== actualHash) { // The hash sent by the client does not match the expected value. - // Respond with an error. - res.statusCode = 400 - res.setHeader('content-type', 'text/plain') + // Redirect to the URL with the correct cache-busting search param. + // This prevents cache poisoning attacks on CDNs that don't respect Vary headers. + // Note: When no headers are present, expectedHash is empty string and client + // must send `_rsc` param, otherwise actualHash is null and hash check fails. + const url = new URL(req.url || '', 'http://localhost') + setCacheBustingSearchParamWithHash(url, expectedHash) + res.statusCode = 307 + res.setHeader('location', `${url.pathname}${url.search}`) res.body('').send() return null } @@ -2173,10 +2176,6 @@ export default abstract class Server< const isPrefetchRSCRequest = getRequestMeta(req, 'isPrefetchRSCRequest') ?? false - // NOTE: Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later - - const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false - // when we are handling a middleware prefetch and it doesn't // resolve to a static data route we bail early to avoid // unexpected SSR invocations diff --git a/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts b/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts index f827c6c438b23..a9cec3204ee3f 100644 --- a/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts +++ b/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts @@ -5,14 +5,14 @@ export function computeCacheBustingSearchParam( segmentPrefetchHeader: string | string[] | undefined, stateTreeHeader: string | string[] | undefined, nextUrlHeader: string | string[] | undefined -): string | null { +): string { if ( prefetchHeader === undefined && segmentPrefetchHeader === undefined && stateTreeHeader === undefined && nextUrlHeader === undefined ) { - return null + return '' } return hexHash( [ diff --git a/test/e2e/app-dir/app-inline-css/index.test.ts b/test/e2e/app-dir/app-inline-css/index.test.ts index e300fad138737..78523e24454d8 100644 --- a/test/e2e/app-dir/app-inline-css/index.test.ts +++ b/test/e2e/app-dir/app-inline-css/index.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers' describe('app dir - css - experimental inline css', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, @@ -17,7 +18,7 @@ describe('app dir - css - experimental inline css', () => { it('should not return rsc payload with inlined style as a dynamic client nav', async () => { const rscPayload = await ( - await next.fetch('/a', { + await next.fetch(`/a?${NEXT_RSC_UNION_QUERY}`, { method: 'GET', headers: { rsc: '1', @@ -32,7 +33,7 @@ describe('app dir - css - experimental inline css', () => { expect( await ( - await next.fetch('/a', { + await next.fetch(`/a?${NEXT_RSC_UNION_QUERY}`, { method: 'GET', }) ).text() diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index d06513d5c705f..b711add3041d1 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -1,6 +1,7 @@ import { nextTestSetup } from 'e2e-utils' import { check, waitFor, retry } from 'next-test-utils' import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers' +import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' const browserConfigWithFixedTime = { beforePageLoad: (page) => { @@ -294,14 +295,26 @@ describe('app dir - prefetching', () => { true, ]) ) - const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, { - headers: { - RSC: '1', - 'Next-Router-Prefetch': '1', - 'Next-Router-State-Tree': stateTree, - 'Next-Url': '/prefetch-auto/vercel', - }, - }) + + const headers = { + RSC: '1', + 'Next-Router-Prefetch': '1', + 'Next-Router-State-Tree': stateTree, + 'Next-Url': '/prefetch-auto/vercel', + } + + const url = new URL('/prefetch-auto/justputit', 'http://localhost') + const cacheBustingParam = computeCacheBustingSearchParam( + headers['Next-Router-Prefetch'], + undefined, + headers['Next-Router-State-Tree'], + headers['Next-Url'] + ) + if (cacheBustingParam) { + url.searchParams.set('_rsc', cacheBustingParam) + } + + const response = await next.fetch(url.toString(), { headers }) const prefetchResponse = await response.text() expect(prefetchResponse).not.toContain('Page Data!') diff --git a/test/e2e/app-dir/app-validation/validation.test.ts b/test/e2e/app-dir/app-validation/validation.test.ts index 49bf85e2878d6..ffa19fe2aa2c5 100644 --- a/test/e2e/app-dir/app-validation/validation.test.ts +++ b/test/e2e/app-dir/app-validation/validation.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' describe('app dir - validation', () => { const { next, skipped } = nextTestSetup({ @@ -11,20 +12,47 @@ describe('app dir - validation', () => { } it('should error when passing invalid router state tree', async () => { - const res = await next.fetch('/', { - headers: { - RSC: '1', - 'Next-Router-State-Tree': JSON.stringify(['', '']), - }, - }) + const stateTree1 = JSON.stringify(['', '']) + const stateTree2 = JSON.stringify(['', {}]) + + const headers1 = { + RSC: '1', + 'Next-Router-State-Tree': stateTree1, + } + + const headers2 = { + RSC: '1', + 'Next-Router-State-Tree': stateTree2, + } + + const url1 = new URL('/', 'http://localhost') + const url2 = new URL('/', 'http://localhost') + + // Add cache busting search param for both requests + const cacheBustingParam1 = computeCacheBustingSearchParam( + undefined, + undefined, + stateTree1, + undefined + ) + const cacheBustingParam2 = computeCacheBustingSearchParam( + undefined, + undefined, + stateTree2, + undefined + ) + + if (cacheBustingParam1) { + url1.searchParams.set('_rsc', cacheBustingParam1) + } + if (cacheBustingParam2) { + url2.searchParams.set('_rsc', cacheBustingParam2) + } + + const res = await next.fetch(url1.toString(), { headers: headers1 }) expect(res.status).toBe(500) - const res2 = await next.fetch('/', { - headers: { - RSC: '1', - 'Next-Router-State-Tree': JSON.stringify(['', {}]), - }, - }) + const res2 = await next.fetch(url2.toString(), { headers: headers2 }) expect(res2.status).toBe(200) }) }) diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 8a0734c926f20..f804db8ba60b8 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -2,6 +2,10 @@ import { nextTestSetup } from 'e2e-utils' import { check, retry, waitFor } from 'next-test-utils' import cheerio from 'cheerio' import stripAnsi from 'strip-ansi' +import { + NEXT_RSC_UNION_QUERY, + RSC_HEADER, +} from 'next/dist/client/components/app-router-headers' // TODO: We should decide on an established pattern for gating test assertions // on experimental flags. For example, as a first step we could all the common @@ -306,18 +310,21 @@ describe('app dir - basic', () => { } it('should use text/x-component for flight', async () => { - const res = await next.fetch('/dashboard/deployments/123', { - headers: { - ['RSC'.toString()]: '1', - }, - }) + const res = await next.fetch( + `/dashboard/deployments/123?${NEXT_RSC_UNION_QUERY}`, + { + headers: { + [RSC_HEADER]: '1', + }, + } + ) expect(res.headers.get('Content-Type')).toBe('text/x-component') }) it('should use text/x-component for flight with edge runtime', async () => { - const res = await next.fetch('/dashboard', { + const res = await next.fetch(`/dashboard?${NEXT_RSC_UNION_QUERY}`, { headers: { - ['RSC'.toString()]: '1', + [RSC_HEADER]: '1', }, }) expect(res.headers.get('Content-Type')).toBe('text/x-component') @@ -332,9 +339,9 @@ describe('app dir - basic', () => { }) it('should return the `vary` header from pages for flight requests', async () => { - const res = await next.fetch('/', { + const res = await next.fetch(`/?${NEXT_RSC_UNION_QUERY}`, { headers: { - ['RSC'.toString()]: '1', + [RSC_HEADER]: '1', }, }) expect(res.headers.get('vary')).toBe( diff --git a/test/e2e/app-dir/ppr-full/ppr-full.test.ts b/test/e2e/app-dir/ppr-full/ppr-full.test.ts index 86e4a770c28f2..f02d4b721047f 100644 --- a/test/e2e/app-dir/ppr-full/ppr-full.test.ts +++ b/test/e2e/app-dir/ppr-full/ppr-full.test.ts @@ -3,6 +3,7 @@ import { measurePPRTimings } from 'e2e-utils/ppr' import { links } from './components/links' import cheerio from 'cheerio' import { retry } from 'next-test-utils' +import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' type Page = { pathname: string @@ -46,6 +47,26 @@ const pages: Page[] = [ }, ] +const addCacheBustingSearchParam = ( + pathname: string, + headers: Record +) => { + const cacheKey = computeCacheBustingSearchParam( + headers['Next-Router-Prefetch'], + headers['Next-Router-Segment-Prefetch'], + headers['Next-Router-State-Tree'], + headers['Next-URL'] + ) + + if (cacheKey === null) { + return pathname + } + + const url = new URL(pathname, 'http://localhost') + url.searchParams.set('_rsc', cacheKey) + return url.pathname + url.search +} + describe('ppr-full', () => { const { next, isNextDev, isNextDeploy } = nextTestSetup({ files: __dirname, @@ -536,8 +557,17 @@ describe('ppr-full', () => { describe.each(pages)('for $pathname', ({ pathname, revalidate }) => { it('should have correct headers', async () => { await retry(async () => { - const res = await next.fetch(pathname, { - headers: { RSC: '1', 'Next-Router-Prefetch': '1' }, + const headers = { + RSC: '1', + 'Next-Router-Prefetch': '1', + } + const urlWithCacheBusting = addCacheBustingSearchParam( + pathname, + headers + ) + + const res = await next.fetch(urlWithCacheBusting, { + headers, }) expect(res.status).toEqual(200) @@ -565,12 +595,18 @@ describe('ppr-full', () => { it('should not contain dynamic content', async () => { const unexpected = `${Date.now()}:${Math.random()}` - const res = await next.fetch(pathname, { - headers: { - RSC: '1', - 'Next-Router-Prefetch': '1', - 'X-Test-Input': unexpected, - }, + const headers = { + RSC: '1', + 'Next-Router-Prefetch': '1', + 'X-Test-Input': unexpected, + } + const urlWithCacheBusting = addCacheBustingSearchParam( + pathname, + headers + ) + + const res = await next.fetch(urlWithCacheBusting, { + headers, }) expect(res.status).toEqual(200) expect(res.headers.get('content-type')).toEqual('text/x-component') @@ -583,8 +619,14 @@ describe('ppr-full', () => { describe('Dynamic RSC Response', () => { describe.each(pages)('for $pathname', ({ pathname, dynamic }) => { it('should have correct headers', async () => { - const res = await next.fetch(pathname, { - headers: { RSC: '1' }, + const headers = { RSC: '1' } + const urlWithCacheBusting = addCacheBustingSearchParam( + pathname, + headers + ) + + const res = await next.fetch(urlWithCacheBusting, { + headers, }) expect(res.status).toEqual(200) expect(res.headers.get('content-type')).toEqual('text/x-component') @@ -601,8 +643,17 @@ describe('ppr-full', () => { if (dynamic === true || dynamic === 'force-dynamic') { it('should contain dynamic content', async () => { const expected = `${Date.now()}:${Math.random()}` - const res = await next.fetch(pathname, { - headers: { RSC: '1', 'X-Test-Input': expected }, + const headers = { + RSC: '1', + 'X-Test-Input': expected, + } + const urlWithCacheBusting = addCacheBustingSearchParam( + pathname, + headers + ) + + const res = await next.fetch(urlWithCacheBusting, { + headers, }) expect(res.status).toEqual(200) expect(res.headers.get('content-type')).toEqual('text/x-component') @@ -612,11 +663,17 @@ describe('ppr-full', () => { } else { it('should not contain dynamic content', async () => { const unexpected = `${Date.now()}:${Math.random()}` - const res = await next.fetch(pathname, { - headers: { - RSC: '1', - 'X-Test-Input': unexpected, - }, + const headers = { + RSC: '1', + 'X-Test-Input': unexpected, + } + const urlWithCacheBusting = addCacheBustingSearchParam( + pathname, + headers + ) + + const res = await next.fetch(urlWithCacheBusting, { + headers, }) expect(res.status).toEqual(200) expect(res.headers.get('content-type')).toEqual('text/x-component') diff --git a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts index d344095e78ef3..3384d52ece8a0 100644 --- a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts +++ b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' const targets = ['x-nextjs-rewritten-path', 'x-nextjs-rewritten-query'] as const @@ -381,7 +382,29 @@ describe('rewrite-headers', () => { ({ pathname, headers = {}, expected }) => { let response beforeAll(async () => { - response = await next.fetch(pathname, { headers }) + const url = new URL(pathname, 'http://localhost') + + // Add cache busting param for RSC requests + if (headers.RSC === '1') { + const cacheBustingParam = computeCacheBustingSearchParam( + headers['Next-Router-Prefetch'], + undefined, + headers['Next-Router-State-Tree'], + undefined + ) + if (cacheBustingParam) { + // Preserve existing search params if any + const existingSearch = url.search + const rawQuery = existingSearch.startsWith('?') + ? existingSearch.slice(1) + : existingSearch + const pairs = rawQuery.split('&').filter(Boolean) + pairs.push(`_rsc=${cacheBustingParam}`) + url.search = pairs.length ? `?${pairs.join('&')}` : '' + } + } + + response = await next.fetch(url.toString(), { headers }) if (response.status !== 200) { throw new Error( `Expected status 200, got ${response.status} for ${pathname}` diff --git a/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts index 9dee341b4f458..6c69c93289769 100644 --- a/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts @@ -2,6 +2,10 @@ import path from 'path' import { check } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' import cheerio from 'cheerio' +import { + NEXT_RSC_UNION_QUERY, + RSC_HEADER, +} from 'next/dist/client/components/app-router-headers' // TODO: We should decide on an established pattern for gating test assertions // on experimental flags. For example, as a first step we could all the common @@ -380,8 +384,10 @@ describe('app dir - rsc basics', () => { it('should support streaming for flight response', async () => { await next - .fetch('/', { - headers: { RSC: '1' }, + .fetch(`/?${NEXT_RSC_UNION_QUERY}`, { + headers: { + [RSC_HEADER]: '1', + }, }) .then(async (response) => { const result = await resolveStreamResponse(response) diff --git a/test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts b/test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts index fd6f065421fa3..4387f1e48801a 100644 --- a/test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts +++ b/test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { fetchViaHTTP } from 'next-test-utils' +import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers' describe('rsc-redirect', () => { const { next } = nextTestSetup({ @@ -14,13 +15,17 @@ describe('rsc-redirect', () => { }) it('should get 200 status code for rsc request', async () => { - // TODO: add RSC cache busting query param - const response = await fetchViaHTTP(next.url, '/origin', undefined, { - redirect: 'manual', - headers: { - RSC: '1', - }, - }) + const response = await fetchViaHTTP( + next.url, + `/origin?${NEXT_RSC_UNION_QUERY}`, + undefined, + { + redirect: 'manual', + headers: { + RSC: '1', + }, + } + ) expect(response.status).toBe(200) }) }) diff --git a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts index f72dfebdef726..f7d45ff89326f 100644 --- a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts +++ b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts @@ -74,23 +74,30 @@ describe('segment cache (CDN cache busting)', () => { ) it( - 'prevent cache poisoning attacks by responding with an error if a custom ' + - 'header is sent during a prefetch without a corresponding cache-busting ' + - 'search param', + 'prevent cache poisoning attacks by responding with a redirect to correct ' + + 'cache busting query param if a custom header is sent during a prefetch ' + + 'without a corresponding cache-busting search param', async () => { const browser = await webdriver(port, '/') - const { status } = await browser.eval(async () => { - const res = await fetch('/target-page', { - headers: { - RSC: '1', - 'Next-Router-Prefetch': '1', - 'Next-Router-Segment-Prefetch': '/_tree', - }, - }) - return { status: res.status, text: await res.text() } - }) - - expect(status).toBe(400) + const { status, responseUrl, redirected } = await browser.eval( + async () => { + const res = await fetch('/target-page', { + headers: { + RSC: '1', + 'Next-Router-Prefetch': '1', + 'Next-Router-Segment-Prefetch': '/_tree', + }, + }) + return { + status: res.status, + responseUrl: res.url, + redirected: res.redirected, + } + } + ) + expect(status).toBe(200) + expect(responseUrl).toContain('_rsc=') + expect(redirected).toBe(true) } ) diff --git a/test/e2e/app-dir/segment-cache/conflicting-routes/conflicting-routes.test.ts b/test/e2e/app-dir/segment-cache/conflicting-routes/conflicting-routes.test.ts index fb02abf42d0ab..470a6aa209ddf 100644 --- a/test/e2e/app-dir/segment-cache/conflicting-routes/conflicting-routes.test.ts +++ b/test/e2e/app-dir/segment-cache/conflicting-routes/conflicting-routes.test.ts @@ -1,6 +1,5 @@ import { nextTestSetup } from 'e2e-utils' - -import { computeCacheBustingSearchParam } from '../../../../../packages/next/src/shared/lib/router/utils/cache-busting-search-param' +import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' describe('conflicting routes', () => { const { next, isNextDev, isNextDeploy } = nextTestSetup({ diff --git a/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts b/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts index ef65c33811cc9..a71a15b3d88ca 100644 --- a/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts +++ b/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' +import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers' import { SavedSpan } from './constants' import { type Collector, connectCollector } from './collector' @@ -358,7 +359,7 @@ describe('opentelemetry', () => { }) it('should handle RSC with fetch in RSC mode', async () => { - await next.fetch('/app/param/rsc-fetch', { + await next.fetch(`/app/param/rsc-fetch?${NEXT_RSC_UNION_QUERY}`, { ...env.fetchInit, headers: { ...env.fetchInit?.headers, @@ -376,7 +377,7 @@ describe('opentelemetry', () => { 'http.method': 'GET', 'http.route': '/app/[param]/rsc-fetch', 'http.status_code': 200, - 'http.target': '/app/param/rsc-fetch', + 'http.target': `/app/param/rsc-fetch?${NEXT_RSC_UNION_QUERY}`, 'next.route': '/app/[param]/rsc-fetch', 'next.rsc': true, 'next.span_name': 'RSC GET /app/[param]/rsc-fetch',