Skip to content

Commit c3c5bf8

Browse files
authored
Prevent client prefetch stream from closing (#72420)
Based on: - #72418 --- When PPR is enabled, prefetch streams may contain references that never resolve, because that's how we encode dynamic data access. In the decoded object returned by the Flight client, these are reified into hanging promises that suspend during render, which is effectively what we want. The UI resolves when it switches to the dynamic data stream (via useDeferredValue(dynamic, static)). However, the Flight implementation currently errors if the server closes the response before all the references are resolved. As a cheat to work around this, we wrap the original stream in a new stream that never closes, and therefore doesn't error.
1 parent 3888713 commit c3c5bf8

File tree

1 file changed

+37
-1
lines changed

1 file changed

+37
-1
lines changed

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,11 @@ export async function fetchServerResponse(
213213
}
214214

215215
// Handle the `fetch` readable stream that can be unwrapped by `React.use`.
216+
const flightStream = postponed
217+
? createUnclosingPrefetchStream(res.body)
218+
: res.body
216219
const response: NavigationFlightResponse = await createFromReadableStream(
217-
res.body,
220+
flightStream,
218221
{ callServer, findSourceMapURL }
219222
)
220223

@@ -248,3 +251,36 @@ export async function fetchServerResponse(
248251
}
249252
}
250253
}
254+
255+
function createUnclosingPrefetchStream(
256+
originalFlightStream: ReadableStream<Uint8Array>
257+
): ReadableStream<Uint8Array> {
258+
// When PPR is enabled, prefetch streams may contain references that never
259+
// resolve, because that's how we encode dynamic data access. In the decoded
260+
// object returned by the Flight client, these are reified into hanging
261+
// promises that suspend during render, which is effectively what we want.
262+
// The UI resolves when it switches to the dynamic data stream
263+
// (via useDeferredValue(dynamic, static)).
264+
//
265+
// However, the Flight implementation currently errors if the server closes
266+
// the response before all the references are resolved. As a cheat to work
267+
// around this, we wrap the original stream in a new stream that never closes,
268+
// and therefore doesn't error.
269+
const reader = originalFlightStream.getReader()
270+
return new ReadableStream({
271+
async pull(controller) {
272+
while (true) {
273+
const { done, value } = await reader.read()
274+
if (!done) {
275+
// Pass to the target stream and keep consuming the Flight response
276+
// from the server.
277+
controller.enqueue(value)
278+
continue
279+
}
280+
// The server stream has closed. Exit, but intentionally do not close
281+
// the target stream.
282+
return
283+
}
284+
},
285+
})
286+
}

0 commit comments

Comments
 (0)