Skip to content

Commit 316c062

Browse files
authored
[dev] Define request ID for RSC requests on the client (#84605)
Previously, we defined a request ID for RSC requests on the server. The request ID is used to tag debug information sent via WebSocket to the client, which then routes those chunks to the debug channel associated with this ID. However, this meant that the client had to await the server response to retrieve the request ID from the response headers, before we could pass the response (and the debug channel) to React. For #84580, we want to pass the response promise directly to React, to accurately show timing information about the request in the React DevTools. To achieve this, we now define the request ID on the client, and send it to the server via a request header. This means that the client can create the debug channel immediately, without waiting for the server response. closes NAR-426
1 parent b5eba8f commit 316c062

File tree

6 files changed

+88
-38
lines changed

6 files changed

+88
-38
lines changed

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,5 +851,6 @@
851851
"850": "metadataBase is not a valid URL: %s",
852852
"851": "Pass either `webpack` or `turbopack`, not both.",
853853
"852": "Only custom servers can pass `webpack`, `turbo`, or `turbopack`.",
854-
"853": "Turbopack build failed"
854+
"853": "Turbopack build failed",
855+
"854": "Expected a %s request header."
855856
}

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
NEXT_DID_POSTPONE_HEADER,
2222
NEXT_ROUTER_STALE_TIME_HEADER,
2323
NEXT_HTML_REQUEST_ID_HEADER,
24+
NEXT_REQUEST_ID_HEADER,
2425
} from '../app-router-headers'
2526
import { callServer } from '../../app-call-server'
2627
import { findSourceMapURL } from '../../app-find-source-map-url'
@@ -77,6 +78,7 @@ export type RequestHeaders = {
7778
// A header that is only added in test mode to assert on fetch priority
7879
'Next-Test-Fetch-Priority'?: RequestInit['priority']
7980
[NEXT_HTML_REQUEST_ID_HEADER]?: string // dev-only
81+
[NEXT_REQUEST_ID_HEADER]?: string // dev-only
8082
}
8183

