Skip to content

Commit a21d147

Browse files
authored
[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.
1 parent 36e4cbe commit a21d147

File tree

4 files changed

+39
-5
lines changed

4 files changed

+39
-5
lines changed

fixtures/flight/src/Form.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ export default function Form({action, children}) {
2020
React.startTransition(() => setIsPending(false));
2121
}
2222
}}>
23-
<input name="name" />
23+
<label>
24+
Name: <input name="name" />
25+
</label>
26+
<label>
27+
File: <input type="file" name="file" />
28+
</label>
2429
<button>Say Hi</button>
30+
{isPending ? 'Saving...' : null}
2531
</form>
2632
</ErrorBoundary>
2733
);

fixtures/flight/src/actions.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,12 @@ export async function like() {
55
}
66

77
export async function greet(formData) {
8-
return 'Hi ' + formData.get('name') + '!';
8+
const name = formData.get('name') || 'you';
9+
const file = formData.get('file');
10+
if (file) {
11+
return `Ok, ${name}, here is ${file.name}:
12+
${(await file.text()).toUpperCase()}
13+
`;
14+
}
15+
return 'Hi ' + name + '!';
916
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,17 @@ function decodeReplyFromBusboy<T>(
8888
webpackMap: ServerManifest,
8989
): Thenable<T> {
9090
const response = createResponse(webpackMap, '');
91+
let pendingFiles = 0;
92+
const queuedFields: Array<string> = [];
9193
busboyStream.on('field', (name, value) => {
92-
resolveField(response, name, value);
94+
if (pendingFiles > 0) {
95+
// Because the 'end' event fires two microtasks after the next 'field'
96+
// we would resolve files and fields out of order. To handle this properly
97+
// we queue any fields we receive until the previous file is done.
98+
queuedFields.push(name, value);
99+
} else {
100+
resolveField(response, name, value);
101+
}
93102
});
94103
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
95104
if (encoding.toLowerCase() === 'base64') {
@@ -99,12 +108,21 @@ function decodeReplyFromBusboy<T>(
99108
'the wrong assumption, we can easily fix it.',
100109
);
101110
}
111+
pendingFiles++;
102112
const file = resolveFileInfo(response, name, filename, mimeType);
103113
value.on('data', chunk => {
104114
resolveFileChunk(response, file, chunk);
105115
});
106116
value.on('end', () => {
107117
resolveFileComplete(response, name, file);
118+
pendingFiles--;
119+
if (pendingFiles === 0) {
120+
// Release any queued fields
121+
for (let i = 0; i < queuedFields.length; i += 2) {
122+
resolveField(response, queuedFields[i], queuedFields[i + 1]);
123+
}
124+
queuedFields.length = 0;
125+
}
108126
});
109127
});
110128
busboyStream.on('finish', () => {

packages/react-server/src/ReactFlightReplyServer.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,8 +564,11 @@ export function resolveFileComplete(
564564
handle: FileHandle,
565565
): void {
566566
// Add this file to the backing store.
567-
const file = new File(handle.chunks, handle.filename, {type: handle.mime});
568-
response._formData.append(key, file);
567+
// Node.js doesn't expose a global File constructor so we need to use
568+
// the append() form that takes the file name as the third argument,
569+
// to create a File object.
570+
const blob = new Blob(handle.chunks, {type: handle.mime});
571+
response._formData.append(key, blob, handle.filename);
569572
}
570573

571574
export function close(response: Response): void {

0 commit comments

Comments
 (0)