[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.
This commit is contained in:
Sebastian Markbåge 2023-04-22 01:04:24 -04:00 committed by GitHub
parent 36e4cbe2e9
commit a21d1475ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 39 additions and 5 deletions

View File

@ -20,8 +20,14 @@ export default function Form({action, children}) {
React.startTransition(() => setIsPending(false)); React.startTransition(() => setIsPending(false));
} }
}}> }}>
<input name="name" /> <label>
Name: <input name="name" />
</label>
<label>
File: <input type="file" name="file" />
</label>
<button>Say Hi</button> <button>Say Hi</button>
{isPending ? 'Saving...' : null}
</form> </form>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@ -5,5 +5,12 @@ export async function like() {
} }
export async function greet(formData) { 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 + '!';
} }

View File

@ -88,8 +88,17 @@ function decodeReplyFromBusboy<T>(
webpackMap: ServerManifest, webpackMap: ServerManifest,
): Thenable<T> { ): Thenable<T> {
const response = createResponse(webpackMap, ''); const response = createResponse(webpackMap, '');
let pendingFiles = 0;
const queuedFields: Array<string> = [];
busboyStream.on('field', (name, value) => { 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}) => { busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
if (encoding.toLowerCase() === 'base64') { if (encoding.toLowerCase() === 'base64') {
@ -99,12 +108,21 @@ function decodeReplyFromBusboy<T>(
'the wrong assumption, we can easily fix it.', 'the wrong assumption, we can easily fix it.',
); );
} }
pendingFiles++;
const file = resolveFileInfo(response, name, filename, mimeType); const file = resolveFileInfo(response, name, filename, mimeType);
value.on('data', chunk => { value.on('data', chunk => {
resolveFileChunk(response, file, chunk); resolveFileChunk(response, file, chunk);
}); });
value.on('end', () => { value.on('end', () => {
resolveFileComplete(response, name, file); 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', () => { busboyStream.on('finish', () => {

View File

@ -564,8 +564,11 @@ export function resolveFileComplete(
handle: FileHandle, handle: FileHandle,
): void { ): void {
// Add this file to the backing store. // Add this file to the backing store.
const file = new File(handle.chunks, handle.filename, {type: handle.mime}); // Node.js doesn't expose a global File constructor so we need to use
response._formData.append(key, file); // 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 { export function close(response: Response): void {