Streaming an S3 file from server to server
I thought this would be easy (because it's easy to stream from a backend to a frontend and vice versa) but it was not. Let's look at how to stream a file from an S3 connection on a server to another server in Node.js.
-
Create a new
FormDatainstance (the _global built-inFormData):const formData = new FormData(); -
Create a stream of the file from S3:
import {S3Client, GetObjectCommand} from '@aws-sdk/client-s3';
const s3Client = new S3Client({
region: yourRegion,
});
const {Body} = await s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: bucketFilePath,
}),
);
if (!Body) {
throw new Error(`Failed to get S3 file: ${bucketName}:${bucketFilePath}`);
return undefined;
}
const readableStream = Body.transformToWebStream(); -
Attach metadata to the stream (if necessary):
formData.set(
'metadata',
new Blob(
[
JSON.stringify({
documentName: fileName,
}),
],
{
type: 'application/json',
},
),
'metadata',
); -
Attach the stream (in this case a PDF):
formData.set('file', {
type: 'application/pdf',
name: fileName,
[Symbol.toStringTag]: 'File',
stream() {
return readableStream;
},
} as any as Blob);This awful hack is the only way I was able to get this to work. See the following links that I found useful:
-
Install the
form-data-encoderpackage. -
Create a
FormDataEncoderinstance:import {FormDataEncoder} from 'form-data-encoder';
const formDataEncoder = new FormDataEncoder(formData); -
Create a form data encoder stream for your request body:
const body = Readable.from(formDataEncoder); -
Send your request with headers and an undocumented and untyped
duplexoption (that is required or it won't work):const response = await fetch(url, {
body,
headers: formDataEncoder.headers,
duplex: 'half',
});
All together now:
import {S3Client, GetObjectCommand} from '@aws-sdk/client-s3';
import {FormDataEncoder} from 'form-data-encoder';
const formData = new FormData();
const s3Client = new S3Client({
region: yourRegion,
});
const {Body} = await s3Client.send(
new GetObjectCommand({
Bucket: bucketName,
Key: bucketFilePath,
}),
);
if (!Body) {
throw new Error(`Failed to get S3 file: ${bucketName}:${bucketFilePath}`);
return undefined;
}
const readableStream = Body.transformToWebStream();
formData.set(
'metadata',
new Blob(
[
JSON.stringify({
documentName: fileName,
}),
],
{
type: 'application/json',
},
),
'metadata',
);
formData.set('file', {
type: 'application/pdf',
name: fileName,
[Symbol.toStringTag]: 'File',
stream() {
return readableStream;
},
} as any as Blob);
const formDataEncoder = new FormDataEncoder(formData);
const body = Readable.from(formDataEncoder);
const response = await fetch(url, {
body,
headers: formDataEncoder.headers,
duplex: 'half',
});