UNPKG

axios

Version:

Promise based HTTP client for the browser and node.js

114 lines (105 loc) • 3.63 kB
import utils from '../utils.js'; const CRLF = '\r\n'; const normalizeLines = (value) => String(value).replace(/\r\n|\r|\n/g, CRLF); const escapeName = (value) => String(value).replace(/[\r\n"]/g, (char) => char === '\r' ? '%0D' : char === '\n' ? '%0A' : '%22' ); // Count UTF-8 bytes without allocating an encoded copy of a field. FormData // string values normalize line endings; Blob/File contents remain unchanged. const stringBytes = (value, normalize) => { let length = 0; for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); if (normalize && (code === 13 || code === 10)) { if (code === 13 && value.charCodeAt(i + 1) === 10) i++; length += 2; } else if (code < 0x80) { length++; } else if (code < 0x800) { length += 2; } else if ( code >= 0xd800 && code <= 0xdbff && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff ) { length += 4; i++; } else { length += 3; } } return length; }; // Describe serialization failures independently of any request library. export class FormDataError extends TypeError { constructor(message, reason) { super(message); this.name = 'FormDataError'; this.reason = reason; } } // The caller supplies the boundary, Blob implementation, and limit error. export default function formDataToBlob(form, Blob, limit, limitError, boundary) { if (!utils.isFunction(Blob)) { throw new FormDataError('Blob is not supported', 'blob'); } if ( !utils.isString(boundary) || !/^[A-Za-z0-9'()+_,./:=? -]{1,70}$/.test(boundary) || / $/.test(boundary) ) { throw new FormDataError('Invalid multipart boundary', 'boundary'); } const footer = '--' + boundary + '--' + CRLF; let length = footer.length; const parts = []; const checkLength = (extra) => { if (length + extra > limit) throw limitError(); }; checkLength(0); for (const [name, value] of form) { const isString = utils.isString(value); const valueLength = isString ? stringBytes(value, true) : value.size; if (!Number.isFinite(valueLength) || valueLength < 0) { throw new FormDataError('Unsupported FormData part size', 'part'); } // Reject large values and names before building normalized header strings. checkLength( valueLength + stringBytes(name, true) + (!isString && value.name ? stringBytes(value.name, false) : 0) + (!isString && value.type ? stringBytes(value.type, false) : 0) ); let header = '--' + boundary + CRLF + 'Content-Disposition: form-data; name="' + escapeName(normalizeLines(name)) + '"'; if (!isString) { header += '; filename="' + escapeName(utils.isString(value.name) ? value.name : 'blob') + '"' + CRLF + 'Content-Type: ' + String(value.type || 'application/octet-stream').replace(/[\r\n]/g, ''); } header += CRLF + CRLF; const partLength = stringBytes(header, false) + valueLength + CRLF.length; checkLength(partLength); length += partLength; parts.push({ header, value, isString }); } // Native Blob composition retains existing Blob/File data. No file is read // and no complete multipart ArrayBuffer is created to determine its length. const blobParts = []; for (const part of parts) { blobParts.push(part.header, part.isString ? normalizeLines(part.value) : part.value, CRLF); } blobParts.push(footer); return new Blob(blobParts, { type: 'multipart/form-data; boundary=' + boundary }); }