Skip to content

Commit 7dc903c

Browse files
authored
Patch FlightReplyServer with fixes from ReactFlightClient (#35277)
FlightReplyServer are for client->server and ReactFlightClient is for server->client. They're not 100% symmetrical. We did a number of refactors to ReactFlightClient in PRs like #29823 and #33664 to change the structure of the resolution. This PR brings those changes to synchronize the two approaches. Which addresses deep resolution of cycles and deferred error handling. This also fixes a critical security vulnerability.
1 parent 36df5e8 commit 7dc903c

File tree

9 files changed

+712
-278
lines changed

9 files changed

+712
-278
lines changed

packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -344,31 +344,42 @@ function decodeReplyFromBusboy<T>(
344344
// we queue any fields we receive until the previous file is done.
345345
queuedFields.push(name, value);
346346
} else {
347-
resolveField(response, name, value);
347+
try {
348+
resolveField(response, name, value);
349+
} catch (error) {
350+
busboyStream.destroy(error);
351+
}
348352
}
349353
});
350354
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
351355
if (encoding.toLowerCase() === 'base64') {
352-
throw new Error(
353-
"React doesn't accept base64 encoded file uploads because we don't expect " +
354-
"form data passed from a browser to ever encode data that way. If that's " +
355-
'the wrong assumption, we can easily fix it.',
356+
busboyStream.destroy(
357+
new Error(
358+
"React doesn't accept base64 encoded file uploads because we don't expect " +
359+
"form data passed from a browser to ever encode data that way. If that's " +
360+
'the wrong assumption, we can easily fix it.',
361+
),
356362
);
363+
return;
357364
}
358365
pendingFiles++;
359366
const file = resolveFileInfo(response, name, filename, mimeType);
360367
value.on('data', chunk => {
361368
resolveFileChunk(response, file, chunk);
362369
});
363370
value.on('end', () => {
364-
resolveFileComplete(response, name, file);
365-
pendingFiles--;
366-
if (pendingFiles === 0) {
367-
// Release any queued fields
368-
for (let i = 0; i < queuedFields.length; i += 2) {
369-
resolveField(response, queuedFields[i], queuedFields[i + 1]);
371+
try {
372+
resolveFileComplete(response, name, file);
373+
pendingFiles--;
374+
if (pendingFiles === 0) {
375+
// Release any queued fields
376+
for (let i = 0; i < queuedFields.length; i += 2) {
377+
resolveField(response, queuedFields[i], queuedFields[i + 1]);
378+
}
379+
queuedFields.length = 0;
370380
}
371-
queuedFields.length = 0;
381+
} catch (error) {
382+
busboyStream.destroy(error);
372383
}
373384
});
374385
});

packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
} from '../shared/ReactFlightImportMetadata';
2020
import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
2121

22+
import hasOwnProperty from 'shared/hasOwnProperty';
23+
2224
export type ServerManifest = {
2325
[string]: Array<string>,
2426
};
@@ -78,7 +80,10 @@ export function preloadModule<T>(
7880

7981
export function requireModule<T>(metadata: ClientReference<T>): T {
8082
const moduleExports = parcelRequire(metadata[ID]);
81-
return moduleExports[metadata[NAME]];
83+
if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
84+
return moduleExports[metadata[NAME]];
85+
}
86+
return (undefined: any);
8287
}
8388

