repo_description: Promise based HTTP client for the browser and node.js
repo_topics: hacktoberfest http-client javascript nodejs promise
code: import util from 'util';
  import { Readable } from 'stream';
  import utils from '../utils.js';
  import readBlob from './readBlob.js';
  import platform from '../platform/index.js';
  
  const BOUNDARY_ALPHABET = platform.ALPHABET.ALPHA_DIGIT + '-_';
  
  const textEncoder = typeof TextEncoder === 'function' ? new TextEncoder() : new util.TextEncoder();
  
  const CRLF = '\r\n';
  const CRLF_BYTES = textEncoder.encode(CRLF);
  const CRLF_BYTES_COUNT = 2;
  
  class FormDataPart {
    constructor(name, value) {
      const { escapeName } = this.constructor;
      const isStringValue = utils.isString(value);
  
      let headers = `Content-Disposition: form-data; name="${escapeName(name)}"${
        !isStringValue && value.name ? `; filename="${escapeName(value.name)}"` : ''
      }${CRLF}`;
  
      if (isStringValue) {
        value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF));
      } else {
        const safeType = String(value.type || 'application/octet-stream').replace(/[\r\n]/g, '');
        headers += `Content-Type: ${safeType}${CRLF}`;
      }
  
      this.headers = textEncoder.encode(headers + CRLF);
  
      this.contentLength = isStringValue ? value.byteLength : value.size;
  
      this.size = this.headers.byteLength + this.contentLength + CRLF_BYTES_COUNT;
  
      this.name = name;
      this.value = value;
    }
  
    async *encode() {
      yield this.headers;
  
      const { value } = this;
  
      if (utils.isTypedArray(value)) {
        yield value;
      } else {
        yield* readBlob(value);
      }
  
      yield CRLF_BYTES;
    }
  
    static escapeName(name) {
      return String(name).replace(
        /[\r\n"]/g,
        (match) =>
          ({
            '\r': '%0D',
            '\n': '%0A',
            '"': '%22',
          })[match]
      );
    }
  }
  
  const formDataToStream = (form, headersHandler, options) => {
    const {
      tag = 'form-data-boundary',
      size = 25,
      boundary = tag + '-' + platform.generateString(size, BOUNDARY_ALPHABET),
    } = options || {};
  
    if (!utils.isFormData(form)) {
      throw TypeError('FormData instance required');
    }
  
    if (boundary.length < 1 || boundary.length > 70) {
      throw Error('boundary must be 10-70 characters long');
    }
  
    const boundaryBytes = textEncoder.encode('--' + boundary + CRLF);
    const footerBytes = textEncoder.encode('--' + boundary + '--' + CRLF);
    let contentLength = footerBytes.byteLength;
  
    const parts = Array.from(form.entries()).map(([name, value]) => {
      const part = new FormDataPart(name, value);
      contentLength += part.size;
      return part;
    });
  
    contentLength += boundaryBytes.byteLength * parts.length;
  
    contentLength = utils.toFiniteNumber(contentLength);
  
    const computedHeaders = {
      'Content-Type': `multipart/form-data; boundary=${boundary}`,
    };
  
    if (Number.isFinite(contentLength)) {
      computedHeaders['Content-Length'] = contentLength;
    }
  
    headersHandler && headersHandler(computedHeaders);
  
    return Readable.from(
      (async function* () {
        for (const part of parts) {
          yield boundaryBytes;
          yield* part.encode();
        }
  
        yield footerBytes;
      })()
    );
  };
  
  export default formDataToStream;
  