diff --git a/package.json b/package.json
index 7e9b35f00beea..00a3a5e67df68 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"through2": "^3.0.1",
"tmp": "^0.1.0",
"typescript": "^3.7.5",
+ "undici": "^5.28.4",
"web-streams-polyfill": "^3.1.1",
"yargs": "^15.3.1"
},
diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js
index eb562dd7b23c9..b54f6e4edb0f5 100644
--- a/packages/react-client/src/ReactFlightReplyClient.js
+++ b/packages/react-client/src/ReactFlightReplyClient.js
@@ -187,9 +187,17 @@ export function processReply(
function serializeTypedArray(
tag: string,
- typedArray: ArrayBuffer | $ArrayBufferView,
+ typedArray: $ArrayBufferView,
): string {
- const blob = new Blob([typedArray]);
+ const blob = new Blob([
+ // We should be able to pass the buffer straight through but Node < 18 treat
+ // multi-byte array blobs differently so we first convert it to single-byte.
+ new Uint8Array(
+ typedArray.buffer,
+ typedArray.byteOffset,
+ typedArray.byteLength,
+ ),
+ ]);
const blobId = nextPartId++;
if (formData === null) {
formData = new FormData();
@@ -392,7 +400,13 @@ export function processReply(
if (enableBinaryFlight) {
if (value instanceof ArrayBuffer) {
- return serializeTypedArray('A', value);
+ const blob = new Blob([value]);
+ const blobId = nextPartId++;
+ if (formData === null) {
+ formData = new FormData();
+ }
+ formData.append(formFieldPrefix + blobId, blob);
+ return '$' + 'A' + blobId.toString(16);
}
if (value instanceof Int8Array) {
// char
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 7071ef5081542..a2e4d7af2953d 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -10,6 +10,14 @@
'use strict';
+if (typeof Blob === 'undefined') {
+ global.Blob = require('buffer').Blob;
+}
+if (typeof File === 'undefined' || typeof FormData === 'undefined') {
+ global.File = require('undici').File;
+ global.FormData = require('undici').FormData;
+}
+
function normalizeCodeLocInfo(str) {
return (
str &&
@@ -513,39 +521,37 @@ describe('ReactFlight', () => {
`);
});
- if (typeof FormData !== 'undefined') {
- it('can transport FormData (no blobs)', async () => {
- function ComponentClient({prop}) {
- return `
- formData: ${prop instanceof FormData}
- hi: ${prop.get('hi')}
- multiple: ${prop.getAll('multiple')}
- content: ${JSON.stringify(Array.from(prop))}
- `;
- }
- const Component = clientReference(ComponentClient);
-
- const formData = new FormData();
- formData.append('hi', 'world');
- formData.append('multiple', 1);
- formData.append('multiple', 2);
+ it('can transport FormData (no blobs)', async () => {
+ function ComponentClient({prop}) {
+ return `
+ formData: ${prop instanceof FormData}
+ hi: ${prop.get('hi')}
+ multiple: ${prop.getAll('multiple')}
+ content: ${JSON.stringify(Array.from(prop))}
+ `;
+ }
+ const Component = clientReference(ComponentClient);
- const model = ;
+ const formData = new FormData();
+ formData.append('hi', 'world');
+ formData.append('multiple', 1);
+ formData.append('multiple', 2);
- const transport = ReactNoopFlightServer.render(model);
+ const model = ;
- await act(async () => {
- ReactNoop.render(await ReactNoopFlightClient.read(transport));
- });
+ const transport = ReactNoopFlightServer.render(model);
- expect(ReactNoop).toMatchRenderedOutput(`
- formData: true
- hi: world
- multiple: 1,2
- content: [["hi","world"],["multiple","1"],["multiple","2"]]
- `);
+ await act(async () => {
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
- }
+
+ expect(ReactNoop).toMatchRenderedOutput(`
+ formData: true
+ hi: world
+ multiple: 1,2
+ content: [["hi","world"],["multiple","1"],["multiple","2"]]
+ `);
+ });
it('can transport cyclic objects', async () => {
function ComponentClient({prop}) {
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
index 6e2e02047bbe6..87a5266dee079 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
@@ -15,11 +15,10 @@ global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
-if (typeof Blob === 'undefined') {
- global.Blob = require('buffer').Blob;
-}
-if (typeof File === 'undefined') {
- global.File = require('buffer').File;
+global.Blob = require('buffer').Blob;
+if (typeof File === 'undefined' || typeof FormData === 'undefined') {
+ global.File = require('buffer').File || require('undici').File;
+ global.FormData = require('undici').FormData;
}
// Don't wait before processing work on the server.
@@ -379,45 +378,40 @@ describe('ReactFlightDOMEdge', () => {
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
});
- if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
- // @gate enableBinaryFlight
- it('can transport FormData (blobs)', async () => {
- const bytes = new Uint8Array([
- 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
- ]);
- const blob = new Blob([bytes, bytes], {
- type: 'application/x-test',
- });
-
- const formData = new FormData();
- formData.append('hi', 'world');
- formData.append('file', blob, 'filename.test');
-
- expect(formData.get('file') instanceof File).toBe(true);
- expect(formData.get('file').name).toBe('filename.test');
-
- const stream = passThrough(
- ReactServerDOMServer.renderToReadableStream(formData),
- );
- const result = await ReactServerDOMClient.createFromReadableStream(
- stream,
- {
- ssrManifest: {
- moduleMap: null,
- moduleLoading: null,
- },
- },
- );
-
- expect(result instanceof FormData).toBe(true);
- expect(result.get('hi')).toBe('world');
- const resultBlob = result.get('file');
- expect(resultBlob instanceof Blob).toBe(true);
- expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
- expect(resultBlob.size).toBe(bytes.length * 2);
- expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
+ // @gate enableBinaryFlight
+ it('can transport FormData (blobs)', async () => {
+ const bytes = new Uint8Array([
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
+ ]);
+ const blob = new Blob([bytes, bytes], {
+ type: 'application/x-test',
});
- }
+
+ const formData = new FormData();
+ formData.append('hi', 'world');
+ formData.append('file', blob, 'filename.test');
+
+ expect(formData.get('file') instanceof File).toBe(true);
+ expect(formData.get('file').name).toBe('filename.test');
+
+ const stream = passThrough(
+ ReactServerDOMServer.renderToReadableStream(formData),
+ );
+ const result = await ReactServerDOMClient.createFromReadableStream(stream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+
+ expect(result instanceof FormData).toBe(true);
+ expect(result.get('hi')).toBe('world');
+ const resultBlob = result.get('file');
+ expect(resultBlob instanceof Blob).toBe(true);
+ expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
+ expect(resultBlob.size).toBe(bytes.length * 2);
+ expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
+ });
it('can pass an async import that resolves later to an outline object like a Map', async () => {
let resolve;
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js
index 00a53a590c5e1..ab0d54c0bc0f3 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js
@@ -16,11 +16,10 @@ global.ReadableStream =
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
-if (typeof Blob === 'undefined') {
- global.Blob = require('buffer').Blob;
-}
-if (typeof File === 'undefined') {
- global.File = require('buffer').File;
+global.Blob = require('buffer').Blob;
+if (typeof File === 'undefined' || typeof FormData === 'undefined') {
+ global.File = require('buffer').File || require('undici').File;
+ global.FormData = require('undici').FormData;
}
// let serverExports;
@@ -44,13 +43,6 @@ describe('ReactFlightDOMReplyEdge', () => {
ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
});
- if (typeof FormData === 'undefined') {
- // We can't test if we don't have a native FormData implementation because the JSDOM one
- // is missing the arrayBuffer() method.
- it('cannot test', () => {});
- return;
- }
-
it('can encode a reply', async () => {
const body = await ReactServerDOMClient.encodeReply({some: 'object'});
const decoded = await ReactServerDOMServer.decodeReply(
@@ -89,6 +81,8 @@ describe('ReactFlightDOMReplyEdge', () => {
);
expect(result).toEqual(buffers);
+ // Array buffers can't use the toEqual helper.
+ expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
});
// @gate enableBinaryFlight
@@ -109,35 +103,33 @@ describe('ReactFlightDOMReplyEdge', () => {
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
});
- if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
- it('can transport FormData (blobs)', async () => {
- const bytes = new Uint8Array([
- 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
- ]);
- const blob = new Blob([bytes, bytes], {
- type: 'application/x-test',
- });
-
- const formData = new FormData();
- formData.append('hi', 'world');
- formData.append('file', blob, 'filename.test');
-
- expect(formData.get('file') instanceof File).toBe(true);
- expect(formData.get('file').name).toBe('filename.test');
-
- const body = await ReactServerDOMClient.encodeReply(formData);
- const result = await ReactServerDOMServer.decodeReply(
- body,
- webpackServerMap,
- );
-
- expect(result instanceof FormData).toBe(true);
- expect(result.get('hi')).toBe('world');
- const resultBlob = result.get('file');
- expect(resultBlob instanceof Blob).toBe(true);
- expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
- expect(resultBlob.size).toBe(bytes.length * 2);
- expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
+ it('can transport FormData (blobs)', async () => {
+ const bytes = new Uint8Array([
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
+ ]);
+ const blob = new Blob([bytes, bytes], {
+ type: 'application/x-test',
});
- }
+
+ const formData = new FormData();
+ formData.append('hi', 'world');
+ formData.append('file', blob, 'filename.test');
+
+ expect(formData.get('file') instanceof File).toBe(true);
+ expect(formData.get('file').name).toBe('filename.test');
+
+ const body = await ReactServerDOMClient.encodeReply(formData);
+ const result = await ReactServerDOMServer.decodeReply(
+ body,
+ webpackServerMap,
+ );
+
+ expect(result instanceof FormData).toBe(true);
+ expect(result.get('hi')).toBe('world');
+ const resultBlob = result.get('file');
+ expect(resultBlob instanceof Blob).toBe(true);
+ expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
+ expect(resultBlob.size).toBe(bytes.length * 2);
+ expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
+ });
});
diff --git a/yarn.lock b/yarn.lock
index b68244928f09d..e56d08d371ca0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2182,6 +2182,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691"
integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==
+"@fastify/busboy@^2.0.0":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
+ integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
+
"@gitbeaker/core@^21.7.0":
version "21.7.0"
resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-21.7.0.tgz#fcf7a12915d39f416e3f316d0a447a814179b8e5"
@@ -15762,6 +15767,13 @@ unc-path-regex@^0.1.0, unc-path-regex@^0.1.2:
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
+undici@^5.28.4:
+ version "5.28.4"
+ resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
+ integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
+ dependencies:
+ "@fastify/busboy" "^2.0.0"
+
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"