Repository: vercel/next.js
Pull request: Fix potential race condition with request ID in dev mode

In development mode, the request ID needs to be available before the bootstrap script executes and triggers hydration. Previously, the request ID was set inline with the flight data stream in a script tag that was inserted after the bootstrap script, which could lead to a race condition where `self.__next_r` was accessed before it was defined.

With this PR, we're moving the request ID initialization to `bootstrapScriptContent`, which is inserted as a script tag by React immediately before the bootstrap script. This ensures that the request ID is always defined when the bootstrap script runs.

I wasn't actually able to reproduce the issue, so I also couldn't add a test for it. But we did receive reports from users that this was happening, which is plausible given the previous script order.

Changed files:
filename: packages/next/src/server/app-render/app-render.tsx
patch: @@ -2206,6 +2206,13 @@ async function renderToStream(
     page
   )
 
+  // In development mode, set the request ID as a global variable, before the
+  // bootstrap script is executed, which depends on it during hydration.
+  const bootstrapScriptContent =
+    process.env.NODE_ENV !== 'production'
+      ? `self.__next_r=${JSON.stringify(requestId)}`
+      : undefined
+
   const reactServerErrorsByDigest: Map<string, DigestedError> = new Map()
   const silenceLogger = false
   function onHTMLRenderRSCError(err: DigestedError) {
@@ -2377,8 +2384,7 @@ async function renderToStream(
         const inlinedReactServerDataStream = createInlinedDataReadableStream(
           reactServerResult.tee(),
           nonce,
-          formState,
-          requestId
+          formState
         )
 
         return chainStreams(
@@ -2425,8 +2431,7 @@ async function renderToStream(
           inlinedDataStream: createInlinedDataReadableStream(
             reactServerResult.consume(),
             nonce,
-            formState,
-            requestId
+            formState
           ),
           getServerInsertedHTML,
           getServerInsertedMetadata,
@@ -2459,6 +2464,7 @@ async function renderToStream(
           })
         },
         maxHeadersLength: reactMaxHeadersLength,
+        bootstrapScriptContent,
         bootstrapScripts: [bootstrapScript],
         formState,
       }
@@ -2495,8 +2501,7 @@ async function renderToStream(
       inlinedDataStream: createInlinedDataReadableStream(
         reactServerResult.consume(),
         nonce,
-        formState,
-        requestId
+        formState
       ),
       isStaticGeneration: generateStaticHTML,
       isBuildTimePrerendering: ctx.workStore.isBuildTimePrerendering === true,
@@ -2613,6 +2618,7 @@ async function renderToStream(
           ),
           streamOptions: {
             nonce,
+            bootstrapScriptContent,
             // Include hydration scripts in the HTML
             bootstrapScripts: [errorBootstrapScript],
             formState,
@@ -2645,8 +2651,7 @@ async function renderToStream(
           // render
           reactServerResult.consume(),
           nonce,
-          formState,
-          requestId
+          formState
         ),
         isStaticGeneration: generateStaticHTML,
         isBuildTimePrerendering: ctx.workStore.isBuildTimePrerendering === true,
@@ -3362,7 +3367,6 @@ async function prerenderToStream(
     nonce,
     pagePath,
     renderOpts,
-    requestId,
     workStore,
   } = ctx
 
@@ -4154,8 +4158,7 @@ async function prerenderToStream(
             inlinedDataStream: createInlinedDataReadableStream(
               reactServerResult.consumeAsStream(),
               nonce,
-              formState,
-              requestId
+              formState
             ),
             getServerInsertedHTML,
             getServerInsertedMetadata,
@@ -4403,8 +4406,7 @@ async function prerenderToStream(
             inlinedDataStream: createInlinedDataReadableStream(
               reactServerResult.consumeAsStream(),
               nonce,
-              formState,
-              requestId
+              formState
             ),
             getServerInsertedHTML,
             getServerInsertedMetadata,
@@ -4501,8 +4503,7 @@ async function prerenderToStream(
           inlinedDataStream: createInlinedDataReadableStream(
             reactServerResult.consumeAsStream(),
             nonce,
-            formState,
-            requestId
+            formState
           ),
           isStaticGeneration: true,
           isBuildTimePrerendering:
@@ -4680,8 +4681,7 @@ async function prerenderToStream(
           inlinedDataStream: createInlinedDataReadableStream(
             flightStream,
             nonce,
-            formState,
-            requestId
+            formState
           ),
           isStaticGeneration: true,
           isBuildTimePrerendering:

filename: packages/next/src/server/app-render/use-flight-response.tsx
patch: @@ -103,8 +103,7 @@ export function useFlightStream<T>(
 export function createInlinedDataReadableStream(
   flightStream: ReadableStream<Uint8Array>,
   nonce: string | undefined,
-  formState: unknown | null,
-  requestId: string
+  formState: unknown | null
 ): ReadableStream<Uint8Array> {
   const startScriptTag = nonce
     ? `<script nonce=${JSON.stringify(nonce)}>`
@@ -117,12 +116,7 @@ export function createInlinedDataReadableStream(
     type: 'bytes',
     start(controller) {
       try {
-        writeInitialInstructions(
-          controller,
-          startScriptTag,
-          formState,
-          requestId
-        )
+        writeInitialInstructions(controller, startScriptTag, formState)
       } catch (error) {
         // during encoding or enqueueing forward the error downstream
         controller.error(error)
@@ -166,18 +160,12 @@ export function createInlinedDataReadableStream(
 function writeInitialInstructions(
   controller: ReadableStreamDefaultController,
   scriptStart: string,
-  formState: unknown | null,
-  requestId: string
+  formState: unknown | null
 ) {
   let scriptContents = `(self.__next_f=self.__next_f||[]).push(${htmlEscapeJsonString(
     JSON.stringify([INLINE_FLIGHT_PAYLOAD_BOOTSTRAP])
   )})`
 
-  if (process.env.NODE_ENV !== 'production') {
-    // The request ID is only needed in development mode.
-    scriptContents = `self.__next_r=${JSON.stringify(requestId)};${scriptContents}`
-  }
-
   if (formState != null) {
     scriptContents += `;self.__next_f.push(${htmlEscapeJsonString(
       JSON.stringify([INLINE_FLIGHT_PAYLOAD_FORM_STATE, formState])