Skip to content

Commit 0dd9483

Browse files
ztannereps1lon
authored andcommitted
fix: add explicit checks for RSC header (#83) (#98)
(cherry picked from commit 807e363b13cc9395aa74f75122d1d16b4a46dc1a)
1 parent d166096 commit 0dd9483

8 files changed

Lines changed: 65 additions & 7 deletions

File tree

packages/next/src/build/templates/app-page.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
NodeNextResponse,
3030
} from '../../server/base-http/node' with { 'turbopack-transition': 'next-server-utility' }
3131
import { checkIsAppPPREnabled } from '../../server/lib/experimental/ppr' with { 'turbopack-transition': 'next-server-utility' }
32+
import { isRSCRequestHeader } from '../../server/lib/is-rsc-request' with { 'turbopack-transition': 'next-server-utility' }
3233
import {
3334
getFallbackRouteParams,
3435
getPlaceholderFallbackRouteParams,
@@ -292,7 +293,8 @@ export async function handler(
292293
// NOTE: Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later
293294

294295
const isRSCRequest =
295-
getRequestMeta(req, 'isRSCRequest') ?? Boolean(req.headers[RSC_HEADER])
296+
getRequestMeta(req, 'isRSCRequest') ??
297+
isRSCRequestHeader(req.headers[RSC_HEADER])
296298

297299
const isPossibleServerAction = getIsPossibleServerAction(req)
298300

@@ -424,7 +426,7 @@ export async function handler(
424426
const isInstantNavigationTest =
425427
exposeTestingApi &&
426428
(req.headers[NEXT_INSTANT_PREFETCH_HEADER] === '1' ||
427-
(req.headers[RSC_HEADER] === undefined &&
429+
(!isRSCRequestHeader(req.headers[RSC_HEADER]) &&
428430
typeof req.headers.cookie === 'string' &&
429431
req.headers.cookie.includes(NEXT_INSTANT_TEST_COOKIE + '=')))
430432

packages/next/src/server/app-render/app-render.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {
7171
} from '../../client/components/app-router-headers'
7272
import { createMetadataContext } from '../../lib/metadata/metadata-context'
7373
import { createRequestStoreForRender } from '../async-storage/request-store'
74+
import { isRSCRequestHeader } from '../lib/is-rsc-request'
7475
import { createWorkStore } from '../async-storage/work-store'
7576
import {
7677
getAccessFallbackErrorTypeByStatus,
@@ -379,7 +380,7 @@ function parseRequestHeaders(
379380

380381
const isHmrRefresh = headers[NEXT_HMR_REFRESH_HEADER] !== undefined
381382

382-
const isRSCRequest = headers[RSC_HEADER] !== undefined
383+
const isRSCRequest = isRSCRequestHeader(headers[RSC_HEADER])
383384

384385
const shouldProvideFlightRouterState =
385386
isRSCRequest && (!isPrefetchRequest || !options.isRoutePPREnabled)

packages/next/src/server/base-server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import type { InstrumentationModule } from './instrumentation/types'
4848
import * as path from 'path'
4949
import { format as formatUrl } from 'url'
5050
import { formatHostname } from './lib/format-hostname'
51+
import { isRSCRequestHeader } from './lib/is-rsc-request'
5152
import {
5253
APP_PATHS_MANIFEST,
5354
NEXT_BUILTIN_DOCUMENT,
@@ -656,7 +657,7 @@ export default abstract class Server<
656657
stripFlightHeaders(req.headers)
657658

658659
return false
659-
} else if (req.headers[RSC_HEADER] === '1') {
660+
} else if (isRSCRequestHeader(req.headers[RSC_HEADER])) {
660661
addRequestMeta(req, 'isRSCRequest', true)
661662

662663
if (req.headers[NEXT_ROUTER_PREFETCH_HEADER] === '1') {
@@ -2231,7 +2232,7 @@ export default abstract class Server<
22312232
// even during a locked scope, with blocking happening on the client side.
22322233
const hasInstantTestCookie =
22332234
exposeTestingApi &&
2234-
req.headers[RSC_HEADER] === undefined &&
2235+
!isRSCRequestHeader(req.headers[RSC_HEADER]) &&
22352236
typeof req.headers.cookie === 'string' &&
22362237
req.headers.cookie.includes(NEXT_INSTANT_TEST_COOKIE + '=') &&
22372238
couldSupportPPR
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { isRSCRequestHeader } from './is-rsc-request'
2+
3+
describe('isRSCRequestHeader', () => {
4+
it('returns true for the canonical RSC header value', () => {
5+
expect(isRSCRequestHeader('1')).toBe(true)
6+
})
7+
8+
it('returns false for invalid or missing values', () => {
9+
expect(isRSCRequestHeader('0')).toBe(false)
10+
expect(isRSCRequestHeader(undefined)).toBe(false)
11+
expect(isRSCRequestHeader(null)).toBe(false)
12+
})
13+
14+
it('returns false for repeated header values', () => {
15+
expect(isRSCRequestHeader(['1'])).toBe(false)
16+
expect(isRSCRequestHeader(['1', '1'])).toBe(false)
17+
})
18+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Normalizes the raw RSC header value. Only the literal string "1" is treated
3+
* as a valid RSC request marker; malformed or repeated values are ignored.
4+
*/
5+
export function isRSCRequestHeader(
6+
value: string | string[] | null | undefined
7+
): boolean {
8+
return value === '1'
9+
}

packages/next/src/server/lib/router-utils/resolve-routes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { NextDataPathnameNormalizer } from '../../normalizers/request/next-data'
3333
import { BasePathPathnameNormalizer } from '../../normalizers/request/base-path'
3434

3535
import { addRequestMeta } from '../../request-meta'
36+
import { isRSCRequestHeader } from '../is-rsc-request'
3637
import {
3738
compileNonPath,
3839
matchHas,
@@ -871,7 +872,7 @@ export function getResolveRoutes(
871872

872873
// Set the rewrite headers only if this is a RSC request.
873874
if (
874-
req.headers[RSC_HEADER] === '1' &&
875+
isRSCRequestHeader(req.headers[RSC_HEADER]) &&
875876
(!parsedDestination.origin || isAllowedOrigin)
876877
) {
877878
// We set the rewritten path and query headers on the response now

packages/next/src/server/web/adapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { CloseController } from './web-on-close'
3535
import { getEdgePreviewProps } from './get-edge-preview-props'
3636
import { getBuiltinRequestContext } from '../after/builtin-request-context'
3737
import { getImplicitTags } from '../lib/implicit-tags'
38+
import { isRSCRequestHeader } from '../lib/is-rsc-request'
3839
import { setRequestMeta } from '../request-meta'
3940

4041
export class NextRequestHint extends NextRequest {
@@ -151,7 +152,7 @@ export async function adapter(
151152

152153
const requestHeaders = fromNodeOutgoingHttpHeaders(params.request.headers)
153154
const isNextDataRequest = requestHeaders.has('x-nextjs-data')
154-
const isRSCRequest = requestHeaders.get(RSC_HEADER) === '1'
155+
const isRSCRequest = isRSCRequestHeader(requestHeaders.get(RSC_HEADER))
155156

156157
if (isNextDataRequest && requestURL.pathname === '/index') {
157158
requestURL.pathname = '/'

test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,31 @@ describe('segment cache (CDN cache busting)', () => {
103103
}
104104
)
105105

106+
it('ignores invalid RSC header values when serving a document request', async () => {
107+
const url = new URL(`http://localhost:${port}/target-page`)
108+
url.searchParams.set('test', 'invalid-rsc-header')
109+
110+
const invalidHeaderRes = await fetch(url, {
111+
headers: {
112+
rsc: '0',
113+
},
114+
})
115+
116+
expect(invalidHeaderRes.status).toBe(200)
117+
expect(invalidHeaderRes.headers.get('content-type')).toContain('text/html')
118+
expect(await invalidHeaderRes.text()).toContain(
119+
'<div id="target-page">Target page</div>'
120+
)
121+
122+
const htmlRes = await fetch(url)
123+
124+
expect(htmlRes.status).toBe(200)
125+
expect(htmlRes.headers.get('content-type')).toContain('text/html')
126+
expect(await htmlRes.text()).toContain(
127+
'<div id="target-page">Target page</div>'
128+
)
129+
})
130+
106131
it(
107132
'perform fully prefetched navigation when a third-party proxy ' +
108133
'performs a redirect',

0 commit comments

Comments
 (0)