8489
export function getModuleDebugInfo<T>(

packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -572,31 +572,42 @@ export function decodeReplyFromBusboy<T>(
572572
// we queue any fields we receive until the previous file is done.
573573
queuedFields.push(name, value);
574574
} else {
575-
resolveField(response, name, value);
575+
try {
576+
resolveField(response, name, value);
577+
} catch (error) {
578+
busboyStream.destroy(error);
579+
}
576580
}
577581
});
578582
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
579583
if (encoding.toLowerCase() === 'base64') {
580-
throw new Error(
581-
"React doesn't accept base64 encoded file uploads because we don't expect " +
582-
"form data passed from a browser to ever encode data that way. If that's " +
583-
'the wrong assumption, we can easily fix it.',
584+
busboyStream.destroy(
585+
new Error(
586+
"React doesn't accept base64 encoded file uploads because we don't expect " +
587+
"form data passed from a browser to ever encode data that way. If that's " +
588+
'the wrong assumption, we can easily fix it.',
589+
),
584590
);
591+
return;
585592
}
586593
pendingFiles++;
587594
const file = resolveFileInfo(response, name, filename, mimeType);
588595
value.on('data', chunk => {
589596
resolveFileChunk(response, file, chunk);
590597
});
591598
value.on('end', () => {
592-
resolveFileComplete(response, name, file);
593-
pendingFiles--;
594-
if (pendingFiles === 0) {
595-
// Release any queued fields
596-
for (let i = 0; i < queuedFields.length; i += 2) {
597-
resolveField(response, queuedFields[i], queuedFields[i + 1]);
599+
try {
600+
resolveFileComplete(response, name, file);
601+
pendingFiles--;
602+
if (pendingFiles === 0) {
603+
// Release any queued fields
604+
for (let i = 0; i < queuedFields.length; i += 2) {
605+
resolveField(response, queuedFields[i], queuedFields[i + 1]);
606+
}
607+
queuedFields.length = 0;
598608
}
599-
queuedFields.length = 0;
609+
} catch (error) {
610+
busboyStream.destroy(error);
600611
}
601612
});
602613
});

packages/react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
addChunkDebugInfo,
3535
} from 'react-client/src/ReactFlightClientConfig';
3636

37+
import hasOwnProperty from 'shared/hasOwnProperty';
38+
3739
export type ServerConsumerModuleMap = null | {
3840
[clientId: string]: {
3941
[clientExportName: string]: ClientReferenceManifestEntry,
@@ -245,7 +247,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
245247
// default property of this if it was an ESM interop module.
246248
return moduleExports.__esModule ? moduleExports.default : moduleExports;
247249
}
248-
return moduleExports[metadata[NAME]];
250+
if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
251+
return moduleExports[metadata[NAME]];
252+
}
253+
return (undefined: any);
249254
}
250255

251256
export function getModuleDebugInfo<T>(

packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -564,31 +564,42 @@ function decodeReplyFromBusboy<T>(
564564
// we queue any fields we receive until the previous file is done.
565565
queuedFields.push(name, value);
566566
} else {
567-
resolveField(response, name, value);
567+
try {
568+
resolveField(response, name, value);
569+
} catch (error) {
570+
busboyStream.destroy(error);
571+
}
568572
}
569573
});
570574
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
571575
if (encoding.toLowerCase() === 'base64') {
572-
throw new Error(
573-
"React doesn't accept base64 encoded file uploads because we don't expect " +
574-
"form data passed from a browser to ever encode data that way. If that's " +
575-
'the wrong assumption, we can easily fix it.',
576+
busboyStream.destroy(
577+
new Error(
578+
"React doesn't accept base64 encoded file uploads because we don't expect " +
579+
"form data passed from a browser to ever encode data that way. If that's " +
580+
'the wrong assumption, we can easily fix it.',
581+
),
576582
);
583+
return;
577584
}
578585
pendingFiles++;
579586
const file = resolveFileInfo(response, name, filename, mimeType);
580587
value.on('data', chunk => {
581588
resolveFileChunk(response, file, chunk);
582589
});
583590
value.on('end', () => {
584-
resolveFileComplete(response, name, file);
585-
pendingFiles--;
586-
if (pendingFiles === 0) {
587-
// Release any queued fields
588-
for (let i = 0; i < queuedFields.length; i += 2) {
589-
resolveField(response, queuedFields[i], queuedFields[i + 1]);
591+
try {
592+
resolveFileComplete(response, name, file);
593+
pendingFiles--;
594+
if (pendingFiles === 0) {
595+
// Release any queued fields
596+
for (let i = 0; i < queuedFields.length; i += 2) {
597+
resolveField(response, queuedFields[i], queuedFields[i + 1]);
598+
}
599+
queuedFields.length = 0;
590600
}
591-
queuedFields.length = 0;
601+
} catch (error) {
602+
busboyStream.destroy(error);
592603
}
593604
});
594605
});

packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
} from '../shared/ReactFlightImportMetadata';
2525
import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
2626

