@@ -73,6 +73,7 @@ import { getAssetQueryString } from './get-asset-query-string'
7373import { setReferenceManifestsSingleton } from './action-encryption-utils'
7474import { createStaticRenderer } from './static/static-renderer'
7575import { MissingPostponeDataError } from './is-missing-postpone-error'
76+ import { DetachedPromise } from '../../lib/detached-promise'
7677
7778export type GetDynamicParamFromSegment = (
7879 // [slug] / [[slug]] / [...slug]
@@ -654,7 +655,13 @@ async function renderToHTMLOrFlightImpl(
654655 createServerInsertedHTML ( )
655656
656657 getTracer ( ) . getRootSpanAttributes ( ) ?. set ( 'next.route' , pagePath )
657- const bodyResult = getTracer ( ) . wrap (
658+
659+ // Create a promise that will help us signal when the headers have been
660+ // written to the metadata for static generation as they aren't written to the
661+ // response directly.
662+ const onHeadersFinished = new DetachedPromise < void > ( )
663+
664+ const renderToStream = getTracer ( ) . wrap (
658665 AppRenderSpan . getBodyResult ,
659666 {
660667 spanName : `render route (app) ${ pagePath } ` ,
@@ -703,21 +710,13 @@ async function renderToHTMLOrFlightImpl(
703710 nonce
704711 )
705712
706- const renderer = createStaticRenderer ( {
707- ppr : renderOpts . experimental . ppr ,
708- isStaticGeneration : staticGenerationStore . isStaticGeneration ,
709- postponed : renderOpts . postponed
710- ? JSON . parse ( renderOpts . postponed )
711- : null ,
712- } )
713-
714713 const ServerComponentsRenderer = createServerComponentsRenderer ( tree , {
715714 ctx,
716715 preinitScripts,
717716 options : serverComponentsRenderOpts ,
718717 } )
719718
720- const content = (
719+ const children = (
721720 < HeadManagerContext . Provider
722721 value = { {
723722 appDir : true ,
@@ -736,34 +735,51 @@ async function renderToHTMLOrFlightImpl(
736735 hasPostponed,
737736 } )
738737
739- function onHeaders ( headers : Headers ) : void {
740- // Copy headers created by React into the response object.
741- headers . forEach ( ( value : string , key : string ) => {
742- res . appendHeader ( key , value )
743- if ( ! extraRenderResultMeta . extraHeaders ) {
744- extraRenderResultMeta . extraHeaders = { }
745- }
746- extraRenderResultMeta . extraHeaders [ key ] = value
747- } )
748- }
749-
750- try {
751- const renderStream = await renderer . render ( content , {
738+ const renderer = createStaticRenderer ( {
739+ ppr : renderOpts . experimental . ppr ,
740+ isStaticGeneration,
741+ // If provided, the postpone state should be parsed as JSON so it can be
742+ // provided to React.
743+ postponed : renderOpts . postponed
744+ ? JSON . parse ( renderOpts . postponed )
745+ : null ,
746+ streamOptions : {
752747 onError : htmlRendererErrorHandler ,
753- onHeaders : onHeaders ,
748+ onHeaders : ( headers : Headers ) => {
749+ // If this is during static generation, we shouldn't write to the
750+ // headers object directly, instead we should add to the render
751+ // result.
752+ if ( isStaticGeneration ) {
753+ headers . forEach ( ( value , key ) => {
754+ extraRenderResultMeta . headers ??= { }
755+ extraRenderResultMeta . headers [ key ] = value
756+ } )
757+
758+ // Resolve the promise to continue the stream.
759+ onHeadersFinished . resolve ( )
760+ } else {
761+ headers . forEach ( ( value , key ) => {
762+ res . appendHeader ( key , value )
763+ } )
764+ }
765+ } ,
754766 maxHeadersLength : 600 ,
755767 nonce,
756768 bootstrapScripts : [ bootstrapScript ] ,
757769 formState,
758- } )
770+ } ,
771+ } )
759772
760- const { stream, postponed } = renderStream
773+ try {
774+ let { stream, postponed } = await renderer . render ( children )
761775
776+ // If the stream was postponed, we need to add the result to the
777+ // metadata so that it can be resumed later.
762778 if ( postponed ) {
763779 extraRenderResultMeta . postponed = JSON . stringify ( postponed )
764780
765- // If this render generated a postponed state, we don't want to add
766- // any other data to the response .
781+ // We don't need to "continue" this stream now as it's continued when
782+ // we resume the stream .
767783 return stream
768784 }
769785
@@ -786,10 +802,10 @@ async function renderToHTMLOrFlightImpl(
786802 }
787803
788804 if ( renderOpts . postponed ) {
789- return continuePostponedFizzStream ( stream , options )
805+ return await continuePostponedFizzStream ( stream , options )
790806 }
791807
792- return continueFizzStream ( stream , options )
808+ return await continueFizzStream ( stream , options )
793809 } catch ( err : any ) {
794810 if (
795811 err . code === 'NEXT_STATIC_GEN_BAILOUT' ||
@@ -967,8 +983,8 @@ async function renderToHTMLOrFlightImpl(
967983 ComponentMod,
968984 serverModuleMap,
969985 generateFlight,
970- staticGenerationStore : staticGenerationStore ,
971- requestStore : requestStore ,
986+ staticGenerationStore,
987+ requestStore,
972988 serverActions,
973989 ctx,
974990 } )
@@ -978,7 +994,7 @@ async function renderToHTMLOrFlightImpl(
978994 if ( actionRequestResult . type === 'not-found' ) {
979995 const notFoundLoaderTree = createNotFoundLoaderTree ( loaderTree )
980996 return new RenderResult (
981- await bodyResult ( {
997+ await renderToStream ( {
982998 asNotFound : true ,
983999 tree : notFoundLoaderTree ,
9841000 formState,
@@ -996,15 +1012,20 @@ async function renderToHTMLOrFlightImpl(
9961012 }
9971013
9981014 const renderResult = new RenderResult (
999- await bodyResult ( {
1015+ await renderToStream ( {
10001016 asNotFound : isNotFoundPath ,
10011017 tree : loaderTree ,
10021018 formState,
10031019 } ) ,
10041020 {
10051021 ...extraRenderResultMeta ,
1022+ // Wait for and collect the flight payload data if we don't have it
1023+ // already.
10061024 pageData : await stringifiedFlightPayloadPromise ,
1007- waitUntil : Promise . all ( staticGenerationStore . pendingRevalidates || [ ] ) ,
1025+ // If we have pending revalidates, wait until they are all resolved.
1026+ waitUntil : staticGenerationStore . pendingRevalidates
1027+ ? Promise . all ( staticGenerationStore . pendingRevalidates )
1028+ : undefined ,
10081029 }
10091030 )
10101031
@@ -1015,8 +1036,30 @@ async function renderToHTMLOrFlightImpl(
10151036 } )
10161037
10171038 if ( staticGenerationStore . isStaticGeneration ) {
1039+ // Collect the entire render result to a string (by streaming it to a
1040+ // string).
10181041 const htmlResult = await renderResult . toUnchunkedString ( true )
10191042
1043+ // Timeout after 1.5 seconds for the headers to write. If it takes
1044+ // longer than this it's more likely that the stream has stalled and
1045+ // there is a React bug. The headers will then be updated in the render
1046+ // result below when the metadata is re-added to the new render result.
1047+ const onTimeout = new DetachedPromise < never > ( )
1048+ const timeout = setTimeout ( ( ) => {
1049+ onTimeout . reject (
1050+ new Error (
1051+ 'Timeout waiting for headers to be emitted, this is a bug in Next.js'
1052+ )
1053+ )
1054+ } , 1500 )
1055+
1056+ // Race against the timeout and the headers being written.
1057+ await Promise . race ( [ onHeadersFinished . promise , onTimeout . promise ] )
1058+
1059+ // It got here, which means it did not reject, so clear the timeout to avoid
1060+ // it from rejecting again (which is a no-op anyways).
1061+ clearTimeout ( timeout )
1062+
10201063 if (
10211064 // if PPR is enabled
10221065 renderOpts . experimental . ppr &&
0 commit comments