got
Version:
Human-friendly and powerful HTTP request library for Node.js
2,473 lines • 101 kB
JavaScript
import { promisify, inspect, isDeepStrictEqual, } from 'node:util';
import { checkServerIdentity } from 'node:tls';
// DO NOT use destructuring for `https.request` and `http.request` as it's not compatible with `nock`.
import https from 'node:https';
import http from 'node:http';
import is, { assert } from '@sindresorhus/is';
import lowercaseKeys from 'lowercase-keys';
import parseLinkHeader from './parse-link-header.js';
import { decodeUint8Array } from './response.js';
import { normalizeError } from './errors.js';
import { isArmableTimerDelay } from './timed-out.js';
import { getUnixSocketPath } from './utils/is-unix-socket-url.js';
import DnsCache from './utils/dns-cache.js';
import http2Client from './utils/http2-client.js';
const isAgentObject = (agent) => is.object(agent) && ('http' in agent || 'https' in agent || 'http2' in agent);
const getNativeAgent = (url, agent) => {
if (!isAgentObject(agent)) {
return agent;
}
return url.protocol === 'https:' ? agent.https : agent.http;
};
/**
Generic helper that wraps any assertion function to add context to error messages.
*/
function wrapAssertionWithContext(optionName, assertionFn) {
try {
assertionFn();
}
catch (error) {
if (error instanceof Error) {
error.message = `Option '${optionName}': ${error.message}`;
}
throw error;
}
}
/**
Helper function that wraps assert.any() to provide better error messages.
When assertion fails, it includes the option name in the error message.
*/
function assertAny(optionName, validators, value) {
wrapAssertionWithContext(optionName, () => {
assert.any(validators, value);
});
}
// A count. `Infinity` stays a valid "never" value.
function assertCount(optionName, value) {
if (value === Infinity) {
return;
}
assertAny(optionName, [is.nonNegativeInteger], value);
}
/**
Helper function that wraps assert.plainObject() to provide better error messages.
When assertion fails, it includes the option name in the error message.
*/
function assertPlainObject(optionName, value) {
wrapAssertionWithContext(optionName, () => {
assert.plainObject(value);
});
}
export function isSameOrigin(previousUrl, nextUrl) {
return previousUrl.origin === nextUrl.origin
&& getUnixSocketPath(previousUrl) === getUnixSocketPath(nextUrl);
}
export const crossOriginStripHeaders = ['host', 'cookie', 'cookie2', 'authorization', 'proxy-authorization'];
const bodyHeaderNames = ['content-length', 'content-encoding', 'content-language', 'content-location', 'content-type', 'transfer-encoding', 'digest', 'content-digest', 'repr-digest', 'last-modified'];
function usesUnixSocket(url) {
return url.protocol === 'unix:' || getUnixSocketPath(url) !== undefined;
}
function hasCredentialInUrl(url, credential) {
if (url instanceof URL) {
return url[credential] !== '';
}
if (!is.string(url)) {
return false;
}
try {
return new URL(url)[credential] !== '';
}
catch {
return false;
}
}
export const hasExplicitCredentialInUrlChange = (changedState, url, credential) => (changedState.has(credential)
|| ((changedState.has('url') || changedState.has('prefixUrl')) && url?.[credential] !== ''));
export const hasBodyOptionChanged = (changedState) => changedState.has('body') || changedState.has('json') || changedState.has('form');
// The URL parser leaves malformed percent-escapes untouched, so decoding must not throw.
function safeDecode(value) {
try {
return decodeURIComponent(value);
}
catch {
return value;
}
}
const hasProtocolSlashes = (value) => /^[a-z][\d+\-.a-z]*:\/\//iv.test(value);
const hasHttpProtocolWithoutSlashes = (value) => /^https?:(?!\/\/)/iv.test(value);
const hasUnixProtocolWithoutSlashes = (value) => /^unix:/iv.test(value);
const isAbsoluteUrl = (url) => is.urlInstance(url) || (is.string(url) && (hasProtocolSlashes(url) || url.startsWith('//')));
const isSlashOrBackslash = (character) => character === '/' || character === '\\';
const startsWithSchemeRelativeSeparators = (value) => value.length > 1 && isSlashOrBackslash(value[0]) && isSlashOrBackslash(value[1]);
const stripLeadingC0ControlOrSpace = (value) => {
let index = 0;
while (index < value.length && value.codePointAt(index) <= 0x20) {
index++;
}
return value.slice(index);
};
const removeAsciiTabOrNewline = (value) => {
let result = '';
for (const character of value) {
const codePoint = character.codePointAt(0);
if (codePoint !== 0x09 && codePoint !== 0x0A && codePoint !== 0x0D) {
result += character;
}
}
return result;
};
const assertRelativeUrlIfNeeded = (options, url) => {
if (!options.prefixUrl || options.allowAbsoluteUrls) {
return;
}
const normalizedUrl = is.string(url) ? stripLeadingC0ControlOrSpace(removeAsciiTabOrNewline(url)) : url;
const isDisallowed = isAbsoluteUrl(normalizedUrl)
|| (is.string(normalizedUrl) && (hasHttpProtocolWithoutSlashes(normalizedUrl)
|| startsWithSchemeRelativeSeparators(normalizedUrl)
|| (options.enableUnixSockets && hasUnixProtocolWithoutSlashes(normalizedUrl))));
if (isDisallowed) {
throw new Error('The `url` option must be relative when `allowAbsoluteUrls` is false and `prefixUrl` is set');
}
};
export const assertUrlHasSameOriginAsPrefixUrlIfNeeded = (options, url) => {
if (!options.prefixUrl || options.allowAbsoluteUrls) {
return;
}
let prefixUrl;
try {
prefixUrl = new URL(options.prefixUrl);
}
catch {
return;
}
if (isSameOrigin(prefixUrl, url)) {
return;
}
throw new Error('The `url` option must stay on the same origin as `prefixUrl` when `allowAbsoluteUrls` is false');
};
export const getUrlPrefixBoundary = (options) => ({
url: options.url instanceof URL ? new URL(options.url) : undefined,
prefixUrl: options.prefixUrl.toString(),
allowAbsoluteUrls: options.allowAbsoluteUrls,
});
export const hasUrlOrPrefixUrlBoundaryChanged = (options, currentUrl, previous) => (currentUrl.href !== previous.url?.href
|| options.prefixUrl.toString() !== previous.prefixUrl
|| options.allowAbsoluteUrls !== previous.allowAbsoluteUrls);
export function applyUrlOverride(options, url, { username, password, baseUrl } = {}) {
assertRelativeUrlIfNeeded(options, url);
if (is.string(url) && options.url) {
const resolvedUrl = new URL(url, baseUrl ?? options.url);
url = resolvedUrl.href;
}
if (options.allowAbsoluteUrls) {
options.prefixUrl = '';
options.url = url;
}
else {
const { allowAbsoluteUrls } = options;
try {
options.allowAbsoluteUrls = true;
options.url = url;
}
finally {
options.allowAbsoluteUrls = allowAbsoluteUrls;
}
}
if (username !== undefined) {
options.username = username;
}
if (password !== undefined) {
options.password = password;
}
return options.url;
}
function assertValidHeaderName(name) {
if (name.startsWith(':')) {
throw new TypeError(`HTTP/2 pseudo-headers are not supported in \`options.headers\`: ${name}`);
}
// Assigning it would re-prototype the live headers object instead of adding a header.
if (name === '__proto__') {
throw new TypeError('The `__proto__` header name is not supported in `options.headers`');
}
}
/**
Safely assign own properties from source to target, skipping `__proto__` to prevent prototype pollution from JSON.parse'd input.
*/
function safeObjectAssign(target, source) {
for (const [key, value] of Object.entries(source)) {
if (key === '__proto__') {
continue;
}
Reflect.set(target, key, value);
}
}
// `__proto__` is skipped like in `safeObjectAssign`, which never assigns it.
function assertKnownKeys(value, knownOptions, createError) {
for (const key of Object.keys(value)) {
if (key === '__proto__') {
continue;
}
if (!Object.hasOwn(knownOptions, key)) {
throw createError(key);
}
}
}
const isToughCookieJar = (cookieJar) => cookieJar.setCookie.length === 4 && cookieJar.getCookieString.length === 0;
function validateSearchParameters(searchParameters) {
for (const [key, value] of Object.entries(searchParameters)) {
if (key === '__proto__') {
continue;
}
assertAny(`searchParams.${key}`, [is.string, is.number, is.boolean, is.null, is.undefined], value);
}
}
const globalCache = new Map();
const dnsCacheLookups = new WeakMap();
const adaptedDnsLookups = new WeakMap();
let globalDnsCache;
const getGlobalDnsCache = () => {
globalDnsCache ??= new DnsCache();
return globalDnsCache;
};
// Node asks a lookup for every address when it selects a family itself, so a lookup written
// against the documented single-result shape has to be adapted.
const getAdaptedLookup = (lookup) => {
let adapted = adaptedDnsLookups.get(lookup);
if (!adapted) {
adapted = (hostname, options, callback) => {
lookup(hostname, options, (error, address, family) => {
if (error) {
callback(error, address, family);
return;
}
callback(null, options.all && !Array.isArray(address) ? [{ address, family: family }] : address, family);
});
};
adaptedDnsLookups.set(lookup, adapted);
}
return adapted;
};
const getDnsCacheLookup = (cache) => {
let lookup = dnsCacheLookups.get(cache);
if (!lookup) {
// Preserve the cache receiver and stable lookup identity for HTTP/2 session reuse.
lookup = (hostname, options, callback) => {
cache.lookup(hostname, options, callback);
};
dnsCacheLookups.set(cache, lookup);
}
return lookup;
};
// Detects and wraps QuickLRU v7+ instances to make them compatible with the StorageAdapter interface
const wrapQuickLruIfNeeded = (value) => {
// Check if this is QuickLRU v7+ using Symbol.toStringTag and the evict method (added in v7)
if (value?.[Symbol.toStringTag] === 'QuickLRU' && typeof value.evict === 'function') {
// QuickLRU v7+ uses set(key, value, {maxAge: number}) but StorageAdapter expects set(key, value, ttl)
// Wrap it to translate the interface
return {
get(key) {
return value.get(key);
},
set(key, cacheValue, ttl) {
if (ttl === undefined) {
value.set(key, cacheValue);
}
else {
value.set(key, cacheValue, { maxAge: ttl });
}
return true;
},
delete(key) {
return value.delete(key);
},
clear() {
return value.clear();
},
has(key) {
return value.has(key);
},
};
}
// QuickLRU v5 and other caches work as-is
return value;
};
const defaultInternals = {
request: undefined,
agent: {
http: undefined,
https: undefined,
http2: undefined,
},
h2session: undefined,
decompress: true,
timeout: {
connect: undefined,
lookup: undefined,
read: undefined,
request: undefined,
response: undefined,
secureConnect: undefined,
send: undefined,
socket: undefined,
},
prefixUrl: '',
body: undefined,
form: undefined,
json: undefined,
cookieJar: undefined,
ignoreInvalidCookies: false,
searchParams: undefined,
dnsLookup: undefined,
dnsCache: undefined,
context: {},
hooks: {
init: [],
beforeRequest: [],
beforeError: [],
beforeRedirect: [],
beforeRetry: [],
beforeCache: [],
afterResponse: [],
},
followRedirect: true,
maxRedirects: 10,
cache: undefined,
throwHttpErrors: true,
username: '',
password: '',
http2: false,
allowGetBody: false,
allowAbsoluteUrls: true,
copyPipedHeaders: false,
headers: {
'user-agent': 'got (https://github.com/sindresorhus/got)',
},
methodRewriting: false,
dnsLookupIpVersion: undefined,
parseJson: JSON.parse,
stringifyJson: JSON.stringify,
retry: {
limit: 2,
methods: [
'GET',
'PUT',
'HEAD',
'DELETE',
'OPTIONS',
'TRACE',
'QUERY',
],
statusCodes: [
408,
413,
429,
500,
502,
503,
504,
521,
522,
524,
],
errorCodes: [
'ETIMEDOUT',
'ECONNRESET',
'EADDRINUSE',
'ECONNREFUSED',
'EPIPE',
'ENOTFOUND',
'ENETUNREACH',
'EAI_AGAIN',
],
maxRetryAfter: undefined,
calculateDelay: ({ computedValue }) => computedValue,
backoffLimit: Infinity,
noise: 100,
enforceRetryRules: true,
},
localAddress: undefined,
method: 'GET',
createConnection: undefined,
cacheOptions: {
shared: undefined,
cacheHeuristic: undefined,
immutableMinTimeToLive: undefined,
ignoreCargoCult: undefined,
},
https: {
alpnProtocols: undefined,
rejectUnauthorized: undefined,
checkServerIdentity: undefined,
serverName: undefined,
certificateAuthority: undefined,
key: undefined,
certificate: undefined,
passphrase: undefined,
pfx: undefined,
ciphers: undefined,
honorCipherOrder: undefined,
minVersion: undefined,
maxVersion: undefined,
signatureAlgorithms: undefined,
tlsSessionLifetime: undefined,
dhparam: undefined,
ecdhCurve: undefined,
certificateRevocationLists: undefined,
secureOptions: undefined,
},
encoding: undefined,
resolveBodyOnly: false,
isStream: false,
responseType: 'text',
url: undefined,
pagination: {
transform(response) {
if (response.request.options.responseType === 'json') {
return response.body;
}
const body = response.request.options.responseType === 'buffer'
? decodeUint8Array(response.body, response.request.options.encoding)
: response.body;
return response.request.options.parseJson(body);
},
paginate({ response }) {
const rawLinkHeader = response.headers.link;
if (typeof rawLinkHeader !== 'string' || rawLinkHeader.trim() === '') {
return false;
}
const parsed = parseLinkHeader(rawLinkHeader);
// The `rel` parameter is a space-separated list of relation types compared case-insensitively. See https://www.rfc-editor.org/rfc/rfc8288#section-3.3
// Ignore links with an anchor instead of changing their context (RFC 8288, section 3.2).
const next = parsed.find(entry => !Object.hasOwn(entry.parameters, 'anchor') && entry.parameters.rel?.replaceAll('"', '').toLowerCase().split(/\s+/v).includes('next'));
if (next) {
return {
url: next.reference,
};
}
return false;
},
filter: () => true,
shouldContinue: () => true,
countLimit: Infinity,
backoff: 0,
requestLimit: 10_000,
stackAllItems: false,
},
setHost: true,
maxHeaderSize: undefined,
signal: undefined,
enableUnixSockets: false,
strictContentLength: true,
};
const cloneHeaderValue = (value) => {
if (Array.isArray(value)) {
return [...value];
}
return typeof value === 'number' ? String(value) : value;
};
// `__proto__` is skipped so a `JSON.parse`d headers object cannot become a request header.
const cloneHeaders = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name !== '__proto__').map(([name, value]) => [name, cloneHeaderValue(value)]));
// TLS option lists and key/PFX descriptors must not be shared between requests.
const cloneHttps = (https) => Object.fromEntries(Object.entries(https).map(([key, value]) => [key, Array.isArray(value) ? value.map(item => is.plainObject(item) ? { ...item } : item) : value]));
const cloneInternals = (internals) => {
const { hooks, retry } = internals;
const result = {
...internals,
url: internals.url ? new URL(internals.url) : undefined,
context: { ...internals.context },
cacheOptions: { ...internals.cacheOptions },
https: cloneHttps(internals.https),
agent: { ...internals.agent },
headers: cloneHeaders(internals.headers),
retry: {
...retry,
errorCodes: [...retry.errorCodes],
methods: [...retry.methods],
statusCodes: [...retry.statusCodes],
},
timeout: { ...internals.timeout },
hooks: Object.fromEntries(Object.entries(hooks).map(([event, handlers]) => [event, [...handlers]])),
searchParams: internals.searchParams ? new URLSearchParams(internals.searchParams) : undefined,
pagination: { ...internals.pagination },
};
return result;
};
const cloneRaw = (raw) => {
const result = { ...raw };
// The result is a copy that is meant to be filled in, so assigning onto it is the point.
// eslint-disable-next-line unicorn/no-immediate-mutation
if (Object.hasOwn(raw, 'https') && is.object(raw.https)) {
result.https = cloneHttps(raw.https);
}
for (const key of ['context', 'cacheOptions', 'agent', 'timeout', 'pagination']) {
if (Object.hasOwn(raw, key) && is.object(raw[key])) {
result[key] = { ...raw[key] };
}
}
if (Object.hasOwn(raw, 'headers') && is.object(raw.headers)) {
result.headers = cloneHeaders(raw.headers);
}
if (Object.hasOwn(raw, 'retry') && is.object(raw.retry)) {
const { retry } = raw;
result.retry = { ...retry };
if (is.array(retry.errorCodes)) {
result.retry.errorCodes = [...retry.errorCodes];
}
if (is.array(retry.methods)) {
result.retry.methods = [...retry.methods];
}
if (is.array(retry.statusCodes)) {
result.retry.statusCodes = [...retry.statusCodes];
}
}
if (Object.hasOwn(raw, 'hooks') && is.object(raw.hooks)) {
const { hooks } = raw;
result.hooks = {
...hooks,
};
for (const event of Object.keys(defaultInternals.hooks)) {
const handlers = hooks[event];
if (is.array(handlers)) {
// @ts-expect-error Indexing by a widened `keyof Hooks` loses the correlation to `handlers`'s specific array type.
result.hooks[event] = [...handlers];
}
}
}
if (Object.hasOwn(raw, 'searchParams') && raw.searchParams) {
if (is.string(raw.searchParams)) {
result.searchParams = raw.searchParams;
}
else if (raw.searchParams instanceof URLSearchParams) {
result.searchParams = new URLSearchParams(raw.searchParams);
}
else {
// A `URL`, `Map` or `Set` is an object without enumerable own keys, so it has to be
// rejected before the spread turns it into an empty object.
assertPlainObject('searchParams', raw.searchParams);
result.searchParams = { ...raw.searchParams };
}
}
return result;
};
// The HTTP/2 client uses one timer for these phases, so the smallest value decides both when
// it fires and which phase the resulting error reports. Delays Node.js cannot arm mean no
// constraint and are excluded instead of winning the minimum.
const getHttp2Timeout = (internals) => {
const delays = [
['lookup', internals.timeout.lookup],
['connect', internals.timeout.connect],
['secureConnect', internals.timeout.secureConnect],
['request', internals.timeout.request],
].filter((entry) => typeof entry[1] === 'number' && isArmableTimerDelay(entry[1]));
if (delays.length === 0) {
return undefined;
}
let lowest = delays[0];
for (const entry of delays) {
if (entry[1] < lowest[1]) {
lowest = entry;
}
}
return { timeout: lowest[1], phase: lowest[0] };
};
const usesHttp2Alpn = (internals, url) => {
const usesCustomHttpsAgent = internals.agent.https !== undefined
&& internals.agent.https !== false
&& internals.createConnection === undefined;
return internals.http2
&& url.protocol === 'https:'
&& !internals.h2session
&& !usesCustomHttpsAgent;
};
const trackReplacedHeaderMutations = (trackedStateMutations, previousHeaders, nextHeaders) => {
if (!trackedStateMutations) {
return;
}
for (const header of new Set([...Object.keys(previousHeaders), ...Object.keys(nextHeaders)])) {
if (previousHeaders[header] !== nextHeaders[header]) {
trackedStateMutations.add(header);
}
}
};
const init = (options, withOptions, self) => {
const initHooks = options.hooks?.init;
if (initHooks) {
for (const hook of initHooks) {
hook(withOptions, self);
}
}
};
// Keys never merged: got.extend() internals, url (passed as first arg), control flags, security
const nonMergeableKeys = new Set(['mutableDefaults', 'handlers', 'url', 'preserveHooks', 'isStream', '__proto__']);
// Options where an explicit `undefined` resets the inherited value instead of keeping it.
const resettableByUndefined = new Set(['searchParams', 'cookieJar']);
// Whether the options supply a body, JSON, or form payload explicitly.
const hasExplicitBodyOption = (options) => (Object.hasOwn(options, 'body') && options.body !== undefined)
|| (Object.hasOwn(options, 'json') && options.json !== undefined)
|| (Object.hasOwn(options, 'form') && options.form !== undefined);
export default class Options {
#internals;
#headersProxy;
#merging = false;
#init;
#explicitHeaders;
#trackedStateMutations;
constructor(input, options, defaults) {
assertAny('input', [is.string, is.urlInstance, is.object, is.undefined], input);
assertAny('options', [is.object, is.undefined], options);
assertAny('defaults', [is.object, is.undefined], defaults);
if (input instanceof Options || options instanceof Options) {
throw new TypeError('The defaults must be passed as the third argument');
}
if (defaults) {
this.#internals = cloneInternals(defaults.#internals);
this.#init = [...defaults.#init];
this.#explicitHeaders = new Set(defaults.#explicitHeaders);
}
else {
this.#internals = cloneInternals(defaultInternals);
this.#init = [];
this.#explicitHeaders = new Set();
}
this.#headersProxy = this.#createHeadersProxy();
// This rule allows `finally` to be considered more important.
// Meaning no matter the error thrown in the `try` block,
// if `finally` throws then the `finally` error will be thrown.
//
// Yes, we want this. If we set `url` first, then the `url.searchParams`
// would get merged. Instead we set the `searchParams` first, then
// `url.searchParams` is overwritten as expected.
//
/* eslint-disable no-unsafe-finally -- `finally` is used intentionally here to ensure `url` is always set last, overwriting any merged searchParams */
try {
if (is.plainObject(input)) {
// The plain object carries the url, so one in the second argument would be
// silently ignored, the same conflict the string input reports.
if (options?.url !== undefined) {
throw new TypeError('The `url` option is mutually exclusive with the `input` argument');
}
try {
this.merge(input);
this.merge(options);
}
finally {
this.url = input.url;
}
}
else {
try {
this.merge(options);
}
finally {
if (options?.url !== undefined) {
if (input === undefined) {
this.url = options.url;
}
else {
throw new TypeError('The `url` option is mutually exclusive with the `input` argument');
}
}
else if (input !== undefined) {
this.url = input;
}
}
}
}
catch (error) {
const optionsError = normalizeError(error);
optionsError.options = this;
throw optionsError;
}
/* eslint-enable no-unsafe-finally */
}
merge(options) {
if (!options) {
return;
}
if (options instanceof Options) {
// Create a copy of the #init array to avoid infinite loop
// when merging an Options instance with itself
const initArray = [...options.#init];
for (const init of initArray) {
this.merge(init);
}
return;
}
options = cloneRaw(options);
init(this, options, this);
init(options, options, this);
this.#merging = true;
try {
let push = false;
for (const key of Object.keys(options)) {
if (nonMergeableKeys.has(key)) {
continue;
}
if (!optionNames.has(key)) {
throw new Error(`Unexpected option: ${key}`);
}
// @ts-expect-error Type 'unknown' is not assignable to type 'never'.
const value = options[key];
if (value === undefined && !resettableByUndefined.has(key)) {
continue;
}
// @ts-expect-error Type 'unknown' is not assignable to type 'never'.
this[key] = value;
push = true;
}
if (push) {
this.#init.push(options);
}
}
finally {
this.#merging = false;
}
}
/**
Custom request function.
@default Got's built-in HTTP/1.1 or HTTP/2 request implementation
*/
get request() {
return this.#internals.request;
}
set request(value) {
assertAny('request', [is.function, is.undefined], value);
this.#internals.request = value;
}
/**
An object representing `http`, `https` and `http2` keys for [`http.Agent`](https://nodejs.org/api/http.html#http_class_http_agent), [`https.Agent`](https://nodejs.org/api/https.html#https_class_https_agent), and Got's internal HTTP/2 session pool.
This is necessary because a request to one protocol might redirect to another.
In such a scenario, Got will switch over to the right protocol agent for you.
When `http2` is enabled, a custom `agent.https` instance makes Got use the native HTTP/1.1 request path because Got's built-in HTTP/2 session pool does not support custom HTTPS agents.
If a key is not present, it will default to a global agent.
@example
```
import got from 'got';
import HttpAgent from 'agentkeepalive';
const {HttpsAgent} = HttpAgent;
await got('https://sindresorhus.com', {
agent: {
http: new HttpAgent(),
https: new HttpsAgent()
}
});
```
*/
get agent() {
return this.#internals.agent;
}
set agent(value) {
assertPlainObject('agent', value);
for (const key of Object.keys(value)) {
if (key === '__proto__') {
continue;
}
if (!Object.hasOwn(defaultInternals.agent, key)) {
throw new TypeError(`Unexpected agent option: ${key}`);
}
const validators = key === 'http2'
? [is.undefined, (v) => v === false]
: [is.object, is.undefined, (v) => v === false];
assertAny(`agent.${key}`, validators, value[key]);
}
this.#mergeOrReplace('agent', value);
}
get h2session() {
return this.#internals.h2session;
}
set h2session(value) {
assertAny('h2session', [is.object, is.undefined], value);
this.#internals.h2session = value;
}
/**
Decompress the response automatically.
This will set the `accept-encoding` header to `gzip, deflate, br` unless you set it yourself.
If this is disabled, a compressed response is returned as a `Uint8Array`.
This may be useful if you want to handle decompression yourself or stream the raw compressed data.
@default true
*/
get decompress() {
return this.#internals.decompress;
}
set decompress(value) {
assert.boolean(value);
this.#internals.decompress = value;
}
/**
Milliseconds to wait for the server to end the response before aborting the request with `got.TimeoutError` error (a.k.a. `request` property).
By default, there's no timeout.
This also accepts an `object` with the following fields to constrain the duration of each phase of the request lifecycle:
- `lookup` starts when a socket is assigned and ends when the hostname has been resolved.
Does not apply when using a Unix domain socket.
- `connect` starts when `lookup` completes (or when the socket is assigned if lookup does not apply to the request) and ends when the socket is connected.
- `secureConnect` starts when `connect` completes and ends when the handshaking process completes (HTTPS only).
- `socket` starts when the socket is connected. See [request.setTimeout](https://nodejs.org/api/http.html#http_request_settimeout_timeout_callback).
- `response` starts when the request has been written to the socket and ends when the response headers are received.
- `send` starts when the socket is connected and ends with the request has been written to the socket.
- `request` starts when the request is initiated and ends when the response's end event fires.
*/
get timeout() {
// We always return `Delays` here.
// It has to be `Delays | number`, otherwise TypeScript will error because the getter and the setter have incompatible types.
return this.#internals.timeout;
}
set timeout(value) {
assertPlainObject('timeout', value);
for (const key of Object.keys(value)) {
if (key === '__proto__') {
continue;
}
if (!Object.hasOwn(defaultInternals.timeout, key)) {
throw new Error(`Unexpected timeout option: ${key}`);
}
assertAny(`timeout.${key}`, [is.number, is.undefined], value[key]);
}
this.#mergeOrReplace('timeout', value);
}
/**
When specified, `prefixUrl` will be prepended to relative string `url` input.
The prefix must be an absolute URL, since Got has no base to resolve it against. It must not contain a query string or a fragment.
A trailing slash `/` is optional - one will be added automatically.
__Note__: Absolute string URLs and URL instances bypass `prefixUrl` by default. Other instance defaults, including headers, still apply. For untrusted URLs, set `allowAbsoluteUrls` to `false`.
__Note__: Got cannot know which custom headers are sensitive. If you use headers like `x-api-key`, only pass trusted URLs or use `allowAbsoluteUrls: false`.
__Note__: Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion.
For example, when the prefix URL is `https://example.com/foo` and the input is `/bar`, there's ambiguity whether the resulting URL would become `https://example.com/foo/bar` or `https://example.com/bar`.
The latter is used by browsers.
__Tip__: Useful when used with `got.extend()` to create niche-specific Got instances.
__Tip__: You can change `prefixUrl` using hooks as long as the URL still includes the `prefixUrl`.
If the URL doesn't include it anymore, it will throw.
@example
```
import got from 'got';
await got('unicorn', {prefixUrl: 'https://cats.com'});
//=> 'https://cats.com/unicorn'
const instance = got.extend({
prefixUrl: 'https://google.com'
});
await instance('unicorn', {
hooks: {
beforeRequest: [
options => {
options.prefixUrl = 'https://cats.com';
}
]
}
});
//=> 'https://cats.com/unicorn'
```
*/
get prefixUrl() {
// We always return `string` here.
// It has to be `string | URL`, otherwise TypeScript will error because the getter and the setter have incompatible types.
return this.#internals.prefixUrl;
}
set prefixUrl(value) {
assertAny('prefixUrl', [is.string, is.urlInstance], value);
if (value === '') {
this.#internals.prefixUrl = '';
return;
}
value = value.toString();
if (hasProtocolSlashes(value)) {
// The prefix is concatenated with the path, so a query or a fragment would swallow it.
// A scheme-relative prefix has no base to parse against and fails at request time.
const prefix = new URL(value);
if (prefix.search !== '' || prefix.hash !== '') {
throw new TypeError('The `prefixUrl` option must not contain a query string or a fragment');
}
}
if (!value.endsWith('/')) {
value += '/';
}
if (this.#internals.prefixUrl && this.#internals.url) {
const url = this.#internals.url;
const previousUrl = new URL(url);
const { username, password } = url;
const urlWithoutCredentials = new URL(url);
urlWithoutCredentials.username = '';
urlWithoutCredentials.password = '';
let prefixUrlWithoutCredentials = this.#internals.prefixUrl.toString();
let hasNewPrefixCredentials = false;
if (isAbsoluteUrl(value)) {
const nextPrefixUrl = new URL(value);
hasNewPrefixCredentials = nextPrefixUrl.username !== '' || nextPrefixUrl.password !== '';
}
if (isAbsoluteUrl(this.#internals.prefixUrl)) {
const prefixUrl = new URL(this.#internals.prefixUrl);
prefixUrl.username = '';
prefixUrl.password = '';
prefixUrlWithoutCredentials = prefixUrl.href;
}
if (!urlWithoutCredentials.href.startsWith(prefixUrlWithoutCredentials)) {
throw new Error('The `url` option must include the `prefixUrl` option');
}
url.href = value + urlWithoutCredentials.href.slice(prefixUrlWithoutCredentials.length);
const isSameOriginUrl = isSameOrigin(previousUrl, url);
if (username && !url.username && isSameOriginUrl && !hasNewPrefixCredentials) {
url.username = username;
}
if (password && !url.password && isSameOriginUrl && !hasNewPrefixCredentials) {
url.password = password;
}
}
this.#internals.prefixUrl = value;
this.#trackedStateMutations?.add('prefixUrl');
}
/**
__Note #1__: The `body` option cannot be used with the `json` or `form` option.
__Note #2__: If you provide this option, `got.stream()` will be read-only.
__Note #3__: If you provide a payload with the `GET` or `HEAD` method, it will throw a `TypeError` unless the method is `GET` and the `allowGetBody` option is set to `true`.
__Note #4__: This option is not enumerable and will not be merged with the instance defaults.
The `content-length` header will be automatically set if `body` is a `string` or an `ArrayBufferView` (including `Uint8Array`, other typed arrays, and `DataView`), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
You can use `Iterable` and `AsyncIterable` objects as request body, including Web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
@example
```
import got from 'got';
// Using an async generator
async function* generateData() {
yield 'Hello, ';
yield 'world!';
}
await got.post('https://httpbin.org/anything', {
body: generateData()
});
```
*/
get body() {
return this.#internals.body;
}
set body(value) {
assertAny('body', [is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, ArrayBuffer.isView, is.undefined], value);
if (is.nodeStream(value)) {
assert.truthy(value.readable);
}
if (value !== undefined) {
assert.undefined(this.#internals.form);
assert.undefined(this.#internals.json);
}
this.#internals.body = value;
this.#trackedStateMutations?.add('body');
}
/**
The form body is converted to a query string using [`(new URLSearchParams(object)).toString()`](https://nodejs.org/api/url.html#url_constructor_new_urlsearchparams_obj).
If the `Content-Type` header is not present, it will be set to `application/x-www-form-urlencoded`.
__Note #1__: If you provide this option, `got.stream()` will be read-only.
__Note #2__: This option is not enumerable and will not be merged with the instance defaults.
*/
get form() {
return this.#internals.form;
}
set form(value) {
assertAny('form', [is.plainObject, is.undefined], value);
if (value !== undefined) {
assert.undefined(this.#internals.body);
assert.undefined(this.#internals.json);
}
this.#internals.form = value;
this.#trackedStateMutations?.add('form');
}
/**
JSON request body. If the `content-type` header is not set, it will be set to `application/json`.
__Important__: This option only affects the request body you send to the server. To parse the response as JSON, you must either call `.json()` on the promise or set `responseType: 'json'` in the options.
__Note #1__: If you provide this option, `got.stream()` will be read-only.
__Note #2__: This option is not enumerable and will not be merged with the instance defaults.
*/
get json() {
return this.#internals.json;
}
set json(value) {
if (value !== undefined) {
assert.undefined(this.#internals.body);
assert.undefined(this.#internals.form);
}
this.#internals.json = value;
this.#trackedStateMutations?.add('json');
}
/**
The URL to request, as a string or a [WHATWG `URL`](https://nodejs.org/api/url.html#url_class_url).
Pass it as the first argument to `got(url, options)`. Setting `url` inside an options object is not supported.
Properties from `options` will override properties in the parsed `url`.
If no protocol is specified, it will throw a `TypeError`.
__Note__: The query string is **not** parsed as search params.
@example
```
await got('https://example.com/?query=a b'); //=> https://example.com/?query=a%20b
await got('https://example.com/', {searchParams: {query: 'a b'}}); //=> https://example.com/?query=a+b
// The query string is overridden by `searchParams`
await got('https://example.com/?query=a b', {searchParams: {query: 'a b'}}); //=> https://example.com/?query=a+b
```
*/
get url() {
return this.#internals.url;
}
set url(value) {
assertAny('url', [is.string, is.urlInstance, is.undefined], value);
if (value === undefined) {
this.#internals.url = undefined;
this.#trackedStateMutations?.add('url');
return;
}
if (is.string(value) && value.startsWith('/')) {
throw new Error('`url` must not start with a slash');
}
const valueString = value.toString();
if (is.string(value)
&& !this.prefixUrl
// The URL parser strips leading C0 control characters and spaces, so the guard has
// to see the normalized string or whitespace would smuggle a protocol past it.
&& hasHttpProtocolWithoutSlashes(stripLeadingC0ControlOrSpace(removeAsciiTabOrNewline(value)))) {
throw new Error('`url` protocol must be followed by `//`');
}
// Detect if URL is already absolute.
const isAbsolute = isAbsoluteUrl(value);
assertRelativeUrlIfNeeded(this, value);
// Only concatenate prefixUrl if the URL is relative
const urlString = isAbsolute ? valueString : `${this.prefixUrl}${valueString}`;
const url = new URL(urlString);
// Validate before storing, so a rejected url never stays on the instance.
if (usesUnixSocket(url) && !this.#internals.enableUnixSockets) {
throw new Error('Using UNIX domain sockets but option `enableUnixSockets` is not enabled');
}
if (url.protocol === 'unix:') {
url.href = `http://unix${url.pathname}${url.search}`;
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
const error = new Error(`Unsupported protocol: ${url.protocol}`);
error.code = 'ERR_UNSUPPORTED_PROTOCOL';
throw error;
}
this.#internals.url = url;
this.#trackedStateMutations?.add('url');
if (this.#internals.username) {
url.username = this.#internals.username;
this.#internals.username = '';
}
if (this.#internals.password) {
url.password = this.#internals.password;
this.#internals.password = '';
}
if (this.#internals.searchParams) {
url.search = this.#internals.searchParams.toString();
this.#internals.searchParams = undefined;
}
}
/**
Cookie support. You don't have to care about parsing or how to store them.
__Note__: If you provide this option, `options.headers.cookie` will be overridden.
*/
get cookieJar() {
return this.#internals.cookieJar;
}
set cookieJar(value) {
assertAny('cookieJar', [is.object, is.undefined], value);
if (value === undefined) {
this.#internals.cookieJar = undefined;
return;
}
const { setCookie, getCookieString } = value;
assert.function(setCookie);
assert.function(getCookieString);
/* istanbul ignore next: Horrible `tough-cookie` v3 check */
if (isToughCookieJar(value)) {
this.#internals.cookieJar = {
setCookie: promisify(value.setCookie.bind(value)),
getCookieString: promisify(value.getCookieString.bind(value)),
};
}
else {
this.#internals.cookieJar = value;
}
}
/**
You can abort the `request` using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
@example
```
import got from 'got';
const abortController = new AbortController();
const request = got('https://httpbin.org/anything', {
signal: abortController.signal
});
setTimeout(() => {
abortController.abort();
}, 100);
```
*/
get signal() {
return this.#internals.signal;
}
set signal(value) {
assertAny('signal', [is.object, is.undefined], value);
this.#internals.signal = value;
}
/**
Ignore invalid cookies instead of throwing an error.
Only useful when the `cookieJar` option has been set. Not recommended.
@default false
*/
get ignoreInvalidCookies() {
return this.#internals.ignoreInvalidCookies;
}
set ignoreInvalidCookies(value) {
assert.boolean(value);
this.#internals.ignoreInvalidCookies = value;
}
/**
Query string that will be added to the request URL.
This will override the query string in `url`.
If you need to pass in an array, you can do it using a `URLSearchParams` instance.
@example
```
import got from 'got';
const searchParams = new URLSearchParams([['key', 'a'], ['key', 'b']]);
await got('https://example.com', {searchParams});
console.log(searchParams.toString());
//=> 'key=a&key=b'
```
*/
get searchParams() {
if (Object.isFrozen(this.#internals)) {
// A frozen instance is read-only, so a copy is handed out. Writing to it is
// discarded instead of reaching the live parameters or the url they belong to.
const { url } = this.#internals;
return new URLSearchParams(url ? url.searchParams : this.#internals.searchParams);
}
if (this.#internals.url) {
return this.#internals.url.searchParams;
}
this.#internals.searchParams ??= new URLSearchParams();
return this.#internals.searchParams;
}
set searchParams(value) {
assertAny('searchParams', [is.string, is.object, is.undefined], value);
// A frozen instance is read-only, and the url it holds is part of that state. Without
// this the assignment below would rewrite the query string of a frozen url instead of
// failing like every other frozen option does.
if (Object.isFrozen(this.#internals)) {
throw new TypeError('Cannot assign to read only option `searchParams`');
}
const url = this.#internals.url;
if (value === undefined) {
this.#internals.searchParams = undefined;
if (url) {
url.search = '';
}
return;
}
const searchParameters = this.searchParams;
let updated;
if (is.string(value) || value instanceof URLSearchParams) {
// A `URLSearchParams` is cloned so the caller-owned object is not stored by reference.
updated = new URLSearchParams(value);
}
else {
// A `URL`, `Map` or `Set` is an object without enumerable own keys, which would
// silently erase the query string instead of being reported.
assertPlainObject('searchParams', value);
validateSearchParameters(value);
updated = new URLSearchParams();
for (const [key, entry] of Object.entries(value)) {
if (key === '__proto__') {
continue;
}
if (entry === null) {
updated.append(key, '');
}
else if (entry === undefined) {
searchParameters.delete(key);
}
else {
updated.append(key, entry);
}
}
}
if (this.#merging) {
// These keys will be replaced
for (const key of updated.keys()) {
searchParameters.delete(key);
}
for (const [key, value] of updated) {
searchParameters.append(key, value);
}
}
else if (url) {
// Overrides the query string in the URL.
url.search = updated.toString();
}
else {
this.#internals.searchParams = updated;
}
}
get dnsLookup() {
return this.#internals.dnsLookup;
}
set dnsLookup(value) {
assertAny('dnsLookup', [is.function, is.undefined], value);
this.#internals.dnsLookup = value;
}
/**
A DNS cache instance used for making DNS lookups.
Useful when making lots of requests to different *public* hostnames.
Set to `true` to use Got's shared DNS cache.
When using `got.extend()`, set to `false` to opt out of a DNS cache configured by the parent instance.
Got's built-in DNS cache uses `dns.resolve4(…)` and `dns.resolve6(…)` under the hood and falls back to `dns.lookup(…)` when no DNS records are found, which may lead to additional delay.
Because it resolves A and AAAA records separately, it cannot preserve OS-specific `verbatim` address ordering from `dns.lookup(…)`.
If present, `clear(hostname?)` can be called by user code to clear cached entries.
__Note__: This should stay disabled when making requests to internal hostnames such as `localhost`, `database.local` etc.
@default undefined
*/
get dnsCache() {
return this.#internals.dnsCache;
}
set dnsCache(value) {
assertAny('dnsCache', [is.object, is.boolean, is.undefined], value);
if (value === true) {
this.#internals.dnsCache = getGlobalDnsCache();
}
else if (value === false) {
this.#internals.dnsCache = undefined;
}
else {
if (value !== undefined) {
assertAny('dnsCache.lookup', [is.function], value.lookup);
assertAny('dnsCache.clear', [is.function, is.undefined], value.clear);
}
this.#internals.dnsCache = value;
}
}
/**
User data. `context` is shallow merged and enumerable. If it contains non-enumerable properties they will NOT be merged.
@example
```
import got from 'got';
const instance = got.extend({
hooks: {
beforeRequest: [
options => {
if (!options.context || !options.context.token) {
throw new Error('Token required');
}
options.headers.token = options.context.token;
}
]
}
});
const context = {
token: 'secret'
};
const response = await instance('https://httpbin.org/headers', {context});
// Let's see the headers
console.log(response.body);
```
*/
get context() {
return this.#internals.context;
}
set context(value) {
assert.object(value);
this.#mergeOrReplace('context', value);
}
/**
Hooks allow modifications during the request lifecycle.
Hook functions may be async and are run serially.
*/
get hooks() {
return this.#internals.hooks;
}
set hooks(value) {
assert.object(value);
for (const knownHookEvent of Object.keys(value)) {
if (knownHookEvent === '__proto__') {
continue;
}
if (!Object.hasOwn(this.#internals.hooks, knownHookEvent)) {
throw new Error(`Unexpected hook event: ${knownHookEvent}`);
}
const typedKnownHookEvent = knownHookEvent;
const hooks = value[typedKnownHookEvent];
assertAny(`hooks.${knownHookEvent}`, [is.array, is.undefined], hooks);
// An `undefined` value is ignored, matching every other option.
if (!hooks) {
continue;
}
for (const hook of hooks) {
assert.function(hook);
}
if (this.#merging) {
// @ts-expect-error Indexing by a widened `keyof Hooks` loses the correlation to `hooks`'s specific array type.
this.#internals.hooks[typedKnownHookEvent].push(...hooks);
}
else {
// @ts-expect-error Indexing by a widened `keyof Hooks` loses the correlation to `hooks`'s specific array type.
this.#internals.hooks[knownHookEvent] = [...hooks];
}
}
}
/**
Whether redirect responses should be followed automatically.
Optionally, pass a function to dynamically decide based on the response object. The function is only called for 3xx responses, and an unchanged function is evaluated only once per response.
Note that if a `303` is sent by the server in response to any request type (`POST`, `DELETE`, etc.), Got will automatically request the resource pointed to in the location header via `GET`.
This is in accordance with [the spec](https://tools.ietf.org/html/rfc7231#section-6.4.4). You can optionally turn on this behavior also for other redirect codes - see `methodRewriting`.
On cross-origin redirects, Got strips `host`, `cookie`, `cookie2`, `authorization`, and `proxy-authorization`. When a redirect rewrites the request to `GET`, Got also strips request body headers. Use `hooks.beforeRedirect` for app-specific sensitive headers.
@default true
*/
get followRedirect() {
return this.#internals.followRedirect;
}
set followRedirect(value) {
assertAny('followRedirect', [is.boolean, is.function], value);
this.#internals.followRedirect = value;
}
/**
If exceeded, the request will be aborted and a `MaxRedirectsError` will be thrown.
@default 10
*/
get maxRedirects() {
return this.#internals.maxRedirects;
}
set maxRedirects(value) {
assertCount('maxRedirects', value);
this.#internals.maxRedirects = value;
}
/**
A cache adapter instance for storing cached response data.
@default undefined
*/
get cache() {
return this.#internals.cache;
}
set cache(value) {
assertAny('cache', [is.object, is.string, is.boolean, is.undefined], value);
if (value === true) {
this.#internals.cache = globalCache;
}
else if (value === false) {
this.#internals.cache = undefined;
}
else {
this.#internals.cache = wrapQuickLruIfNeeded(value);
}
}
/**
Determines if a `got.HTTPError` is thrown for unsuccessful responses.
If this is disabled, requests that encounter an error status code will be resolved with the `response` instead of throwing.
This may be useful if you are checking for resource availability and are expecting error responses.
@default true
*/
get throwHttpErrors() {
return this.#internals.throwHttpErrors;
}
set throwHttpErrors(value) {
assert.boolean(value);
this.#internals.throwHttpErrors = value;
}
get username() {
return this.#getCredential('username');
}
set username(value) {
assert.string(value);
this.#setCredential('username', value);
}
get password() {
return this.#getCredential('password');
}
set password(value) {
assert.string(value);
this.#setCredential('password', value);
}
/**
If set to `true`, Got will additionally accept HTTP/2 requests.
It will choose either HTTP/1.1 or HTTP/2 depending on the ALPN protocol. When a custom `agent.https` instance is set, Got uses that native HTTPS agent directly and skips HTTP/2 negotiation.
__Note__: If `options.request` returns a request or response, it controls the transport and Got's HTTP/2 client is bypassed. Return `undefined` to fall back to Got's built-in transport.
@default false
@example
```
import got from 'got';
const {headers} = await got('https://nghttp2.org/httpbin/anything', {http2: true});
console.log(headers.via);
//=> '2 nghttpx'
```
*/
get http2() {
return this.#internals.http2;
}
set http2(value) {
assert.boolean(value);
this.#internals.http2 = value;
}
/**
Set this to `true` to allow sending body for the `GET` method.
This option is only meant to interact with non-compliant servers when you have no other choice.
__Note__: The [RFC 7231](https://tools.ietf.org/html/rfc7231#section-4.3.1) doesn't specify any particular behavior for the GET method having a payload, therefore __it's considered an [anti-pattern](https://en.wikipedia.org/wiki/Anti-pattern)__.
@default false
*/
get allowGetBody() {
return this.#internals.allowGetBody;
}
set allowGetBody(value) {
assert.boolean(value);
this.#internals.allowGetBody = value;
}
/**
Allow absolute URLs to bypass `prefixUrl`.
When set to `false` with `prefixUrl`, passing an absolute `url` will throw. This also rejects scheme-relative URL strings like `//example.com/path` in retry and pagination URL overrides. Use this when untrusted URL input must stay on the same origin as the configured `prefixUrl`. This is not a path sandbox: relative paths like `../other` still follow standard URL resolution on the same origin. Set `prefixUrl` to an empty string for a request that intentionally needs an absolute URL.
__Note__: This guards the `url` you pass. It does not block cross-origin redirects issued by the server, though inherited sensitive headers are still stripped when a redirect changes origin.
__Note__: The check is defeated if the same hook or `pagination.paginate(…)` return also sets `prefixUrl` or `allowAbsoluteUrls`. Do not populate those options from untrusted data.
@default true
*/
get allowAbsoluteUrls() {
return this.#internals.allowAbsoluteUrls;
}
set allowAbsoluteUrls(value) {
assert.boolean(value);
this.#internals.allowAbsoluteUrls = value;
}
/**
Automatically copy headers from piped streams.
When piping a request into a Got stream (e.g., `request.pipe(got.stream(url))`), this controls whether headers from the source stream are automatically merged into the Got request headers.
Note: Explicitly set headers take precedence over piped headers. Piped headers are only copied when a header is not already explicitly set.
Useful for proxy scenarios when explicitly enabled. Got automatically omits `host`, `authorization`, `cookie`, `cookie2`, `set-cookie`, `set-cookie2`, hop-by-hop headers, and headers nominated by `Connection`/`Proxy-Connection`. Got cannot know which app-specific headers are sensitive. Leave `copyPipedHeaders` disabled and copy only safe headers manually, or explicitly omit those headers before piping. If you trust the upstream and want to forward credentials, pass them explicitly in `headers`.
@default false
@example
```
import got from 'got';
import {pipeline} from 'node:stream/promises';
// Opt in to automatic header copying for proxy scenarios
server.get('/proxy', async (request, response) => {
const gotStream = got.stream('https://example.com', {
copyPipedHeaders: true,
// Explicit headers win over piped headers.
// Add credentials here only when the upstream is trusted.
headers: {
host: 'example.com',
}
});
await pipeline(request, gotStream, response);
});
```
@example
```
import got from 'got';
import {pipeline} from 'node:stream/promises';
// Keep it disabled and manually copy only safe headers
server.get('/proxy', async (request, response) => {
const gotStream = got.stream('https://example.com', {
headers: {
'user-agent': request.headers['user-agent'],
'accept': request.headers['accept'],
// Explicitly NOT copying host, connection, authorization, etc.
}
});
await pipeline(request, gotStream, response);
});
```
*/
get copyPipedHeaders() {
return this.#internals.copyPipedHeaders;
}
set copyPipedHeaders(value) {
assert.boolean(value);
this.#internals.copyPipedHeaders = value;
}
isHeaderExplicitlySet(name) {
return this.#explicitHeaders.has(name.toLowerCase());
}
shouldCopyPipedHeader(name) {
return !this.isHeaderExplicitlySet(name);
}
setPipedHeader(name, value) {
assertValidHeaderName(name);
this.#internals.headers[name.toLowerCase()] = value;
}
getInternalHeaders() {
return this.#internals.headers;
}
setInternalHeader(name, value) {
if (value === undefined) {
this.deleteInternalHeader(name);
return;
}
assertValidHeaderName(name);
this.#internals.headers[name.toLowerCase()] = value;
}
deleteInternalHeader(name) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.#internals.headers[name.toLowerCase()];
}
async trackStateMutations(operation) {
const changedState = new Set();
this.#trackedStateMutations = changedState;
try {
return await operation(changedState);
}
finally {
this.#trackedStateMutations = undefined;
}
}
clearBody() {
this.body = undefined;
this.json = undefined;
this.form = undefined;
for (const header of bodyHeaderNames) {
this.deleteInternalHeader(header);
}
}
clearUnchangedCookieHeader(previousState, changedState) {
if (previousState?.hadCookieJar
&& this.cookieJar === undefined
&& !this.isHeaderExplicitlySet('cookie')
&& !changedState?.has('cookie')
&& this.headers.cookie === previousState.headers.cookie) {
this.deleteInternalHeader('cookie');
}
}
restoreCookieHeader(previousState, headers) {
if (!previousState || Object.keys(headers ?? {}).some(name => name.toLowerCase() === 'cookie')) {
return;
}
if (previousState.cookieWasExplicitlySet) {
this.headers.cookie = previousState.headers.cookie;
return;
}
delete this.headers.cookie;
if (previousState.headers.cookie !== undefined) {
this.setInternalHeader('cookie', previousState.headers.cookie);
}
}
syncCookieHeaderAfterMerge(previousState, headers) {
this.restoreCookieHeader(previousState, headers);
this.clearUnchangedCookieHeader(previousState);
}
stripUnchangedCrossOriginState(previousState, changedState, { clearBody = true } = {}) {
const headers = this.getInternalHeaders();
const url = this.#internals.url;
for (const header of crossOriginStripHeaders) {
if (!changedState.has(header) && headers[header] === previousState.headers[header]) {
this.deleteInternalHeader(header);
}
}
for (const credential of ['username', 'password']) {
if (!hasExplicitCredentialInUrlChange(changedState, url, credential)) {
this[credential] = '';
}
}
if (clearBody && !hasBodyOptionChanged(changedState) && isBodyUnchanged(this, previousState)) {
this.clearBody();
}
}
/**
Strip sensitive headers and credentials when navigating to a different origin.
Headers and credentials explicitly provided in `userOptions` are preserved.
*/
stripSensitiveHeaders(previousUrl, nextUrl, userOptions) {
if (isSameOrigin(previousUrl, nextUrl)) {
return;
}
const headers = lowercaseKeys(userOptions.headers ?? {});
for (const header of crossOriginStripHeaders) {
if (headers[header] === undefined) {
this.deleteInternalHeader(header);
}
}
for (const credential of ['username', 'password']) {
const hasExplicitCredential = (Object.hasOwn(userOptions, credential) && userOptions[credential] !== undefined)
|| hasCredentialInUrl(userOptions.url, credential)
|| hasCredentialInUrl(userOptions.prefixUrl, credential)
|| isCrossOriginCredentialChanged(previousUrl, nextUrl, credential);
if (!hasExplicitCredential && this[credential] !== '') {
this[credential] = '';
}
}
}
/**
Merge the options for a follow-up request into `this` and apply the cross-origin rules. This is shared by `afterResponse` retries (when the hook passes a new options object) and by pagination.
Returns the resolved `nextUrl` when `updatedOptions.url` was supplied, so callers can run their own checks on it, and whether the caller supplied an explicit body.
*/
mergeNextRequestOptions(updatedOptions, { previousUrl, previousState, previousBoundary, baseUrl, }) {
const hasExplicitBody = hasExplicitBodyOption(updatedOptions);
const clearsCookieJar = Object.hasOwn(updatedOptions, 'cookieJar') && updatedOptions.cookieJar === undefined;
if (hasExplicitBody) {
const contentType = this.isHeaderExplicitlySet('content-type') ? this.headers['content-type'] : undefined;
this.clearBody();
this.setInternalHeader('content-type', contentType);
}
if (clearsCookieJar) {
this.cookieJar = undefined;
}
const { url, ...updatedOptionsWithoutUrl } = updatedOptions;
this.merge(updatedOptionsWithoutUrl);
this.syncCookieHeaderAfterMerge(previousState, updatedOptionsWithoutUrl.headers);
const currentUrl = this.url;
if (previousUrl
&& currentUrl instanceof URL
&& hasUrlOrPrefixUrlBoundaryChanged(this, currentUrl, previousBoundary)) {
assertUrlHasSameOriginAsPrefixUrlIfNeeded(this, currentUrl);
}
if (url === undefined
&& previousUrl
&& currentUrl instanceof URL
&& !isSameOrigin(previousUrl, currentUrl)) {
this.stripSensitiveHeaders(previousUrl, currentUrl, updatedOptions);
if (!hasExplicitBody) {
this.clearBody();
}
}
let nextUrl;
if (url !== undefined) {
nextUrl = applyUrlOverride(this, url, { ...updatedOptions, baseUrl });
// Explicit search parameters override the query string in the new URL.
if (updatedOptions.searchParams !== undefined) {
this.searchParams = updatedOptions.searchParams;
}
if (previousUrl) {
this.stripSensitiveHeaders(previousUrl, nextUrl, updatedOptions);
if (!isSameOrigin(previousUrl, nextUrl) && !hasExplicitBody) {
this.clearBody();
}
}
}
return { nextUrl, hasExplicitBody };
}
/**
Request headers.
Existing headers will be overwritten. Headers set to `undefined` will be omitted. Number values are stored and sent as strings.
@default {'user-agent': 'got (https://github.com/sindresorhus/got)'}
*/
get headers() {
return this.#headersProxy;
}
set headers(value) {
assertPlainObject('headers', value);
const normalizedHeaders = lowercaseKeys(cloneHeaders(value));
for (const header of Object.keys(normalizedHeaders)) {
assertValidHeaderName(header);
}
if (this.#merging) {
safeObjectAssign(this.#internals.headers, normalizedHeaders);
}
else {
const previousHeaders = this.#internals.headers;
this.#internals.headers = normalizedHeaders;
this.#headersProxy = this.#createHeadersProxy();
this.#explicitHeaders.clear();
trackReplacedHeaderMutations(this.#trackedStateMutations, previousHeaders, normalizedHeaders);
}
for (const header of Object.keys(normalizedHeaders)) {
this.#explicitHeaders.add(header);
if (this.#merging) {
this.#trackedStateMutations?.add(header);
}
}
}
/**
Specifies if the HTTP request method should be [rewritten as `GET`](https://tools.ietf.org/html/rfc7231#section-6.4) on redirects.
As the [specification](https://tools.ietf.org/html/rfc7231#section-6.4) prefers to rewrite the HTTP method only on `303` responses, this is Got's default behavior. Cross-origin `301` and `302` redirects also rewrite `POST` requests to `GET` by default to avoid forwarding request bodies to another origin.
Setting `methodRewriting` to `true` will also rewrite same-origin `301` and `302` responses, as allowed by the spec. This is the behavior followed by `curl` and browsers.
__Note__: Got never performs method rewriting on `307` and `308` responses, as this is [explicitly prohibited by the specification](https://www.rfc-editor.org/rfc/rfc7231#section-6.4.7).
@default false
*/
get methodRewriting() {
return this.#internals.methodRewriting;
}
set methodRewriting(value) {
assert.boolean(value);
this.#internals.methodRewriting = value;
}
/**
Indicates which DNS record family to use.
Values:
- `undefined`: IPv4 (if present) or IPv6
- `4`: Only IPv4
- `6`: Only IPv6
@default undefined
*/
get dnsLookupIpVersion() {
return this.#internals.dnsLookupIpVersion;
}
set dnsLookupIpVersion(value) {
if (value !== undefined && value !== 4 && value !== 6) {
throw new TypeError(`Invalid DNS lookup IP version: ${value}`);
}
this.#internals.dnsLookupIpVersion = value;
}
/**
A function used to parse JSON responses.
@example
```
import got from 'got';
import {parse} from '@hapi/bourne';
// Preventing prototype pollution by using Bourne
const parsed = await got('https://example.com', {
parseJson: text => parse(text)
}).json();
console.log(parsed);
```
*/
get parseJson() {
return this.#internals.parseJson;
}
set parseJson(value) {
assert.function(value);
this.#internals.parseJson = value;
}
/**
A function used to stringify the body of JSON requests.
@example
```
import got from 'got';
await got.post('https://example.com', {
stringifyJson: object => JSON.stringify(object, (key, value) => {
if (key.startsWith('_')) {
return;
}
return value;
}),
json: {
some: 'payload',
_ignoreMe: 1234
}
});
```
@example
```
import got from 'got';
await got.post('https://example.com', {
stringifyJson: object => JSON.stringify(object, (key, value) => {
if (typeof value === 'number') {
return value.toString();
}
return value;
}),
json: {
some: 'payload',
number: 1
}
});
```
*/
get stringifyJson() {
return this.#internals.stringifyJson;
}
set stringifyJson(value) {
assert.function(value);
this.#internals.stringifyJson = value;
}
/**
An object representing `limit`, `calculateDelay`, `methods`, `statusCodes`, `maxRetryAfter` and `errorCodes` fields for maximum retry count, retry handler, allowed methods, allowed status codes, maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time and allowed error codes.
Undefined settings preserve inherited values, except `maxRetryAfter`, where `undefined` restores the request timeout fallback.
Delays between retries are calculated with `Math.max(1, Math.min((2 ** (attemptCount - 1)) * 1000, backoffLimit) + noise)`, where `attemptCount` starts at 1 and `noise` is the randomized retry noise. Setting both `backoffLimit` and `noise` to `0` requests an immediate retry; returning `0` from `calculateDelay` still aborts the retry.
The `calculateDelay` property is a `function` that receives an object with `attemptCount`, `retryOptions`, `error` and `computedValue` properties for current retry count, the retry options, error and default computed value.
The function must return a delay in milliseconds (or a Promise resolving with it) (`0` return value cancels retry).
Delays above `2_147_483_647` milliseconds (Node.js's timer limit), including `Infinity`, abort the retry instead of triggering an immediate retry. This also applies to delays computed from `Retry-After`.
The `enforceRetryRules` property is a `boolean` that, when set to `true` (default), enforces the `limit`, `methods`, `statusCodes`, and `errorCodes` options before calling `calculateDelay`. Your `calculateDelay` function is only invoked when a retry is allowed based on these criteria. When `false`, `calculateDelay` receives the computed value but can override all retry logic.
__Note:__ When `enforceRetryRules` is `false`, you must check `computedValue` in your `calculateDelay` function to respect retry rules. When `true` (default), the retry rules are enforced automatically.
By default, it retries *only* on the specified methods, status codes, and on these network errors:
- `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
- `ECONNRESET`: Connection was forcibly closed by a peer.
- `EADDRINUSE`: Could not bind to any free port.
- `ECONNREFUSED`: Connection was refused by the server.
- `EPIPE`: The remote side of the stream being written has been closed.
- `ENOTFOUND`: Couldn't resolve the hostname to an IP address.
- `ENETUNREACH`: No internet connection.
- `EAI_AGAIN`: DNS lookup timed out.
__Note__: If `maxRetryAfter` is set to `undefined`, it will use `options.timeout`.
__Note__: If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header is greater than `maxRetryAfter`, it will cancel the request.
*/
get retry() {
return this.#internals.retry;
}
set retry(value) {
assertPlainObject('retry', value);
assertAny('retry.calculateDelay', [is.function, is.undefined], value.calculateDelay);
assertAny('retry.maxRetryAfter', [is.number, is.undefined], value.maxRetryAfter);
// `undefined` restores the inherited limit, so only a defined value is a count.
if (!is.undefined(value.limit)) {
assertCount('retry.limit', value.limit);
}
assertAny('retry.methods', [is.array, is.undefined], value.methods);
assertAny('retry.statusCodes', [is.array, is.undefined], value.statusCodes);
assertAny('retry.errorCodes', [is.array, is.undefined], value.errorCodes);
assertAny('retry.noise', [is.number, is.undefined], value.noise);
assertAny('retry.backoffLimit', [is.number, is.undefined], value.backoffLimit);
assertAny('retry.enforceRetryRules', [is.boolean, is.undefined], value.enforceRetryRules);
// The members are compared against a method name and a numeric status code, so a
// wrong type would silently disable the retry instead of being reported here.
for (const [key, predicate] of [['methods', is.string], ['statusCodes', is.number], ['errorCodes', is.string]]) {
for (const [index, member] of (value[key] ?? []).entries()) {
assertAny(`retry.${key}[${index}]`, [predicate], member);
}
}
if (value.noise && Math.abs(value.noise) > 100) {
throw new Error(`The maximum acceptable retry noise is +/- 100ms, got ${value.noise}`);
}
assertKnownKeys(value, this.#internals.retry, key => new Error(`Unexpected retry option: ${key}`));
// Keep the undefined reset for maxRetryAfter, which falls back to the request timeout.
const updatedRetry = Object.fromEntries(Object.entries(value).filter(([key, option]) => option !== undefined || key === 'maxRetryAfter'));
if (this.#merging) {
safeObjectAssign(this.#internals.retry, updatedRetry);
}
else {
// A partial object must not drop the other retry settings, as the normalization below and the retry logic require them.
this.#internals.retry = { ...this.#internals.retry, ...updatedRetry };
}
const { retry } = this.#internals;
retry.methods = [...new Set(retry.methods.map(method => method.toUpperCase()))];
retry.statusCodes = [...new Set(retry.statusCodes)];
retry.errorCodes = [...new Set(retry.errorCodes)];
}
/**
From `http.RequestOptions`.
The IP address used to send the request from.
*/
get localAddress() {
return this.#internals.localAddress;
}
set localAddress(value) {
assertAny('localAddress', [is.string, is.undefined], value);
this.#internals.localAddress = value;
}
/**
The HTTP method used to make the request.
Custom methods such as `PROPFIND` are supported. Method names are normalized to uppercase.
@default 'GET'
*/
get method() {
return this.#internals.method;
}
set method(value) {
// An empty method would be sent as the default `GET`, hiding the mistake.
assertAny('method', [is.nonEmptyString], value);
this.#internals.method = value.toUpperCase();
// A hook that switches to a method without a body slot has to drop or replace the body.
this.#trackedStateMutations?.add('method');
}
/**
The function used to retrieve a `net.Socket` instance when the `agent` option is not used.
For HTTP/1, either return the socket synchronously or call `callback(null, socket)` asynchronously. Call `callback(error)` if socket creation fails.
When `http2` is enabled for HTTPS, this function must synchronously return a `tls.TLSSocket`. Callback-based creation is not supported by HTTP/2 negotiation.
*/
get createConnection() {
return this.#internals.createConnection;
}
set createConnection(value) {
assertAny('createConnection', [is.function, is.undefined], value);
this.#internals.createConnection = value;
}
/**
From `http-cache-semantics`
@default {}
*/
get cacheOptions() {
return this.#internals.cacheOptions;
}
set cacheOptions(value) {
assertPlainObject('cacheOptions', value);
assertAny('cacheOptions.shared', [is.boolean, is.undefined], value.shared);
assertAny('cacheOptions.cacheHeuristic', [is.number, is.undefined], value.cacheHeuristic);
assertAny('cacheOptions.immutableMinTimeToLive', [is.number, is.undefined], value.immutableMinTimeToLive);
assertAny('cacheOptions.ignoreCargoCult', [is.boolean, is.undefined], value.ignoreCargoCult);
assertKnownKeys(value, defaultInternals.cacheOptions, key => new Error(`Cache option \`${key}\` does not exist`));
this.#mergeOrReplace('cacheOptions', value);
}
/**
Options for the advanced HTTPS API.
*/
get https() {
return this.#internals.https;
}
set https(value) {
assertPlainObject('https', value);
assertAny('https.rejectUnauthorized', [is.boolean, is.undefined], value.rejectUnauthorized);
assertAny('https.checkServerIdentity', [is.function, is.undefined], value.checkServerIdentity);
assertAny('https.serverName', [is.string, is.undefined], value.serverName);
assertAny('https.certificateAuthority', [is.string, is.object, is.array, is.undefined], value.certificateAuthority);
assertAny('https.key', [is.string, is.object, is.array, is.undefined], value.key);
assertAny('https.certificate', [is.string, is.object, is.array, is.undefined], value.certificate);
assertAny('https.passphrase', [is.string, is.undefined], value.passphrase);
assertAny('https.pfx', [is.string, is.uint8Array, is.array, is.undefined], value.pfx);
assertAny('https.alpnProtocols', [is.array, is.undefined], value.alpnProtocols);
assertAny('https.ciphers', [is.string, is.undefined], value.ciphers);
assertAny('https.dhparam', [is.string, is.buffer, is.undefined], value.dhparam);
assertAny('https.signatureAlgorithms', [is.string, is.undefined], value.signatureAlgorithms);
assertAny('https.minVersion', [is.string, is.undefined], value.minVersion);
assertAny('https.maxVersion', [is.string, is.undefined], value.maxVersion);
assertAny('https.honorCipherOrder', [is.boolean, is.undefined], value.honorCipherOrder);
assertAny('https.tlsSessionLifetime', [is.number, is.undefined], value.tlsSessionLifetime);
assertAny('https.ecdhCurve', [is.string, is.undefined], value.ecdhCurve);
assertAny('https.certificateRevocationLists', [is.string, is.buffer, is.array, is.undefined], value.certificateRevocationLists);
assertAny('https.secureOptions', [is.number, is.undefined], value.secureOptions);
assertKnownKeys(value, defaultInternals.https, key => new Error(`HTTPS option \`${key}\` does not exist`));
this.#mergeOrReplace('https', cloneHttps(value));
}
/**
[Encoding](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) to be used on `setEncoding` of the response data.
To get a [`Uint8Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), you need to set `responseType` to `buffer` instead.
Don't set this option to `null`.
__Note__: This doesn't affect streams! Instead, you need to do `got.stream(...).setEncoding(encoding)`.
@default undefined, which decodes the response as UTF-8
*/
get encoding() {
return this.#internals.encoding;
}
set encoding(value) {
if (value === null) {
throw new TypeError('To get a Uint8Array, set `options.responseType` to `buffer` instead');
}
assertAny('encoding', [is.string, is.undefined], value);
this.#internals.encoding = value;
}
/**
When set to `true` the promise will return the Response body instead of the Response object.
@default false
*/
get resolveBodyOnly() {
return this.#internals.resolveBodyOnly;
}
set resolveBodyOnly(value) {
assert.boolean(value);
this.#internals.resolveBodyOnly = value;
}
/**
Returns a `Stream` instead of a `Promise`.
Set internally by `got.stream()`.
@default false
@internal
*/
get isStream() {
return this.#internals.isStream;
}
set isStream(value) {
assert.boolean(value);
this.#internals.isStream = value;
}
/**
The parsing method.
The promise also has `.text()`, `.json()` and `.buffer()` methods which return another Got promise for the parsed body.
It's like setting the options to `{responseType: 'json', resolveBodyOnly: true}` but without affecting the main Got promise.
`.json()` also accepts a [Standard Schema](https://standardschema.dev) validator, in which case the promise resolves with the output of the schema.
__Note__: When using streams, this option is ignored.
@example
```
const responsePromise = got(url);
const bufferPromise = responsePromise.buffer();
const jsonPromise = responsePromise.json();
const [response, buffer, json] = Promise.all([responsePromise, bufferPromise, jsonPromise]);
// `response` is an instance of Got Response
// `buffer` is an instance of Uint8Array
// `json` is an object
```
@example
```
// This
const body = await got(url).json();
// is semantically the same as this
const body = await got(url, {responseType: 'json', resolveBodyOnly: true});
```
*/
get responseType() {
return this.#internals.responseType;
}
set responseType(value) {
if (value !== 'text' && value !== 'buffer' && value !== 'json') {
throw new Error(`Invalid \`responseType\` option: ${value}`);
}
this.#internals.responseType = value;
}
get pagination() {
return this.#internals.pagination;
}
set pagination(value) {
assert.object(value);
const updatedPagination = Object.fromEntries(Object.entries(value).filter(([, option]) => option !== undefined));
if (this.#merging) {
safeObjectAssign(this.#internals.pagination, updatedPagination);
}
else {
// A partial object must not drop the other pagination settings, as the pagination logic requires them.
this.#internals.pagination = { ...this.#internals.pagination, ...updatedPagination };
}
}
get setHost() {
return this.#internals.setHost;
}
set setHost(value) {
assert.boolean(value);
this.#internals.setHost = value;
}
get maxHeaderSize() {
return this.#internals.maxHeaderSize;
}
set maxHeaderSize(value) {
assertAny('maxHeaderSize', [is.number, is.undefined], value);
this.#internals.maxHeaderSize = value;
}
get enableUnixSockets() {
return this.#internals.enableUnixSockets;
}
set enableUnixSockets(value) {
assert.boolean(value);
this.#internals.enableUnixSockets = value;
}
/**
Throw an error if the server response's `content-length` header value doesn't match the number of bytes received.
This is useful for detecting truncated responses and follows RFC 9112 requirements for message completeness.
__Note__: Responses without a `content-length` header are not validated.
__Note__: When enabled and validation fails, a `ReadError` with code `ERR_HTTP_CONTENT_LENGTH_MISMATCH` will be thrown.
__Note__: Keep custom native responses in byte mode for exact validation. Calling `setEncoding()` on the native response can lose the original byte count when decoding malformed input.
@default true
*/
get strictContentLength() {
return this.#internals.strictContentLength;
}
set strictContentLength(value) {
assert.boolean(value);
this.#internals.strictContentLength = value;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
toJSON() {
// A copy of the nested state, so editing the result cannot change these options.
return cloneInternals(this.#internals);
}
[Symbol.for('nodejs.util.inspect.custom')](_depth, options) {
return inspect(this.#internals, options);
}
createNativeRequestOptions() {
const internals = this.#internals;
const url = internals.url;
const usesAlpn = usesHttp2Alpn(internals, url);
const socketTimeout = usesAlpn ? internals.timeout.socket : undefined;
const http2Timeout = usesAlpn ? getHttp2Timeout(internals) : undefined;
let agent;
if (url.protocol === 'https:') {
if (internals.http2) {
// Ensure HTTP/2 pooling is configured for connection reuse.
// If agent.http2 is unset, use the global agent for connection pooling.
agent = {
...internals.agent,
http2: internals.agent.http2 ?? http2Client.globalAgent,
};
}
else {
agent = internals.agent.https;
}
}
else {
agent = internals.agent.http;
}
const { https } = internals;
let { pfx } = https;
if (is.array(pfx)) {
pfx = pfx.map(entry => is.plainObject(entry)
? {
buf: entry.buffer,
passphrase: entry.passphrase,
}
: entry);
}
const unixSocketPath = getUnixSocketPath(url);
if (usesUnixSocket(url) && !internals.enableUnixSockets) {
throw new Error('Using UNIX domain sockets but option `enableUnixSockets` is not enabled');
}
let unixSocketGroups;
if (unixSocketPath !== undefined) {
unixSocketGroups = /^(?<socketPath>[^:]+):(?<path>.+)$/v.exec(`${url.pathname}${url.search}`)?.groups;
}
const unixOptions = unixSocketGroups
// The URL parser percent-encodes the path, but the socket path is a filesystem path.
? { socketPath: safeDecode(unixSocketGroups.socketPath), path: unixSocketGroups.path, host: '' }
: undefined;
const nativeRequestOptions = {
...internals.cacheOptions,
...unixOptions,
// HTTPS options
// eslint-disable-next-line @typescript-eslint/naming-convention
ALPNProtocols: https.alpnProtocols,
ca: https.certificateAuthority,
cert: https.certificate,
key: https.key,
passphrase: https.passphrase,
pfx,
rejectUnauthorized: https.rejectUnauthorized,
checkServerIdentity: https.checkServerIdentity ?? checkServerIdentity,
servername: https.serverName,
ciphers: https.ciphers,
honorCipherOrder: https.honorCipherOrder,
minVersion: https.minVersion,
maxVersion: https.maxVersion,
sigalgs: https.signatureAlgorithms,
sessionTimeout: https.tlsSessionLifetime,
dhparam: https.dhparam,
ecdhCurve: https.ecdhCurve,
crl: https.certificateRevocationLists,
secureOptions: https.secureOptions,
// HTTP options
lookup: internals.dnsLookup ? getAdaptedLookup(internals.dnsLookup) : (internals.dnsCache ? getDnsCacheLookup(internals.dnsCache) : undefined),
family: internals.dnsLookupIpVersion,
agent,
setHost: internals.setHost,
method: internals.method,
maxHeaderSize: internals.maxHeaderSize,
localAddress: internals.localAddress,
// A copy, so a caller editing the native options cannot change these options.
headers: cloneHeaders(internals.headers),
createConnection: internals.createConnection,
signal: internals.http2 ? internals.signal : undefined,
timeout: http2Timeout?.timeout,
...(http2Timeout !== undefined && { _timeoutPhase: http2Timeout.phase }),
...(socketTimeout !== undefined && { _socketTimeout: socketTimeout }),
// HTTP/2 options
h2session: internals.h2session,
};
return nativeRequestOptions;
}
getRequestFunction() {
const { request: customRequest } = this.#internals;
if (!customRequest) {
return this.#getFallbackRequestFunction();
}
const requestWithFallback = (url, options, callback) => {
const nativeAgent = getNativeAgent(url, options.agent);
const hasInternalSocketTimeout = Object.hasOwn(options, '_socketTimeout');
const hasInternalTimeoutPhase = Object.hasOwn(options, '_timeoutPhase');
const customRequestOptions = options.timeout !== undefined || hasInternalSocketTimeout || nativeAgent !== options.agent
? {
...options,
agent: nativeAgent,
timeout: undefined,
}
: options;
if (hasInternalSocketTimeout) {
Reflect.deleteProperty(customRequestOptions, '_socketTimeout');
}
if (hasInternalTimeoutPhase) {
Reflect.deleteProperty(customRequestOptions, '_timeoutPhase');
}
const result = customRequest(url, customRequestOptions, callback);
if (is.promise(result)) {
return this.#resolveRequestWithFallback(result, {
url,
options,
callback,
});
}
return result ?? this.#callFallbackRequest(url, options, callback);
};
return requestWithFallback;
}
freeze() {
const options = this.#internals;
Object.freeze(options);
Object.freeze(options.hooks);
for (const handlers of Object.values(options.hooks)) {
Object.freeze(handlers);
}
Object.freeze(options.https);
for (const value of Object.values(options.https)) {
if (!Array.isArray(value)) {
continue;
}
for (const item of value) {
if (is.plainObject(item)) {
Object.freeze(item);
}
}
Object.freeze(value);
}
Object.freeze(options.cacheOptions);
Object.freeze(options.agent);
Object.freeze(options.headers);
Object.freeze(options.timeout);
Object.freeze(options.pagination);
Object.freeze(options.retry);
Object.freeze(options.retry.errorCodes);
Object.freeze(options.retry.methods);
Object.freeze(options.retry.statusCodes);
for (const value of Object.values(options.headers)) {
Object.freeze(value);
}
}
#getCredential(credential) {
const url = this.#internals.url;
const value = url ? url[credential] : this.#internals[credential];
return safeDecode(value);
}
#setCredential(credential, value) {
const url = this.#internals.url;
const fixedValue = encodeURIComponent(value);
if (url) {
url[credential] = fixedValue;
}
else {
this.#internals[credential] = fixedValue;
}
this.#trackedStateMutations?.add(credential);
}
// Merging assigns into the existing object, while a direct assignment replaces it with a copy.
#mergeOrReplace(key, value) {
if (this.#merging) {
safeObjectAssign(this.#internals[key], value);
}
else {
this.#internals[key] = { ...value };
}
}
#createHeadersProxy() {
return new Proxy(this.#internals.headers, {
get(target, property, receiver) {
if (typeof property === 'string') {
// Only own properties are headers, so an inherited `Object.prototype` member
// must not read as one.
if (Object.hasOwn(target, property)) {
return Reflect.get(target, property, receiver);
}
const normalizedProperty = property.toLowerCase();
return Object.hasOwn(target, normalizedProperty) ? Reflect.get(target, normalizedProperty, receiver) : undefined;
}
return Reflect.get(target, property, receiver);
},
set: (target, property, value) => {
if (typeof property === 'string') {
const normalizedProperty = property.toLowerCase();
assertValidHeaderName(normalizedProperty);
const isSuccess = Reflect.set(target, normalizedProperty, typeof value === 'number' ? String(value) : value);
if (isSuccess) {
this.#explicitHeaders.add(normalizedProperty);
this.#trackedStateMutations?.add(normalizedProperty);
}
return isSuccess;
}
return Reflect.set(target, property, value);
},
deleteProperty: (target, property) => {
if (typeof property === 'string') {
const normalizedProperty = property.toLowerCase();
const isSuccess = Reflect.deleteProperty(target, normalizedProperty);
if (isSuccess) {
this.#explicitHeaders.delete(normalizedProperty);
this.#trackedStateMutations?.add(normalizedProperty);
}
return isSuccess;
}
return Reflect.deleteProperty(target, property);
},
});
}
#getFallbackRequestFunction() {
const url = this.#internals.url;
if (!url) {
return;
}
if (this.#internals.h2session) {
return http2Client.auto;
}
if (url.protocol === 'https:') {
return this.#internals.http2 ? http2Client.auto : https.request;
}
return http.request;
}
#callFallbackRequest(url, options, callback) {
const fallbackRequest = this.#getFallbackRequestFunction();
if (!fallbackRequest) {
throw new TypeError('The request function must return a value');
}
const fallbackResult = fallbackRequest(url, options, callback);
if (fallbackResult === undefined) {
throw new TypeError('The request function must return a value');
}
return is.promise(fallbackResult) ? this.#resolveFallbackRequestResult(fallbackResult) : fallbackResult;
}
async #resolveRequestWithFallback(requestResult, { url, options, callback }) {
const result = await requestResult;
return result ?? this.#callFallbackRequest(url, options, callback);
}
async #resolveFallbackRequestResult(fallbackResult) {
const resolvedFallbackResult = await fallbackResult;
if (resolvedFallbackResult === undefined) {
throw new TypeError('The request function must return a value');
}
return resolvedFallbackResult;
}
}
/**
The option names are the accessor properties. Everything else on the prototype chain is a
method or an `Object.prototype` member, neither of which is an option.
*/
const optionDescriptors = Object.getOwnPropertyDescriptors(Options.prototype);
const optionNames = new Set(Object.entries(optionDescriptors).filter(([, descriptor]) => descriptor.get !== undefined && descriptor.set !== undefined).map(([name]) => name));
export const snapshotCrossOriginState = (options) => ({
headers: { ...options.getInternalHeaders() },
hadCookieJar: options.cookieJar !== undefined,
cookieWasExplicitlySet: options.isHeaderExplicitlySet('cookie'),
username: options.username,
password: options.password,
body: options.body,
json: options.json,
form: options.form,
bodySnapshot: cloneCrossOriginBodyValue(options.body),
jsonSnapshot: cloneCrossOriginBodyValue(options.json),
formSnapshot: cloneCrossOriginBodyValue(options.form),
});
const cloneCrossOriginBodyValue = (value) => {
if (value === undefined || value === null || typeof value !== 'object') {
return value;
}
try {
const clone = structuredClone(value);
// `structuredClone` drops prototypes it does not know, so a `Buffer` would never
// compare equal to the value it was cloned from.
if (Object.getPrototypeOf(clone) !== Object.getPrototypeOf(value)) {
Object.setPrototypeOf(clone, Object.getPrototypeOf(value));
}
return clone;
}
catch {
return undefined;
}
};
const isUnchangedCrossOriginBodyValue = (currentValue, previousValue, previousSnapshot) => {
if (currentValue !== previousValue) {
return false;
}
return currentValue === undefined || currentValue === null || typeof currentValue !== 'object' || (previousSnapshot === undefined) ? true : isDeepStrictEqual(currentValue, previousSnapshot);
};
export const isCrossOriginCredentialChanged = (previousUrl, nextUrl, credential) => (nextUrl[credential] !== '' && nextUrl[credential] !== previousUrl[credential]);
export const isBodyUnchanged = (options, previousState) => isUnchangedCrossOriginBodyValue(options.body, previousState.body, previousState.bodySnapshot)
&& isUnchangedCrossOriginBodyValue(options.json, previousState.json, previousState.jsonSnapshot)
&& isUnchangedCrossOriginBodyValue(options.form, previousState.form, previousState.formSnapshot);