'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js // (MIT licensed) const BUFFER = Symbol('buffer'); const TYPE = Symbol('type'); const CLOSED = Symbol('closed'); class Blob { constructor() { Object.defineProperty(this, Symbol.toStringTag, { value: 'Blob', writable: false, enumerable: false, configurable: true }); this[CLOSED] = false; this[TYPE] = ''; const blobParts = arguments[0]; const options = arguments[1]; const buffers = []; if (blobParts) { const a = blobParts; const length = Number(a.length); for (let i = 0; i < length; i++) { const element = a[i]; let buffer; if (element instanceof Buffer) { buffer = element; } else if (ArrayBuffer.isView(element)) { buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); } else if (element instanceof ArrayBuffer) { buffer = Buffer.from(element); } else if (element instanceof Blob) { buffer = element[BUFFER]; } else { buffer = Buffer.from(typeof element === 'string' ? element : String(element)); } buffers.push(buffer); } } this[BUFFER] = Buffer.concat(buffers); let type = options && options.type !== undefined && String(options.type).toLowerCase(); if (type && !/[^\u0020-\u007E]/.test(type)) { this[TYPE] = type; } } get size() { return this[CLOSED] ? 0 : this[BUFFER].length; } get type() { return this[TYPE]; } get isClosed() { return this[CLOSED]; } slice() { const size = this.size; const start = arguments[0]; const end = arguments[1]; let relativeStart, relativeEnd; if (start === undefined) { relativeStart = 0; } else if (start < 0) { relativeStart = Math.max(size + start, 0); } else { relativeStart = Math.min(start, size); } if (end === undefined) { relativeEnd = size; } else if (end < 0) { relativeEnd = Math.max(size + end, 0); } else { relativeEnd = Math.min(end, size); } const span = Math.max(relativeEnd - relativeStart, 0); const buffer = this[BUFFER]; const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); const blob = new Blob([], { type: arguments[2] }); blob[BUFFER] = slicedBuffer; blob[CLOSED] = this[CLOSED]; return blob; } close() { this[CLOSED] = true; } } Object.defineProperty(Blob.prototype, Symbol.toStringTag, { value: 'BlobPrototype', writable: false, enumerable: false, configurable: true }); /** * fetch-error.js * * FetchError interface for operational errors */ /** * Create FetchError instance * * @param String message Error message for human * @param String type Error type for machine * @param String systemError For Node.js system error * @return FetchError */ function FetchError(message, type, systemError) { Error.call(this, message); this.message = message; this.type = type; // when err.type is `system`, err.code contains system error code if (systemError) { this.code = this.errno = systemError.code; } // hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); } FetchError.prototype = Object.create(Error.prototype); FetchError.prototype.constructor = FetchError; FetchError.prototype.name = 'FetchError'; /** * body.js * * Body interface provides common methods for Request and Response */ const Stream = require('stream'); var _require$1 = require('stream'); const PassThrough$1 = _require$1.PassThrough; const DISTURBED = Symbol('disturbed'); let convert; try { convert = require('encoding').convert; } catch (e) {} /** * Body class * * Cannot use ES6 class because Body must be called with .call(). * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ function Body(body) { var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref$size = _ref.size; let size = _ref$size === undefined ? 0 : _ref$size; var _ref$timeout = _ref.timeout; let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; if (body == null) { // body is undefined or null body = null; } else if (typeof body === 'string') { // body is string } else if (isURLSearchParams(body)) { // body is a URLSearchParams } else if (body instanceof Blob) { // body is blob } else if (Buffer.isBuffer(body)) { // body is buffer } else if (body instanceof Stream) { // body is stream } else { // none of the above // coerce to string body = String(body); } this.body = body; this[DISTURBED] = false; this.size = size; this.timeout = timeout; } Body.prototype = { get bodyUsed() { return this[DISTURBED]; }, /** * Decode response as ArrayBuffer * * @return Promise */ arrayBuffer() { return consumeBody.call(this).then(function (buf) { return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); }); }, /** * Return raw response as Blob * * @return Promise */ blob() { let ct = this.headers && this.headers.get('content-type') || ''; return consumeBody.call(this).then(function (buf) { return Object.assign( // Prevent copying new Blob([], { type: ct.toLowerCase() }), { [BUFFER]: buf }); }); }, /** * Decode response as json * * @return Promise */ json() { var _this = this; return consumeBody.call(this).then(function (buffer) { try { return JSON.parse(buffer.toString()); } catch (err) { return Body.Promise.reject(new FetchError(`invalid json response body at ${_this.url} reason: ${err.message}`, 'invalid-json')); } }); }, /** * Decode response as text * * @return Promise */ text() { return consumeBody.call(this).then(function (buffer) { return buffer.toString(); }); }, /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer() { return consumeBody.call(this); }, /** * Decode response as text, while automatically detecting the encoding and * trying to decode to UTF-8 (non-spec api) * * @return Promise */ textConverted() { var _this2 = this; return consumeBody.call(this).then(function (buffer) { return convertBody(buffer, _this2.headers); }); } }; Body.mixIn = function (proto) { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof if (!(name in proto)) { const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); Object.defineProperty(proto, name, desc); } } }; /** * Decode buffers into utf-8 string * * @return Promise */ function consumeBody(body) { var _this3 = this; if (this[DISTURBED]) { return Body.Promise.reject(new Error(`body used already for: ${this.url}`)); } this[DISTURBED] = true; // body is null if (this.body === null) { return Body.Promise.resolve(Buffer.alloc(0)); } // body is string if (typeof this.body === 'string') { return Body.Promise.resolve(Buffer.from(this.body)); } // body is blob if (this.body instanceof Blob) { return Body.Promise.resolve(this.body[BUFFER]); } // body is buffer if (Buffer.isBuffer(this.body)) { return Body.Promise.resolve(this.body); } // istanbul ignore if: should never happen if (!(this.body instanceof Stream)) { return Body.Promise.resolve(Buffer.alloc(0)); } // body is stream // get ready to actually consume the body let accum = []; let accumBytes = 0; let abort = false; return new Body.Promise(function (resolve, reject) { let resTimeout; // allow timeout on slow response body if (_this3.timeout) { resTimeout = setTimeout(function () { abort = true; reject(new FetchError(`Response timeout while trying to fetch ${_this3.url} (over ${_this3.timeout}ms)`, 'body-timeout')); }, _this3.timeout); } // handle stream error, such as incorrect content-encoding _this3.body.on('error', function (err) { reject(new FetchError(`Invalid response body while trying to fetch ${_this3.url}: ${err.message}`, 'system', err)); }); _this3.body.on('data', function (chunk) { if (abort || chunk === null) { return; } if (_this3.size && accumBytes + chunk.length > _this3.size) { abort = true; reject(new FetchError(`content size at ${_this3.url} over limit: ${_this3.size}`, 'max-size')); return; } accumBytes += chunk.length; accum.push(chunk); }); _this3.body.on('end', function () { if (abort) { return; } clearTimeout(resTimeout); resolve(Buffer.concat(accum)); }); }); } /** * Detect buffer encoding and convert to target encoding * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding * * @param Buffer buffer Incoming buffer * @param String encoding Target encoding * @return String */ function convertBody(buffer, headers) { if (typeof convert !== 'function') { throw new Error('The package `encoding` must be installed to use the textConverted() function'); } const ct = headers.get('content-type'); let charset = 'utf-8'; let res, str; // header if (ct) { res = /charset=([^;]*)/i.exec(ct); } // no charset in content type, peek at response body for at most 1024 bytes str = buffer.slice(0, 1024).toString(); // html5 if (!res && str) { res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str); } // html4 if (!res && str) { res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str); if (res) { res = /charset=(.*)/i.exec(res.pop()); } } // xml if (!res && str) { res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str); } // found charset if (res) { charset = res.pop(); // prevent decode issues when sites use incorrect encoding // ref: https://hsivonen.fi/encoding-menu/ if (charset === 'gb2312' || charset === 'gbk') { charset = 'gb18030'; } } // turn raw buffers into a single utf-8 buffer return convert(buffer, 'UTF-8', charset).toString(); } /** * Detect a URLSearchParams object * ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143 * * @param Object obj Object to detect by type or brand * @return String */ function isURLSearchParams(obj) { // Duck-typing as a necessary condition. if (typeof obj !== 'object' || typeof obj.append !== 'function' || typeof obj.delete !== 'function' || typeof obj.get !== 'function' || typeof obj.getAll !== 'function' || typeof obj.has !== 'function' || typeof obj.set !== 'function') { return false; } // Brand-checking and more duck-typing as optional condition. return obj.constructor.name === 'URLSearchParams' || Object.prototype.toString.call(obj) === '[object URLSearchParams]' || typeof obj.sort === 'function'; } /** * Clone body given Res/Req instance * * @param Mixed instance Response or Request instance * @return Mixed */ function clone(instance) { let p1, p2; let body = instance.body; // don't allow cloning a used body if (instance.bodyUsed) { throw new Error('cannot clone body after it is used'); } // check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if (body instanceof Stream && typeof body.getBoundary !== 'function') { // tee instance body p1 = new PassThrough$1(); p2 = new PassThrough$1(); body.pipe(p1); body.pipe(p2); // set instance body to teed body and return the other teed body instance.body = p1; body = p2; } return body; } /** * Performs the operation "extract a `Content-Type` value from |object|" as * specified in the specification: * https://fetch.spec.whatwg.org/#concept-bodyinit-extract * * This function assumes that instance.body is present and non-null. * * @param Mixed instance Response or Request instance */ function extractContentType(instance) { const body = instance.body; // istanbul ignore if: Currently, because of a guard in Request, body // can never be null. Included here for completeness. if (body === null) { // body is null return null; } else if (typeof body === 'string') { // body is string return 'text/plain;charset=UTF-8'; } else if (isURLSearchParams(body)) { // body is a URLSearchParams return 'application/x-www-form-urlencoded;charset=UTF-8'; } else if (body instanceof Blob) { // body is blob return body.type || null; } else if (Buffer.isBuffer(body)) { // body is buffer return null; } else if (typeof body.getBoundary === 'function') { // detect form data input from form-data module return `multipart/form-data;boundary=${body.getBoundary()}`; } else { // body is stream // can't really do much about this return null; } } function getTotalBytes(instance) { const body = instance.body; // istanbul ignore if: included for completion if (body === null) { // body is null return 0; } else if (typeof body === 'string') { // body is string return Buffer.byteLength(body); } else if (isURLSearchParams(body)) { // body is URLSearchParams return Buffer.byteLength(String(body)); } else if (body instanceof Blob) { // body is blob return body.size; } else if (Buffer.isBuffer(body)) { // body is buffer return body.length; } else if (body && typeof body.getLengthSync === 'function') { // detect form data input from form-data module if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x body.hasKnownLength && body.hasKnownLength()) { // 2.x return body.getLengthSync(); } return null; } else { // body is stream // can't really do much about this return null; } } function writeToStream(dest, instance) { const body = instance.body; if (body === null) { // body is null dest.end(); } else if (typeof body === 'string') { // body is string dest.write(body); dest.end(); } else if (isURLSearchParams(body)) { // body is URLSearchParams dest.write(Buffer.from(String(body))); dest.end(); } else if (body instanceof Blob) { // body is blob dest.write(body[BUFFER]); dest.end(); } else if (Buffer.isBuffer(body)) { // body is buffer dest.write(body); dest.end(); } else { // body is stream body.pipe(dest); } } // expose Promise Body.Promise = global.Promise; /** * A set of utilities borrowed from Node.js' _http_common.js */ /** * Verifies that the given val is a valid HTTP token * per the rules defined in RFC 7230 * See https://tools.ietf.org/html/rfc7230#section-3.2.6 * * Allowed characters in an HTTP token: * ^_`a-z 94-122 * A-Z 65-90 * - 45 * 0-9 48-57 * ! 33 * #$%&' 35-39 * *+ 42-43 * . 46 * | 124 * ~ 126 * * This implementation of checkIsHttpToken() loops over the string instead of * using a regular expression since the former is up to 180% faster with v8 4.9 * depending on the string length (the shorter the string, the larger the * performance difference) * * Additionally, checkIsHttpToken() is currently designed to be inlinable by v8, * so take care when making changes to the implementation so that the source * code size does not exceed v8's default max_inlined_source_size setting. **/ /* istanbul ignore next */ function isValidTokenChar(ch) { if (ch >= 94 && ch <= 122) return true; if (ch >= 65 && ch <= 90) return true; if (ch === 45) return true; if (ch >= 48 && ch <= 57) return true; if (ch === 34 || ch === 40 || ch === 41 || ch === 44) return false; if (ch >= 33 && ch <= 46) return true; if (ch === 124 || ch === 126) return true; return false; } /* istanbul ignore next */ function checkIsHttpToken(val) { if (typeof val !== 'string' || val.length === 0) return false; if (!isValidTokenChar(val.charCodeAt(0))) return false; const len = val.length; if (len > 1) { if (!isValidTokenChar(val.charCodeAt(1))) return false; if (len > 2) { if (!isValidTokenChar(val.charCodeAt(2))) return false; if (len > 3) { if (!isValidTokenChar(val.charCodeAt(3))) return false; for (var i = 4; i < len; i++) { if (!isValidTokenChar(val.charCodeAt(i))) return false; } } } } return true; } /** * True if val contains an invalid field-vchar * field-value = *( field-content / obs-fold ) * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] * field-vchar = VCHAR / obs-text * * checkInvalidHeaderChar() is currently designed to be inlinable by v8, * so take care when making changes to the implementation so that the source * code size does not exceed v8's default max_inlined_source_size setting. **/ /* istanbul ignore next */ function checkInvalidHeaderChar(val) { val += ''; if (val.length < 1) return false; var c = val.charCodeAt(0); if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; if (val.length < 2) return false; c = val.charCodeAt(1); if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; if (val.length < 3) return false; c = val.charCodeAt(2); if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; for (var i = 3; i < val.length; ++i) { c = val.charCodeAt(i); if (c <= 31 && c !== 9 || c > 255 || c === 127) return true; } return false; } /** * headers.js * * Headers class offers convenient helpers */ function sanitizeName(name) { name += ''; if (!checkIsHttpToken(name)) { throw new TypeError(`${name} is not a legal HTTP header name`); } return name.toLowerCase(); } function sanitizeValue(value) { value += ''; if (checkInvalidHeaderChar(value)) { throw new TypeError(`${value} is not a legal HTTP header value`); } return value; } const MAP = Symbol('map'); class Headers { /** * Headers class * * @param Object headers Response headers * @return Void */ constructor() { let init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; this[MAP] = Object.create(null); if (init instanceof Headers) { const rawHeaders = init.raw(); const headerNames = Object.keys(rawHeaders); for (const headerName of headerNames) { for (const value of rawHeaders[headerName]) { this.append(headerName, value); } } return; } // We don't worry about converting prop to ByteString here as append() // will handle it. if (init == null) { // no op } else if (typeof init === 'object') { const method = init[Symbol.iterator]; if (method != null) { if (typeof method !== 'function') { throw new TypeError('Header pairs must be iterable'); } // sequence<sequence<ByteString>> // Note: per spec we have to first exhaust the lists then process them const pairs = []; for (const pair of init) { if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { throw new TypeError('Each header pair must be iterable'); } pairs.push(Array.from(pair)); } for (const pair of pairs) { if (pair.length !== 2) { throw new TypeError('Each header pair must be a name/value tuple'); } this.append(pair[0], pair[1]); } } else { // record<ByteString, ByteString> for (const key of Object.keys(init)) { const value = init[key]; this.append(key, value); } } } else { throw new TypeError('Provided initializer must be an object'); } Object.defineProperty(this, Symbol.toStringTag, { value: 'Headers', writable: false, enumerable: false, configurable: true }); } /** * Return first header value given name * * @param String name Header name * @return Mixed */ get(name) { const list = this[MAP][sanitizeName(name)]; if (!list) { return null; } return list.join(', '); } /** * Iterate over all headers * * @param Function callback Executed for each item with parameters (value, name, thisArg) * @param Boolean thisArg `this` context for callback function * @return Void */ forEach(callback) { let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; let pairs = getHeaderPairs(this); let i = 0; while (i < pairs.length) { var _pairs$i = pairs[i]; const name = _pairs$i[0], value = _pairs$i[1]; callback.call(thisArg, value, name, this); pairs = getHeaderPairs(this); i++; } } /** * Overwrite header values given name * * @param String name Header name * @param String value Header value * @return Void */ set(name, value) { this[MAP][sanitizeName(name)] = [sanitizeValue(value)]; } /** * Append a value onto existing header * * @param String name Header name * @param String value Header value * @return Void */ append(name, value) { if (!this.has(name)) { this.set(name, value); return; } this[MAP][sanitizeName(name)].push(sanitizeValue(value)); } /** * Check for header name existence * * @param String name Header name * @return Boolean */ has(name) { return !!this[MAP][sanitizeName(name)]; } /** * Delete all header values given name * * @param String name Header name * @return Void */ delete(name) { delete this[MAP][sanitizeName(name)]; } /** * Return raw headers (non-spec api) * * @return Object */ raw() { return this[MAP]; } /** * Get an iterator on keys. * * @return Iterator */ keys() { return createHeadersIterator(this, 'key'); } /** * Get an iterator on values. * * @return Iterator */ values() { return createHeadersIterator(this, 'value'); } /** * Get an iterator on entries. * * This is the default iterator of the Headers object. * * @return Iterator */ [Symbol.iterator]() { return createHeadersIterator(this, 'key+value'); } } Headers.prototype.entries = Headers.prototype[Symbol.iterator]; Object.defineProperty(Headers.prototype, Symbol.toStringTag, { value: 'HeadersPrototype', writable: false, enumerable: false, configurable: true }); function getHeaderPairs(headers, kind) { const keys = Object.keys(headers[MAP]).sort(); return keys.map(kind === 'key' ? function (k) { return [k]; } : function (k) { return [k, headers.get(k)]; }); } const INTERNAL = Symbol('internal'); function createHeadersIterator(target, kind) { const iterator = Object.create(HeadersIteratorPrototype); iterator[INTERNAL] = { target, kind, index: 0 }; return iterator; } const HeadersIteratorPrototype = Object.setPrototypeOf({ next() { // istanbul ignore if if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { throw new TypeError('Value of `this` is not a HeadersIterator'); } var _INTERNAL = this[INTERNAL]; const target = _INTERNAL.target, kind = _INTERNAL.kind, index = _INTERNAL.index; const values = getHeaderPairs(target, kind); const len = values.length; if (index >= len) { return { value: undefined, done: true }; } const pair = values[index]; this[INTERNAL].index = index + 1; let result; if (kind === 'key') { result = pair[0]; } else if (kind === 'value') { result = pair[1]; } else { result = pair; } return { value: result, done: false }; } }, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { value: 'HeadersIterator', writable: false, enumerable: false, configurable: true }); /** * response.js * * Response class provides content decoding */ var _require$2 = require('http'); const STATUS_CODES = _require$2.STATUS_CODES; /** * Response class * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ class Response { constructor() { let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; Body.call(this, body, opts); this.url = opts.url; this.status = opts.status || 200; this.statusText = opts.statusText || STATUS_CODES[this.status]; this.headers = new Headers(opts.headers); Object.defineProperty(this, Symbol.toStringTag, { value: 'Response', writable: false, enumerable: false, configurable: true }); } /** * Convenience property representing if the request ended normally */ get ok() { return this.status >= 200 && this.status < 300; } /** * Clone this response * * @return Response */ clone() { return new Response(clone(this), { url: this.url, status: this.status, statusText: this.statusText, headers: this.headers, ok: this.ok }); } } Body.mixIn(Response.prototype); Object.defineProperty(Response.prototype, Symbol.toStringTag, { value: 'ResponsePrototype', writable: false, enumerable: false, configurable: true }); /** * request.js * * Request class contains server only options */ var _require$3 = require('url'); const format_url = _require$3.format; const parse_url = _require$3.parse; const PARSED_URL = Symbol('url'); /** * Request class * * @param Mixed input Url or Request instance * @param Object init Custom options * @return Void */ class Request { constructor(input) { let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let parsedURL; // normalize input if (!(input instanceof Request)) { if (input && input.href) { // in order to support Node.js' Url objects; though WHATWG's URL objects // will fall into this branch also (since their `toString()` will return // `href` property anyway) parsedURL = parse_url(input.href); } else { // coerce input to a string before attempting to parse parsedURL = parse_url(`${input}`); } input = {}; } else { parsedURL = parse_url(input.url); } let method = init.method || input.method || 'GET'; if ((init.body != null || input instanceof Request && input.body !== null) && (method === 'GET' || method === 'HEAD')) { throw new TypeError('Request with GET/HEAD method cannot have body'); } let inputBody = init.body != null ? init.body : input instanceof Request && input.body !== null ? clone(input) : null; Body.call(this, inputBody, { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); // fetch spec options this.method = method.toUpperCase(); this.redirect = init.redirect || input.redirect || 'follow'; this.headers = new Headers(init.headers || input.headers || {}); if (init.body != null) { const contentType = extractContentType(this); if (contentType !== null && !this.headers.has('Content-Type')) { this.headers.append('Content-Type', contentType); } } // server only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; this[PARSED_URL] = parsedURL; Object.defineProperty(this, Symbol.toStringTag, { value: 'Request', writable: false, enumerable: false, configurable: true }); } get url() { return format_url(this[PARSED_URL]); } /** * Clone this request * * @return Request */ clone() { return new Request(this); } } Body.mixIn(Request.prototype); Object.defineProperty(Request.prototype, Symbol.toStringTag, { value: 'RequestPrototype', writable: false, enumerable: false, configurable: true }); function getNodeRequestOptions(request) { const parsedURL = request[PARSED_URL]; const headers = new Headers(request.headers); // fetch step 3 if (!headers.has('Accept')) { headers.set('Accept', '*/*'); } // Basic fetch if (!parsedURL.protocol || !parsedURL.hostname) { throw new TypeError('Only absolute URLs are supported'); } if (!/^https?:$/.test(parsedURL.protocol)) { throw new TypeError('Only HTTP(S) protocols are supported'); } // HTTP-network-or-cache fetch steps 5-9 let contentLengthValue = null; if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { contentLengthValue = '0'; } if (request.body != null) { const totalBytes = getTotalBytes(request); if (typeof totalBytes === 'number') { contentLengthValue = String(totalBytes); } } if (contentLengthValue) { headers.set('Content-Length', contentLengthValue); } // HTTP-network-or-cache fetch step 12 if (!headers.has('User-Agent')) { headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); } // HTTP-network-or-cache fetch step 16 if (request.compress) { headers.set('Accept-Encoding', 'gzip,deflate'); } if (!headers.has('Connection') && !request.agent) { headers.set('Connection', 'close'); } // HTTP-network fetch step 4 // chunked encoding is handled by Node.js return Object.assign({}, parsedURL, { method: request.method, headers: headers.raw(), agent: request.agent }); } /** * index.js * * a request API compatible with window.fetch */ const http = require('http'); const https = require('https'); var _require = require('stream'); const PassThrough = _require.PassThrough; var _require2 = require('url'); const resolve_url = _require2.resolve; const zlib = require('zlib'); /** * Fetch function * * @param Mixed url Absolute url or Request instance * @param Object opts Fetch options * @return Promise */ function fetch(url, opts) { // allow custom promise if (!fetch.Promise) { throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); } Body.Promise = fetch.Promise; // wrap http.request into fetch return new fetch.Promise(function (resolve, reject) { // build request object const request = new Request(url, opts); const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; // http.request only support string as host header, this hack make custom host header possible if (options.headers.host) { options.headers.host = options.headers.host[0]; } // send request const req = send(options); let reqTimeout; if (request.timeout) { req.once('socket', function (socket) { reqTimeout = setTimeout(function () { req.abort(); reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); }, request.timeout); }); } req.on('error', function (err) { clearTimeout(reqTimeout); reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); }); req.on('response', function (res) { clearTimeout(reqTimeout); // handle redirect if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') { if (request.redirect === 'error') { reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect')); return; } if (request.counter >= request.follow) { reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); return; } if (!res.headers.location) { reject(new FetchError(`redirect location header missing at: ${request.url}`, 'invalid-redirect')); return; } // per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') { request.method = 'GET'; request.body = null; request.headers.delete('content-length'); } request.counter++; resolve(fetch(resolve_url(request.url, res.headers.location), request)); return; } // normalize location header for manual redirect mode const headers = new Headers(); for (const name of Object.keys(res.headers)) { if (Array.isArray(res.headers[name])) { for (const val of res.headers[name]) { headers.append(name, val); } } else { headers.append(name, res.headers[name]); } } if (request.redirect === 'manual' && headers.has('location')) { headers.set('location', resolve_url(request.url, headers.get('location'))); } // prepare response let body = res.pipe(new PassThrough()); const response_options = { url: request.url, status: res.statusCode, statusText: res.statusMessage, headers: headers, size: request.size, timeout: request.timeout }; // HTTP-network fetch step 16.1.2 const codings = headers.get('Content-Encoding'); // HTTP-network fetch step 16.1.3: handle content codings // in following scenarios we ignore compression support // 1. compression support is disabled // 2. HEAD request // 3. no Content-Encoding header // 4. no content response (204) // 5. content not modified response (304) if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { resolve(new Response(body, response_options)); return; } // For Node v6+ // Be less strict when decoding compressed responses, since sometimes // servers send slightly invalid responses that are still accepted // by common browsers. // Always using Z_SYNC_FLUSH is what cURL does. const zlibOptions = { flush: zlib.Z_SYNC_FLUSH, finishFlush: zlib.Z_SYNC_FLUSH }; // for gzip if (codings == 'gzip' || codings == 'x-gzip') { body = body.pipe(zlib.createGunzip(zlibOptions)); resolve(new Response(body, response_options)); return; } // for deflate if (codings == 'deflate' || codings == 'x-deflate') { // handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough()); raw.once('data', function (chunk) { // see http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = body.pipe(zlib.createInflate()); } else { body = body.pipe(zlib.createInflateRaw()); } resolve(new Response(body, response_options)); }); return; } // otherwise, use response as-is resolve(new Response(body, response_options)); }); writeToStream(req, request); }); } /** * Redirect code matching * * @param Number code Status code * @return Boolean */ fetch.isRedirect = function (code) { return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; }; // expose Promise fetch.Promise = global.Promise; module.exports = exports = fetch; exports.Headers = Headers; exports.Request = Request; exports.Response = Response; exports.FetchError = FetchError;