27+
import hasOwnProperty from 'shared/hasOwnProperty';
28+
2729
export type ServerConsumerModuleMap = {
2830
[clientId: string]: {
2931
[clientExportName: string]: ClientReference<any>,
@@ -158,7 +160,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
158160
// default property of this if it was an ESM interop module.
159161
return moduleExports.default;
160162
}
161-
return moduleExports[metadata.name];
163+
if (hasOwnProperty.call(moduleExports, metadata.name)) {
164+
return moduleExports[metadata.name];
165+
}
166+
return (undefined: any);
162167
}
163168

164169
export function getModuleDebugInfo<T>(metadata: ClientReference<T>): null {

packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
addChunkDebugInfo,
3535
} from 'react-client/src/ReactFlightClientConfig';
3636

37+
import hasOwnProperty from 'shared/hasOwnProperty';
38+
3739
export type ServerConsumerModuleMap = null | {
3840
[clientId: string]: {
3941
[clientExportName: string]: ClientReferenceManifestEntry,
@@ -253,7 +255,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
253255
// default property of this if it was an ESM interop module.
254256
return moduleExports.__esModule ? moduleExports.default : moduleExports;
255257
}
256-
return moduleExports[metadata[NAME]];
258+
if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
259+
return moduleExports[metadata[NAME]];
260+
}
261+
return (undefined: any);
257262
}
258263

259264
export function getModuleDebugInfo<T>(

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -564,31 +564,42 @@ function decodeReplyFromBusboy<T>(
564564
// we queue any fields we receive until the previous file is done.
565565
queuedFields.push(name, value);
566566
} else {
567-
resolveField(response, name, value);
567+
try {
568+
resolveField(response, name, value);
569+
} catch (error) {
570+
busboyStream.destroy(error);
571+
}
568572
}
569573
});
570574
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
571575
if (encoding.toLowerCase() === 'base64') {
572-
throw new Error(
573-
"React doesn't accept base64 encoded file uploads because we don't expect " +
574-
"form data passed from a browser to ever encode data that way. If that's " +
575-
'the wrong assumption, we can easily fix it.',
576+
busboyStream.destroy(
577+
new Error(
578+
"React doesn't accept base64 encoded file uploads because we don't expect " +
579+
"form data passed from a browser to ever encode data that way. If that's " +
580+
'the wrong assumption, we can easily fix it.',
581+
),
576582
);
583+
return;
577584
}
578585
pendingFiles++;
579586
const file = resolveFileInfo(response, name, filename, mimeType);
580587
value.on('data', chunk => {
581588
resolveFileChunk(response, file, chunk);
582589
});
583590
value.on('end', () => {
584-
resolveFileComplete(response, name, file);
585-
pendingFiles--;
586-
if (pendingFiles === 0) {
587-
// Release any queued fields
588-
for (let i = 0; i < queuedFields.length; i += 2) {
589-
resolveField(response, queuedFields[i], queuedFields[i + 1]);
591+
try {
592+
resolveFileComplete(response, name, file);
593+
pendingFiles--;
594+
if (pendingFiles === 0) {
595+
// Release any queued fields
596+
for (let i = 0; i < queuedFields.length; i += 2) {
597+
resolveField(response, queuedFields[i], queuedFields[i + 1]);
598+
}
599+
queuedFields.length = 0;
590600
}
591-
queuedFields.length = 0;
601+
} catch (error) {
602+
busboyStream.destroy(error);
592603
}
593604
});
594605
});

0 commit comments

Comments
 (0)