UNPKG

got

Version:

Human-friendly and powerful HTTP request library for Node.js

285 lines (284 loc) 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = require("fs"); const CacheableRequest = require("cacheable-request"); const EventEmitter = require("events"); const http = require("http"); const stream = require("stream"); const url_1 = require("url"); const util_1 = require("util"); const is_1 = require("@sindresorhus/is"); const http_timer_1 = require("@szmarczak/http-timer"); const calculate_retry_delay_1 = require("./calculate-retry-delay"); const errors_1 = require("./errors"); const get_response_1 = require("./get-response"); const normalize_arguments_1 = require("./normalize-arguments"); const progress_1 = require("./progress"); const timed_out_1 = require("./utils/timed-out"); const types_1 = require("./types"); const url_to_options_1 = require("./utils/url-to-options"); const pEvent = require("p-event"); const setImmediateAsync = async () => new Promise(resolve => setImmediate(resolve)); const pipeline = util_1.promisify(stream.pipeline); const redirectCodes = new Set([300, 301, 302, 303, 304, 307, 308]); exports.default = (options) => { const emitter = new EventEmitter(); const requestUrl = options.url.toString(); const redirects = []; let retryCount = 0; let currentRequest; // `request.aborted` is a boolean since v11.0.0: https://github.com/nodejs/node/commit/4b00c4fafaa2ae8c41c1f78823c0feb810ae4723#diff-e3bc37430eb078ccbafe3aa3b570c91a const isAborted = () => typeof currentRequest.aborted === 'number' || currentRequest.aborted; const emitError = async (error) => { try { for (const hook of options.hooks.beforeError) { // eslint-disable-next-line no-await-in-loop error = await hook(error); } emitter.emit('error', error); } catch (error_) { emitter.emit('error', error_); } }; const get = async () => { let httpOptions = await normalize_arguments_1.normalizeRequestArguments(options); const handleResponse = async (response) => { var _a; try { /* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */ if (options.useElectronNet) { response = new Proxy(response, { get: (target, name) => { if (name === 'trailers' || name === 'rawTrailers') { return []; } const value = target[name]; return is_1.default.function_(value) ? value.bind(target) : value; } }); } const typedResponse = response; const { statusCode } = typedResponse; typedResponse.statusMessage = is_1.default.nonEmptyString(typedResponse.statusMessage) ? typedResponse.statusMessage : http.STATUS_CODES[statusCode]; typedResponse.url = options.url.toString(); typedResponse.requestUrl = requestUrl; typedResponse.retryCount = retryCount; typedResponse.redirectUrls = redirects; typedResponse.request = { options }; typedResponse.isFromCache = (_a = typedResponse.fromCache, (_a !== null && _a !== void 0 ? _a : false)); delete typedResponse.fromCache; if (!typedResponse.isFromCache) { typedResponse.ip = response.socket.remoteAddress; } const rawCookies = typedResponse.headers['set-cookie']; if (Reflect.has(options, 'cookieJar') && rawCookies) { let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, typedResponse.url)); if (options.ignoreInvalidCookies) { promises = promises.map(async (p) => p.catch(() => { })); } await Promise.all(promises); } if (options.followRedirect && Reflect.has(typedResponse.headers, 'location') && redirectCodes.has(statusCode)) { typedResponse.resume(); // We're being redirected, we don't care about the response. // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare if (statusCode === 303 || options.methodRewriting === false) { if (options.method !== 'GET' && options.method !== 'HEAD') { // Server responded with "see other", indicating that the resource exists at another location, // and the client should request it from that location via GET or HEAD. options.method = 'GET'; } if (Reflect.has(options, 'body')) { delete options.body; } if (Reflect.has(options, 'json')) { delete options.json; } if (Reflect.has(options, 'form')) { delete options.form; } } if (redirects.length >= options.maxRedirects) { throw new errors_1.MaxRedirectsError(typedResponse, options.maxRedirects, options); } // Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604 const redirectBuffer = Buffer.from(typedResponse.headers.location, 'binary').toString(); const redirectUrl = new url_1.URL(redirectBuffer, options.url); // Redirecting to a different site, clear cookies. if (redirectUrl.hostname !== options.url.hostname && Reflect.has(options.headers, 'cookie')) { delete options.headers.cookie; } redirects.push(redirectUrl.toString()); options.url = redirectUrl; for (const hook of options.hooks.beforeRedirect) { // eslint-disable-next-line no-await-in-loop await hook(options, typedResponse); } emitter.emit('redirect', response, options); await get(); return; } await get_response_1.default(typedResponse, options, emitter); } catch (error) { emitError(error); } }; const handleRequest = async (request) => { let isPiped = false; let isFinished = false; // `request.finished` doesn't indicate whether this has been emitted or not request.once('finish', () => { isFinished = true; }); currentRequest = request; const onError = (error) => { if (error instanceof timed_out_1.TimeoutError) { error = new errors_1.TimeoutError(error, request.timings, options); } else { error = new errors_1.RequestError(error, options); } if (!emitter.retry(error)) { emitError(error); } }; request.on('error', error => { if (isPiped) { // Check if it's caught by `stream.pipeline(...)` if (!isFinished) { return; } // We need to let `TimedOutTimeoutError` through, because `stream.pipeline(…)` aborts the request automatically. if (isAborted() && !(error instanceof timed_out_1.TimeoutError)) { return; } } onError(error); }); try { http_timer_1.default(request); timed_out_1.default(request, options.timeout, options.url); emitter.emit('request', request); const uploadStream = progress_1.createProgressStream('uploadProgress', emitter, httpOptions.headers['content-length']); isPiped = true; await pipeline(httpOptions.body, uploadStream, request); request.emit('upload-complete'); } catch (error) { if (isAborted() && error.message === 'Premature close') { // The request was aborted on purpose return; } onError(error); } }; if (options.cache) { // `cacheable-request` doesn't support Node 10 API, fallback. httpOptions = { ...httpOptions, ...url_to_options_1.default(options.url) }; // @ts-ignore `cacheable-request` has got invalid types const cacheRequest = options.cacheableRequest(httpOptions, handleResponse); cacheRequest.once('error', (error) => { if (error instanceof CacheableRequest.RequestError) { emitError(new errors_1.RequestError(error, options)); } else { emitError(new errors_1.CacheError(error, options)); } }); cacheRequest.once('request', handleRequest); } else { // Catches errors thrown by calling `requestFn(…)` try { handleRequest(httpOptions[types_1.requestSymbol](options.url, httpOptions, handleResponse)); } catch (error) { emitError(new errors_1.RequestError(error, options)); } } }; emitter.retry = error => { let backoff; retryCount++; try { backoff = options.retry.calculateDelay({ attemptCount: retryCount, retryOptions: options.retry, error, computedValue: calculate_retry_delay_1.default({ attemptCount: retryCount, retryOptions: options.retry, error, computedValue: 0 }) }); } catch (error_) { emitError(error_); return false; } if (backoff) { const retry = async (options) => { try { for (const hook of options.hooks.beforeRetry) { // eslint-disable-next-line no-await-in-loop await hook(options, error, retryCount); } await get(); } catch (error_) { emitError(error_); } }; setTimeout(retry, backoff, { ...options, forceRefresh: true }); return true; } return false; }; emitter.abort = () => { emitter.prependListener('request', (request) => { request.abort(); }); if (currentRequest) { currentRequest.abort(); } }; (async () => { try { if (options.body instanceof fs_1.ReadStream) { await pEvent(options.body, 'open'); } // Promises are executed immediately. // If there were no `setImmediate` here, // `promise.json()` would have no effect // as the request would be sent already. await setImmediateAsync(); for (const hook of options.hooks.beforeRequest) { // eslint-disable-next-line no-await-in-loop await hook(options); } await get(); } catch (error) { emitError(error); } })(); return emitter; }; exports.proxyEvents = (proxy, emitter) => { const events = [ 'request', 'redirect', 'uploadProgress', 'downloadProgress' ]; for (const event of events) { emitter.on(event, (...args) => { proxy.emit(event, ...args); }); } };