Sending multipart/mixed form data in Workers

With the form-data node package you can define content types / headers per item in the form, see example.
const form = new FormData();
form.append('directive', JSON.stringify(directive), {
contentType: 'application/json',
header: {
'content-type': 'application/json; charset=utf-8',
}
});
const form = new FormData();
form.append('directive', JSON.stringify(directive), {
contentType: 'application/json',
header: {
'content-type': 'application/json; charset=utf-8',
}
});
I'm trying to conform to the following spec: https://docs.expo.dev/technical-specs/expo-updates-1/#multipart-response Any tips on how to achieve this in Workers?
Expo Documentation
Expo Updates v1
Version 1 • Updated 2023-03-13
2 Replies
James
James10mo ago
Here's an example for something similar I'm doing:
const form = new FormData();
form.append('file.mjs', new File(['console.log("test")'], 'index.mjs', {
type: 'application/javascript+module'
}));
const form = new FormData();
form.append('file.mjs', new File(['console.log("test")'], 'index.mjs', {
type: 'application/javascript+module'
}));
Or you could do like:
form.append('directive', new Blob([JSON.stringify(foo)], {
type: "application/json"
}));
form.append('directive', new Blob([JSON.stringify(foo)], {
type: "application/json"
}));
ItsWendell
ItsWendell10mo ago
@cherryjimbo not sure if this is the most optimal way, but we've eventually settled for writing something custom using streams:
export const createMultipartStream = (options: { boundaryName?: string }) => {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();

const boundary = `-----${
options.boundaryName ? options.boundaryName + "-" : ""
}${new Date().getTime()}`;

const encoder = new TextEncoder();

const writeFormData = async (
name: string,
value: string,
options?: {
filename?: string;
headers: Record<string, string>;
},
) => {
return writer.ready.then(() => {
const headers = options?.headers ?? {};
headers["Content-Disposition"] = `form-data; name="${name}"${
options?.filename ? `; filename="${options.filename}"` : ""
}`;

const message =
`--${boundary}\r\n` +
Object.keys(headers)
.map((key) => `${key}: ${headers[key]}\r\n`)
.join("") +
`\r\n` +
`${value}\r\n`;

const encoded = encoder.encode(message);
return writer.write(encoded);
});
};

const close = async () => {
return writer.ready
.then(() => {
// Write footer
return writer.write(encoder.encode(`--${boundary}--\r\n`));
})
.then(() => writer.close());
};

const getHeaders = (headers: Headers = new Headers()) => {
// Get all written Content-Disposition headers
const contentType = headers.get("Content-Type");
headers.set("Content-Type", `multipart/mixed; boundary=${boundary}`);
if (contentType) {
headers.append("Content-Type", contentType);
}
return headers;
};

return {
boundary,
writeFormData,
close,
readable,
getHeaders,
};
};
export const createMultipartStream = (options: { boundaryName?: string }) => {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();

const boundary = `-----${
options.boundaryName ? options.boundaryName + "-" : ""
}${new Date().getTime()}`;

const encoder = new TextEncoder();

const writeFormData = async (
name: string,
value: string,
options?: {
filename?: string;
headers: Record<string, string>;
},
) => {
return writer.ready.then(() => {
const headers = options?.headers ?? {};
headers["Content-Disposition"] = `form-data; name="${name}"${
options?.filename ? `; filename="${options.filename}"` : ""
}`;

const message =
`--${boundary}\r\n` +
Object.keys(headers)
.map((key) => `${key}: ${headers[key]}\r\n`)
.join("") +
`\r\n` +
`${value}\r\n`;

const encoded = encoder.encode(message);
return writer.write(encoded);
});
};

const close = async () => {
return writer.ready
.then(() => {
// Write footer
return writer.write(encoder.encode(`--${boundary}--\r\n`));
})
.then(() => writer.close());
};

const getHeaders = (headers: Headers = new Headers()) => {
// Get all written Content-Disposition headers
const contentType = headers.get("Content-Type");
headers.set("Content-Type", `multipart/mixed; boundary=${boundary}`);
if (contentType) {
headers.append("Content-Type", contentType);
}
return headers;
};

return {
boundary,
writeFormData,
close,
readable,
getHeaders,
};
};