8284
function doMpaNavigation(url: string): FetchServerResponseResult {
@@ -230,7 +232,7 @@ export async function fetchServerResponse(
230232
: res.body
231233
const response = await (createFromNextReadableStream(
232234
flightStream,
233-
res.headers
235+
headers
234236
) as Promise<NavigationFlightResponse>)
235237

236238
if (getAppBuildId() !== response.b) {
@@ -299,8 +301,17 @@ export async function createFetch(
299301
headers['x-deployment-id'] = process.env.NEXT_DEPLOYMENT_ID
300302
}
301303

302-
if (process.env.NODE_ENV !== 'production' && self.__next_r) {
303-
headers[NEXT_HTML_REQUEST_ID_HEADER] = self.__next_r
304+
if (process.env.NODE_ENV !== 'production') {
305+
if (self.__next_r) {
306+
headers[NEXT_HTML_REQUEST_ID_HEADER] = self.__next_r
307+
}
308+
309+
// Create a new request ID for the server action request. The server uses
310+
// this to tag debug information sent via WebSocket to the client, which
311+
// then routes those chunks to the debug channel associated with this ID.
312+
headers[NEXT_REQUEST_ID_HEADER] = crypto
313+
.getRandomValues(new Uint32Array(1))[0]
314+
.toString(16)
304315
}
305316

306317
const fetchOptions: RequestInit = {
@@ -404,12 +415,12 @@ export async function createFetch(
404415

405416
export function createFromNextReadableStream(
406417
flightStream: ReadableStream<Uint8Array>,
407-
responseHeaders: Headers
418+
requestHeaders: RequestHeaders
408419
): Promise<unknown> {
409420
return createFromReadableStream(flightStream, {
410421
callServer,
411422
findSourceMapURL,
412-
debugChannel: createDebugChannel && createDebugChannel(responseHeaders),
423+
debugChannel: createDebugChannel && createDebugChannel(requestHeaders),
413424
})
414425
}
415426

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
NEXT_ROUTER_STATE_TREE_HEADER,
1313
NEXT_URL,
1414
RSC_CONTENT_TYPE_HEADER,
15+
NEXT_REQUEST_ID_HEADER,
1516
} from '../../app-router-headers'
1617
import { UnrecognizedActionError } from '../../unrecognized-action-error'
1718

@@ -119,8 +120,17 @@ async function fetchServerAction(
119120
headers[NEXT_URL] = nextUrl
120121
}
121122

122-
if (process.env.NODE_ENV !== 'production' && self.__next_r) {
123-
headers[NEXT_HTML_REQUEST_ID_HEADER] = self.__next_r
123+
if (process.env.NODE_ENV !== 'production') {
124+
if (self.__next_r) {
125+
headers[NEXT_HTML_REQUEST_ID_HEADER] = self.__next_r
126+
}
127+
128+
// Create a new request ID for the server action request. The server uses
129+
// this to tag debug information sent via WebSocket to the client, which
130+
// then routes those chunks to the debug channel associated with this ID.
131+
headers[NEXT_REQUEST_ID_HEADER] = crypto
132+
.getRandomValues(new Uint32Array(1))[0]
133+
.toString(16)
124134
}
125135

126136
const res = await fetch(state.canonicalUrl, { method: 'POST', headers, body })
@@ -198,7 +208,7 @@ async function fetchServerAction(
198208
callServer,
199209
findSourceMapURL,
200210
temporaryReferences,
201-
debugChannel: createDebugChannel && createDebugChannel(res.headers),
211+
debugChannel: createDebugChannel && createDebugChannel(headers),
202212
}
203213
)
204214

packages/next/src/client/components/segment-cache-impl/cache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1432,7 +1432,7 @@ export async function fetchRouteOnCacheMiss(
14321432
)
14331433
const serverData = await (createFromNextReadableStream(
14341434
prefetchStream,
1435-
response.headers
1435+
headers
14361436
) as Promise<RootTreePrefetch>)
14371437
if (serverData.buildId !== getAppBuildId()) {
14381438
// The server build does not match the client. Treat as a 404. During
@@ -1484,7 +1484,7 @@ export async function fetchRouteOnCacheMiss(
14841484
)
14851485
const serverData = await (createFromNextReadableStream(
14861486
prefetchStream,
1487-
response.headers
1487+
headers
14881488
) as Promise<NavigationFlightResponse>)
14891489
if (serverData.b !== getAppBuildId()) {
14901490
// The server build does not match the client. Treat as a 404. During
@@ -1630,7 +1630,7 @@ export async function fetchSegmentOnCacheMiss(
16301630
)
16311631
const serverData = await (createFromNextReadableStream(
16321632
prefetchStream,
1633-
response.headers
1633+
headers
16341634
) as Promise<SegmentPrefetch>)
16351635
if (serverData.buildId !== getAppBuildId()) {
16361636
// The server build does not match the client. Treat as a 404. During
@@ -1750,7 +1750,7 @@ export async function fetchSegmentPrefetchesUsingDynamicRequest(
17501750
)
17511751
const serverData = await (createFromNextReadableStream(
17521752
prefetchStream,
1753-
response.headers
1753+
headers
17541754
) as Promise<NavigationFlightResponse>)
17551755

17561756
const isResponsePartial =

packages/next/src/client/dev/debug-channel.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@ export function getOrCreateDebugChannelReadableWriterPair(
2323
return pair
2424
}
2525

26-
export function createDebugChannel(responseHeaders: Headers | undefined): {
26+
export function createDebugChannel(
27+
requestHeaders: Record<string, string> | undefined
28+
): {
2729
writable?: WritableStream
2830
readable?: ReadableStream
2931
} {
3032
let requestId: string | undefined
3133

32-
if (responseHeaders) {
33-
requestId = responseHeaders.get(NEXT_REQUEST_ID_HEADER) ?? undefined
34+
if (requestHeaders) {
35+
requestId = requestHeaders[NEXT_REQUEST_ID_HEADER] ?? undefined
3436

3537
if (!requestId) {
3638
throw new InvariantError(
37-
`Expected a ${JSON.stringify(NEXT_REQUEST_ID_HEADER)} response header.`
39+
`Expected a ${JSON.stringify(NEXT_REQUEST_ID_HEADER)} request header.`
3840
)
3941
}
4042
} else {

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

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ interface ParsedRequestHeaders {
287287
readonly isRSCRequest: boolean
288288
readonly nonce: string | undefined
289289
readonly previouslyRevalidatedTags: string[]
290+
readonly requestId: string | undefined
290291
readonly htmlRequestId: string | undefined
291292
}
292293

@@ -332,10 +333,25 @@ function parseRequestHeaders(
332333
options.previewModeId
333334
)
334335

335-
const htmlRequestId =
336-
typeof headers[NEXT_HTML_REQUEST_ID_HEADER] === 'string'
337-
? headers[NEXT_HTML_REQUEST_ID_HEADER]
338-
: undefined
336+
let requestId: string | undefined
337+
let htmlRequestId: string | undefined
338+
339+
if (process.env.NODE_ENV !== 'production') {
340+
// The request IDs are only used in development mode to send debug
341+
// information to the matching client (identified by the HTML request ID
342+
// that was sent to the client with the HTML document) for the current
343+
// request (identified by the request ID, as defined by the client).
344+
345+
requestId =
346+
typeof headers[NEXT_REQUEST_ID_HEADER] === 'string'
347+
? headers[NEXT_REQUEST_ID_HEADER]
348+
: undefined
349+
350+
htmlRequestId =
351+
typeof headers[NEXT_HTML_REQUEST_ID_HEADER] === 'string'
352+
? headers[NEXT_HTML_REQUEST_ID_HEADER]
353+
: undefined
354+
}
339355

340356
return {
341357
flightRouterState,
@@ -347,6 +363,7 @@ function parseRequestHeaders(
347363
isDevWarmupRequest,
348364
nonce,
349365
previouslyRevalidatedTags,
366+
requestId,
350367
htmlRequestId,
351368
}
352369
}
@@ -1632,22 +1649,7 @@ async function renderToHTMLOrFlightImpl(
16321649
const { isStaticGeneration } = workStore
16331650

16341651
let requestId: string
1635-
1636-
if (isStaticGeneration) {
1637-
requestId = Buffer.from(
1638-
await crypto.subtle.digest('SHA-1', Buffer.from(req.url))
1639-
).toString('hex')
1640-
} else if (process.env.NEXT_RUNTIME === 'edge') {
1641-
requestId = crypto.randomUUID()
1642-
} else {
1643-
requestId = (
1644-
require('next/dist/compiled/nanoid') as typeof import('next/dist/compiled/nanoid')
1645-
).nanoid()
1646-
}
1647-
1648-
if (process.env.NODE_ENV !== 'production') {
1649-
res.setHeader(NEXT_REQUEST_ID_HEADER, requestId)
1650-
}
1652+
let htmlRequestId: string
16511653

16521654
const {
16531655
flightRouterState,
@@ -1657,9 +1659,33 @@ async function renderToHTMLOrFlightImpl(
16571659
isDevWarmupRequest,
16581660
isHmrRefresh,
16591661
nonce,
1660-
htmlRequestId = requestId,
16611662
} = parsedRequestHeaders
16621663

1664+
if (parsedRequestHeaders.requestId) {
1665+
// If the client has provided a request ID (in development mode), we use it.
1666+
requestId = parsedRequestHeaders.requestId
1667+
} else {
1668+
// Otherwise we generate a new request ID.
1669+
if (isStaticGeneration) {
1670+
requestId = Buffer.from(
1671+
await crypto.subtle.digest('SHA-1', Buffer.from(req.url))
1672+
).toString('hex')
1673+
} else if (process.env.NEXT_RUNTIME === 'edge') {
1674+
requestId = crypto.randomUUID()
1675+
} else {
1676+
requestId = (
1677+
require('next/dist/compiled/nanoid') as typeof import('next/dist/compiled/nanoid')
1678+
).nanoid()
1679+
}
1680+
}
1681+
1682+
// If the client has provided an HTML request ID, we use it to associate the
1683+
// request with the HTML document from which it originated, which is used to
1684+
// send debug information to the associated WebSocket client. Otherwise, this
1685+
// is the request for the HTML document, so we use the request ID also as the
1686+
// HTML request ID.
1687+
htmlRequestId = parsedRequestHeaders.htmlRequestId || requestId
1688+
16631689
/**
16641690
* Dynamic parameters. E.g. when you visit `/dashboard/vercel` which is rendered by `/dashboard/[slug]` the value will be {"slug": "vercel"}.
16651691
*/

0 commit comments

Comments
 (0)