@@ -9,6 +9,12 @@ import { RSCRouterGlobalErrorBoundary } from "./errorBoundaries";
9
9
import { shouldHydrateRouteLoader } from "../dom/ssr/routes" ;
10
10
import type { RSCPayload } from "./server.rsc" ;
11
11
import { createRSCRouteModules } from "./route-modules" ;
12
+ import { isRouteErrorResponse } from "../router/utils" ;
13
+
14
+ type DecodedPayload = Promise < RSCPayload > & {
15
+ _deepestRenderedBoundaryId ?: string | null ;
16
+ formState : Promise < any > ;
17
+ } ;
12
18
13
19
// Safe version of React.use() that will not cause compilation errors against
14
20
// React 18 and will result in a runtime error if used (you can't use RSC against
@@ -46,13 +52,13 @@ export type SSRCreateFromReadableStreamFunction = (
46
52
* fetchServer,
47
53
* createFromReadableStream,
48
54
* async renderHTML(getPayload) {
49
- * const payload = await getPayload();
55
+ * const payload = getPayload();
50
56
*
51
57
* return await renderHTMLToReadableStream(
52
58
* <RSCStaticRouter getPayload={getPayload} />,
53
59
* {
54
60
* bootstrapScriptContent,
55
- * formState: await getFormState( payload) ,
61
+ * formState: await payload.formState ,
56
62
* }
57
63
* );
58
64
* },
@@ -88,7 +94,7 @@ export async function routeRSCServerRequest({
88
94
fetchServer : ( request : Request ) => Promise < Response > ;
89
95
createFromReadableStream : SSRCreateFromReadableStreamFunction ;
90
96
renderHTML : (
91
- getPayload : ( ) => Promise < RSCPayload > ,
97
+ getPayload : ( ) => DecodedPayload ,
92
98
) => ReadableStream < Uint8Array > | Promise < ReadableStream < Uint8Array > > ;
93
99
hydrate ?: boolean ;
94
100
} ) : Promise < Response > {
@@ -150,8 +156,29 @@ export async function routeRSCServerRequest({
150
156
} ) ;
151
157
} ;
152
158
153
- const getPayload = async ( ) => {
154
- return createFromReadableStream ( createStream ( ) ) as Promise < RSCPayload > ;
159
+ let deepestRenderedBoundaryId : string | null = null ;
160
+ const getPayload = ( ) : DecodedPayload => {
161
+ const payloadPromise = Promise . resolve (
162
+ createFromReadableStream ( createStream ( ) ) ,
163
+ ) as Promise < RSCPayload > ;
164
+
165
+ return Object . defineProperties ( payloadPromise , {
166
+ _deepestRenderedBoundaryId : {
167
+ get ( ) {
168
+ return deepestRenderedBoundaryId ;
169
+ } ,
170
+ set ( boundaryId : string | null ) {
171
+ deepestRenderedBoundaryId = boundaryId ;
172
+ } ,
173
+ } ,
174
+ formState : {
175
+ get ( ) {
176
+ return payloadPromise . then ( ( payload ) =>
177
+ payload . type === "render" ? payload . formState : undefined ,
178
+ ) ;
179
+ } ,
180
+ } ,
181
+ } ) as DecodedPayload ;
155
182
} ;
156
183
157
184
try {
@@ -204,11 +231,69 @@ export async function routeRSCServerRequest({
204
231
if ( reason instanceof Response ) {
205
232
return reason ;
206
233
}
234
+
235
+ try {
236
+ const status = isRouteErrorResponse ( reason ) ? reason . status : 500 ;
237
+
238
+ const html = await renderHTML ( ( ) => {
239
+ const decoded = Promise . resolve (
240
+ createFromReadableStream ( createStream ( ) ) ,
241
+ ) as Promise < RSCPayload > ;
242
+
243
+ const payloadPromise = decoded . then ( ( payload ) =>
244
+ Object . assign ( payload , {
245
+ status,
246
+ errors : deepestRenderedBoundaryId
247
+ ? {
248
+ [ deepestRenderedBoundaryId ] : reason ,
249
+ }
250
+ : { } ,
251
+ } ) ,
252
+ ) ;
253
+
254
+ return Object . defineProperties ( payloadPromise , {
255
+ _deepestRenderedBoundaryId : {
256
+ get ( ) {
257
+ return deepestRenderedBoundaryId ;
258
+ } ,
259
+ set ( boundaryId : string | null ) {
260
+ deepestRenderedBoundaryId = boundaryId ;
261
+ } ,
262
+ } ,
263
+ formState : {
264
+ get ( ) {
265
+ return payloadPromise . then ( ( payload ) =>
266
+ payload . type === "render" ? payload . formState : undefined ,
267
+ ) ;
268
+ } ,
269
+ } ,
270
+ } ) as unknown as DecodedPayload ;
271
+ } ) ;
272
+
273
+ const headers = new Headers ( serverResponse . headers ) ;
274
+ headers . set ( "Content-Type" , "text/html" ) ;
275
+
276
+ if ( ! hydrate ) {
277
+ return new Response ( html , {
278
+ status : status ,
279
+ headers,
280
+ } ) ;
281
+ }
282
+
283
+ if ( ! serverResponseB ?. body ) {
284
+ throw new Error ( "Failed to clone server response" ) ;
285
+ }
286
+
287
+ const body = html . pipeThrough ( injectRSCPayload ( serverResponseB . body ) ) ;
288
+ return new Response ( body , {
289
+ status,
290
+ headers,
291
+ } ) ;
292
+ } catch {
293
+ // Throw the original error below
294
+ }
295
+
207
296
throw reason ;
208
- // TODO: Track deepest rendered boundary and re-try
209
- // Figure out how / if we need to transport the error,
210
- // or if we can just re-try on the client to reach
211
- // the correct boundary.
212
297
}
213
298
}
214
299
@@ -223,7 +308,7 @@ export interface RSCStaticRouterProps {
223
308
* A function that starts decoding of the {@link unstable_RSCPayload}. Usually passed
224
309
* through from {@link unstable_routeRSCServerRequest}'s `renderHTML`.
225
310
*/
226
- getPayload : ( ) => Promise < RSCPayload > ;
311
+ getPayload : ( ) => DecodedPayload ;
227
312
}
228
313
229
314
/**
@@ -243,13 +328,13 @@ export interface RSCStaticRouterProps {
243
328
* fetchServer,
244
329
* createFromReadableStream,
245
330
* async renderHTML(getPayload) {
246
- * const payload = await getPayload();
331
+ * const payload = getPayload();
247
332
*
248
333
* return await renderHTMLToReadableStream(
249
334
* <RSCStaticRouter getPayload={getPayload} />,
250
335
* {
251
336
* bootstrapScriptContent,
252
- * formState: await getFormState( payload) ,
337
+ * formState: await payload.formState ,
253
338
* }
254
339
* );
255
340
* },
@@ -264,8 +349,9 @@ export interface RSCStaticRouterProps {
264
349
* @returns A React component that renders the {@link unstable_RSCPayload} as HTML.
265
350
*/
266
351
export function RSCStaticRouter ( { getPayload } : RSCStaticRouterProps ) {
352
+ const decoded = getPayload ( ) ;
267
353
// Can be replaced with React.use when v18 compatibility is no longer required.
268
- const payload = useSafe ( getPayload ( ) ) ;
354
+ const payload = useSafe ( decoded ) ;
269
355
270
356
if ( payload . type === "redirect" ) {
271
357
throw new Response ( null , {
@@ -298,6 +384,12 @@ export function RSCStaticRouter({ getPayload }: RSCStaticRouterProps) {
298
384
}
299
385
300
386
const context = {
387
+ get _deepestRenderedBoundaryId ( ) {
388
+ return decoded . _deepestRenderedBoundaryId ?? null ;
389
+ } ,
390
+ set _deepestRenderedBoundaryId ( boundaryId : string | null ) {
391
+ decoded . _deepestRenderedBoundaryId = boundaryId ;
392
+ } ,
301
393
actionData : payload . actionData ,
302
394
actionHeaders : { } ,
303
395
basename : payload . basename ,
0 commit comments