UNPKG

got

Version:

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

309 lines (308 loc) • 14.9 kB
import { setTimeout as delay } from 'node:timers/promises'; import is, { assert } from '@sindresorhus/is'; import asPromise from './as-promise/index.js'; import Request from './core/index.js'; import Options, { assertUrlHasSameOriginAsPrefixUrlIfNeeded, getUrlPrefixBoundary, hasUrlOrPrefixUrlBoundaryChanged, isSameOrigin, snapshotCrossOriginState, } from './core/options.js'; const isGotInstance = (value) => is.function(value); const aliases = [ 'get', 'post', 'put', 'patch', 'head', 'delete', 'query', ]; const assertNoUrlInOptionsObject = (options) => { if (Object.hasOwn(options, 'url')) { throw new TypeError('The `url` option is not supported in options objects. Pass it as the first argument instead.'); } }; const create = (defaults) => { defaults = { options: new Options(undefined, undefined, defaults.options), handlers: [...defaults.handlers], mutableDefaults: defaults.mutableDefaults, }; // `body`, `json` and `form` are documented as never being merged with the instance // defaults, so a payload given to `got.extend()` must not reach the instance's requests. // Only the payload is dropped: an instance may still set the body headers it wants. defaults.options.body = undefined; defaults.options.json = undefined; defaults.options.form = undefined; Object.defineProperty(defaults, 'mutableDefaults', { enumerable: true, configurable: false, writable: false, }); const makeRequest = (url, options, defaultOptions, isStream) => { if (is.plainObject(url)) { assertNoUrlInOptionsObject(url); } if (is.plainObject(options)) { assertNoUrlInOptionsObject(options); } // `isStream` is intentionally skipped by `merge()`. // Make the flag visible to `hooks.init` for both stream call forms. let requestInput = url; let requestOptions = options; if (isStream) { if (is.plainObject(url)) { requestInput = Object.defineProperties({}, Object.getOwnPropertyDescriptors(url)); Reflect.set(requestInput, 'isStream', true); } else if (is.plainObject(options)) { requestOptions = Object.defineProperties({}, Object.getOwnPropertyDescriptors(options)); Reflect.set(requestOptions, 'isStream', true); } } const request = new Request(requestInput, requestOptions, defaultOptions); if (isStream && request.options) { request.options.isStream = true; } // `request.options` stays unset when the input was rejected before the options were built, // and the request reports that itself. Otherwise the handler must pass the instance on. const optionsBuilt = request.options instanceof Options; let promise; const boundaryBeforeHandlers = request.options ? getUrlPrefixBoundary(request.options) : undefined; const lastHandler = (normalized) => { // `Options` keeps its state private, so a spread copy or a plain object would lose // everything and then fail later with an unrelated internal error. if (optionsBuilt && !(normalized instanceof Options)) { throw new TypeError('The `next()` function of a handler must be called with the `options` object it received'); } if (boundaryBeforeHandlers?.url && normalized?.url instanceof URL && hasUrlOrPrefixUrlBoundaryChanged(normalized, normalized.url, boundaryBeforeHandlers)) { assertUrlHasSameOriginAsPrefixUrlIfNeeded(normalized, normalized.url); } // Note: `options` is `undefined` when `new Options(...)` fails request.options = normalized; const shouldReturnStream = normalized?.isStream ?? isStream; request._noPipe = !shouldReturnStream; void request.flush(); if (shouldReturnStream) { return request; } promise ??= asPromise(request); return promise; }; let iteration = 0; const iterateHandlers = (newOptions) => { const handler = defaults.handlers[iteration++] ?? lastHandler; const result = handler(newOptions, iterateHandlers); if (is.promise(result) && !request.options?.isStream) { promise ??= asPromise(request); if (result !== promise) { const descriptors = Object.getOwnPropertyDescriptors(promise); for (const key in descriptors) { // `in` is deliberate: it also matches inherited names, and a promise carries `then` // on its prototype, which must not be copied onto the result. // eslint-disable-next-line unicorn/no-computed-property-existence-check if (key in result) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete descriptors[key]; } } // eslint-disable-next-line @typescript-eslint/no-floating-promises Object.defineProperties(result, descriptors); } } return result; }; return iterateHandlers(request.options); }; // Got interface const got = ((url, options, defaultOptions = defaults.options) => makeRequest(url, options, defaultOptions, false)); got.extend = (...instancesOrOptions) => { const options = new Options(undefined, undefined, defaults.options); const handlers = [...defaults.handlers]; // `instance.extend(…)` must behave like `got.extend(instance, …)`, so the parent's mutability is inherited unless an argument overrides it. let { mutableDefaults } = defaults; for (const value of instancesOrOptions) { if (isGotInstance(value)) { options.merge(value.defaults.options); handlers.push(...value.defaults.handlers); mutableDefaults = value.defaults.mutableDefaults; } else { assertNoUrlInOptionsObject(value); options.merge(value); if (value.handlers) { // A non-function entry would otherwise be skipped or crash inside the request // without naming the option that caused it. for (const [index, handler] of value.handlers.entries()) { if (!is.function(handler)) { throw new TypeError(`Expected \`handlers[${index}]\` to be a function, got \`${typeof handler}\``); } } handlers.push(...value.handlers); } mutableDefaults = value.mutableDefaults ?? mutableDefaults; } } return create({ options, handlers, mutableDefaults, }); }; // Pagination const paginateEach = (async function* (url, options) { if (is.plainObject(url)) { assertNoUrlInOptionsObject(url); } if (is.plainObject(options)) { assertNoUrlInOptionsObject(options); } let normalizedOptions = new Options(url, options, defaults.options); let { pagination } = normalizedOptions; assert.function(pagination.transform); assert.function(pagination.shouldContinue); assert.function(pagination.filter); assert.function(pagination.paginate); assert.number(pagination.countLimit); assert.number(pagination.requestLimit); assert.number(pagination.backoff); const allItems = []; let numberOfItems = 0; let numberOfRequests = 0; while (numberOfRequests < pagination.requestLimit && numberOfItems < pagination.countLimit) { if (numberOfRequests !== 0) { try { // eslint-disable-next-line no-await-in-loop await delay(pagination.backoff, undefined, { signal: normalizedOptions.signal }); } catch (error) { if (!normalizedOptions.signal?.aborted) { throw error; } // Let the next request report cancellation through Got's normal error handling. } } // Pagination callbacks always receive the complete response. normalizedOptions.resolveBodyOnly = false; // eslint-disable-next-line no-await-in-loop const response = (await got(undefined, undefined, normalizedOptions)); // eslint-disable-next-line no-await-in-loop const parsed = await pagination.transform(response); if (!is.array(parsed)) { // The same context the paginate override gets, so the failing page is not anonymous. throw new TypeError(`Option 'pagination.transform': Expected values which are \`Array\`. Received values of type \`${typeof parsed}\`.`); } const currentItems = []; for (const item of parsed) { if (!pagination.filter({ item, currentItems, allItems })) { continue; } if (!pagination.shouldContinue({ item, currentItems, allItems })) { return; } yield item; if (pagination.stackAllItems) { allItems.push(item); } currentItems.push(item); if (++numberOfItems >= pagination.countLimit) { return; } } if (++numberOfRequests >= pagination.requestLimit) { return; } const requestOptions = response.request.options; const previousUrl = requestOptions.url ? new URL(requestOptions.url) : undefined; const previousBoundary = getUrlPrefixBoundary(requestOptions); const previousState = previousUrl ? snapshotCrossOriginState(requestOptions) : undefined; // The inner `await` applies an async `paginate`, the same way `transform` is handled. // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-loop-func -- Pagination is updated only after this callback finishes. const [optionsToMerge, changedState] = await requestOptions.trackStateMutations(async (changedState) => [ await pagination.paginate({ response, currentItems, allItems, }), changedState, ]); if (optionsToMerge === false) { return; } // A `paginate` that forgot its return value must be reported at the option, the same // way `transform` is, instead of crashing later while reading `url` off it. if (!(optionsToMerge instanceof Options) && !is.plainObject(optionsToMerge)) { throw new TypeError(`Option 'pagination.paginate': Expected \`false\` or a plain options object, got \`${typeof optionsToMerge}\``); } if (optionsToMerge === requestOptions) { normalizedOptions = requestOptions; normalizedOptions.clearUnchangedCookieHeader(previousState, changedState); const nextUrl = normalizedOptions.url; if (previousUrl && nextUrl) { if (hasUrlOrPrefixUrlBoundaryChanged(normalizedOptions, nextUrl, previousBoundary)) { assertUrlHasSameOriginAsPrefixUrlIfNeeded(normalizedOptions, nextUrl); } if (!isSameOrigin(previousUrl, nextUrl)) { normalizedOptions.prefixUrl = ''; normalizedOptions.stripUnchangedCrossOriginState(previousState, changedState); } } } else { const paginationBoundary = getUrlPrefixBoundary(normalizedOptions); const paginationUrl = paginationBoundary.url; try { assert.any([is.string, is.urlInstance, is.undefined], optionsToMerge.url); } catch (error) { if (error instanceof Error) { error.message = `Option 'pagination.paginate.url': ${error.message}`; } throw error; } const { nextUrl, hasExplicitBody } = normalizedOptions.mergeNextRequestOptions(optionsToMerge, { previousUrl, previousState, previousBoundary: paginationBoundary, baseUrl: previousUrl, }); if (previousUrl && paginationUrl && !isSameOrigin(paginationUrl, previousUrl)) { normalizedOptions.stripSensitiveHeaders(paginationUrl, previousUrl, optionsToMerge); if (!hasExplicitBody) { normalizedOptions.clearBody(); } } if (nextUrl && (normalizedOptions.prefixUrl.toString() !== paginationBoundary.prefixUrl || normalizedOptions.allowAbsoluteUrls !== paginationBoundary.allowAbsoluteUrls)) { assertUrlHasSameOriginAsPrefixUrlIfNeeded(normalizedOptions, nextUrl); } } pagination = normalizedOptions.pagination; } }); got.paginate = paginateEach; got.paginate.all = (async (url, options) => Array.fromAsync(paginateEach(url, options))); // For those who like very descriptive names got.paginate.each = paginateEach; // Stream API got.stream = ((url, options) => makeRequest(url, options, defaults.options, true)); // Shortcuts for (const method of aliases) { got[method] = ((url, options) => got(url, { ...options, method })); got.stream[method] = ((url, options) => makeRequest(url, { ...options, method }, defaults.options, true)); } if (!defaults.mutableDefaults) { Object.freeze(defaults); Object.freeze(defaults.handlers); defaults.options.freeze(); } Object.defineProperty(got, 'defaults', { value: defaults, writable: false, configurable: false, enumerable: true, }); return got; }; export default create;