@remix-run/web-fetch
Version:
Web API compatible fetch implementation
120 lines (99 loc) • 3.16 kB
JavaScript
import {randomBytes} from 'crypto';
import { iterateMultipart } from '@web3-storage/multipart-parser';
import { FormData, File } from '../package.js';
import { isBlob } from './is.js';
const carriage = '\r\n';
const dashes = '-'.repeat(2);
const carriageLength = Buffer.byteLength(carriage);
/**
* @param {string} boundary
*/
const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`;
/**
* @param {string} boundary
* @param {string} name
* @param {*} field
*
* @return {string}
*/
function getHeader(boundary, name, field) {
let header = '';
header += `${dashes}${boundary}${carriage}`;
header += `Content-Disposition: form-data; name="${name}"`;
if (isBlob(field)) {
const { name = 'blob', type } = /** @type {Blob & {name?:string}} */ (field);
header += `; filename="${name}"${carriage}`;
header += `Content-Type: ${type || 'application/octet-stream'}`;
}
return `${header}${carriage.repeat(2)}`;
}
/**
* @return {string}
*/
export const getBoundary = () => randomBytes(8).toString('hex');
/**
* @param {FormData} form
* @param {string} boundary
*/
export async function * formDataIterator(form, boundary) {
const encoder = new TextEncoder();
for (const [name, value] of form) {
yield encoder.encode(getHeader(boundary, name, value));
if (isBlob(value)) {
// @ts-ignore - we know our streams implement aysnc iteration
yield * value.stream();
} else {
yield encoder.encode(value);
}
yield encoder.encode(carriage);
}
yield encoder.encode(getFooter(boundary));
}
/**
* @param {FormData} form
* @param {string} boundary
*/
export function getFormDataLength(form, boundary) {
let length = 0;
for (const [name, value] of form) {
length += Buffer.byteLength(getHeader(boundary, name, value));
if (isBlob(value)) {
length += value.size;
} else {
length += Buffer.byteLength(String(value));
}
length += carriageLength;
}
length += Buffer.byteLength(getFooter(boundary));
return length;
}
/**
* @param {Body & {headers?:Headers}} source
*/
export const toFormData = async (source) => {
let { body, headers } = source;
const contentType = headers?.get('Content-Type') || ''
if (contentType.startsWith('application/x-www-form-urlencoded') && body != null) {
const form = new FormData();
let bodyText = await source.text();
new URLSearchParams(bodyText).forEach((v, k) => form.append(k, v));
return form;
}
const [type, boundary] = contentType.split(/\s*;\s*boundary=/)
if (type === 'multipart/form-data' && boundary != null && body != null) {
const form = new FormData()
const parts = iterateMultipart(body, boundary)
for await (const { name, data, filename, contentType } of parts) {
if (typeof filename === 'string') {
form.append(name, new File([data], filename, { type: contentType }))
} else if (typeof filename !== 'undefined') {
form.append(name, new File([], '', { type: contentType }))
} else {
form.append(name, new TextDecoder().decode(data), filename)
}
}
return form
} else {
throw new TypeError('Could not parse content as FormData.')
}
}