got
Version:
Human-friendly and powerful HTTP request library for Node.js
309 lines (308 loc) • 14.9 kB
JavaScript
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;