From a21d1475ffd7225a463f2d0c0c9b732c8dd795eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 22 Apr 2023 01:04:24 -0400 Subject: [PATCH] [Flight] Fix File Upload in Node.js (#26700) Use the Blob constructor + append with filename instead of File constructor. Node.js doesn't expose a global File constructor but does support it in this form. Queue fields until we get the 'end' event from the previous file. We rely on previous files being available by the time a field is resolved. However, since the 'end' event in Readable is fired after two micro-tasks, these are not resolved in order. I use a queue of the fields while we're still waiting on files to finish. This still doesn't resolve files and fields in order relative to each other but that doesn't matter for our usage. --- fixtures/flight/src/Form.js | 8 +++++++- fixtures/flight/src/actions.js | 9 ++++++++- .../src/ReactFlightDOMServerNode.js | 20 ++++++++++++++++++- .../src/ReactFlightReplyServer.js | 7 +++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/fixtures/flight/src/Form.js b/fixtures/flight/src/Form.js index 8c2ff2922d..a4b92366d4 100644 --- a/fixtures/flight/src/Form.js +++ b/fixtures/flight/src/Form.js @@ -20,8 +20,14 @@ export default function Form({action, children}) { React.startTransition(() => setIsPending(false)); } }}> - + + + {isPending ? 'Saving...' : null} ); diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js index 7143c31a39..87cba005e0 100644 --- a/fixtures/flight/src/actions.js +++ b/fixtures/flight/src/actions.js @@ -5,5 +5,12 @@ export async function like() { } export async function greet(formData) { - return 'Hi ' + formData.get('name') + '!'; + const name = formData.get('name') || 'you'; + const file = formData.get('file'); + if (file) { + return `Ok, ${name}, here is ${file.name}: + ${(await file.text()).toUpperCase()} + `; + } + return 'Hi ' + name + '!'; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index b1cda7e104..f23959b2f8 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -88,8 +88,17 @@ function decodeReplyFromBusboy( webpackMap: ServerManifest, ): Thenable { const response = createResponse(webpackMap, ''); + let pendingFiles = 0; + const queuedFields: Array = []; busboyStream.on('field', (name, value) => { - resolveField(response, name, value); + if (pendingFiles > 0) { + // Because the 'end' event fires two microtasks after the next 'field' + // we would resolve files and fields out of order. To handle this properly + // we queue any fields we receive until the previous file is done. + queuedFields.push(name, value); + } else { + resolveField(response, name, value); + } }); busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { if (encoding.toLowerCase() === 'base64') { @@ -99,12 +108,21 @@ function decodeReplyFromBusboy( 'the wrong assumption, we can easily fix it.', ); } + pendingFiles++; const file = resolveFileInfo(response, name, filename, mimeType); value.on('data', chunk => { resolveFileChunk(response, file, chunk); }); value.on('end', () => { resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; + } }); }); busboyStream.on('finish', () => { diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 88b846f283..078f76f11f 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -564,8 +564,11 @@ export function resolveFileComplete( handle: FileHandle, ): void { // Add this file to the backing store. - const file = new File(handle.chunks, handle.filename, {type: handle.mime}); - response._formData.append(key, file); + // Node.js doesn't expose a global File constructor so we need to use + // the append() form that takes the file name as the third argument, + // to create a File object. + const blob = new Blob(handle.chunks, {type: handle.mime}); + response._formData.append(key, blob, handle.filename); } export function close(response: Response): void {