got
Version:
Human-friendly and powerful HTTP request library for Node.js
1,408 lines (1,324 loc) • 81.1 kB
JavaScript
import process from 'node:process';
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 CacheableLookup from 'cacheable-lookup';
import http2wrapper from 'http2-wrapper';
import parseLinkHeader from './parse-link-header.js';
import { getUnixSocketPath } from './utils/is-unix-socket-url.js';
const [major, minor] = process.versions.node.split('.').map(Number);
/**
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);
});
}
/**
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'];
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') && url?.[credential] !== ''));
const hasProtocolSlashes = (value) => /^[a-z][\d+\-.a-z]*:\/\//iv.test(value);
const hasHttpProtocolWithoutSlashes = (value) => /^https?:(?!\/\/)/iv.test(value);
export function applyUrlOverride(options, url, { username, password } = {}) {
if (is.string(url) && options.url) {
url = new URL(url, options.url).toString();
}
options.prefixUrl = '';
options.url = url;
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}`);
}
}
/**
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 of Object.keys(source)) {
if (key === '__proto__') {
continue;
}
target[key] = source[key];
}
}
function validateSearchParameters(searchParameters) {
for (const key of Object.keys(searchParameters)) {
if (key === '__proto__') {
continue;
}
const value = searchParameters[key];
assertAny(`searchParams.${key}`, [is.string, is.number, is.boolean, is.null, is.undefined], value);
}
}
const globalCache = new Map();
let globalDnsCache;
const getGlobalDnsCache = () => {
if (globalDnsCache) {
return globalDnsCache;
}
globalDnsCache = new CacheableLookup();
return globalDnsCache;
};
// 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,
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',
],
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: Number.POSITIVE_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;
}
return JSON.parse(response.body);
},
paginate({ response }) {
const rawLinkHeader = response.headers.link;
if (typeof rawLinkHeader !== 'string' || rawLinkHeader.trim() === '') {
return false;
}
const parsed = parseLinkHeader(rawLinkHeader);
const next = parsed.find(entry => entry.parameters.rel === 'next' || entry.parameters.rel === '"next"');
if (next) {
const baseUrl = response.request.options.url ?? response.url;
return {
url: new URL(next.reference, baseUrl),
};
}
return false;
},
filter: () => true,
shouldContinue: () => true,
countLimit: Number.POSITIVE_INFINITY,
backoff: 0,
requestLimit: 10_000,
stackAllItems: false,
},
setHost: true,
maxHeaderSize: undefined,
signal: undefined,
enableUnixSockets: false,
strictContentLength: true,
};
const cloneInternals = (internals) => {
const { hooks, retry } = internals;
const result = {
...internals,
context: { ...internals.context },
cacheOptions: { ...internals.cacheOptions },
https: { ...internals.https },
agent: { ...internals.agent },
headers: { ...internals.headers },
retry: {
...retry,
errorCodes: [...retry.errorCodes],
methods: [...retry.methods],
statusCodes: [...retry.statusCodes],
},
timeout: { ...internals.timeout },
hooks: {
init: [...hooks.init],
beforeRequest: [...hooks.beforeRequest],
beforeError: [...hooks.beforeError],
beforeRedirect: [...hooks.beforeRedirect],
beforeRetry: [...hooks.beforeRetry],
beforeCache: [...hooks.beforeCache],
afterResponse: [...hooks.afterResponse],
},
searchParams: internals.searchParams ? new URLSearchParams(internals.searchParams) : undefined,
pagination: { ...internals.pagination },
};
return result;
};
const cloneRaw = (raw) => {
const result = { ...raw };
if (Object.hasOwn(raw, 'context') && is.object(raw.context)) {
result.context = { ...raw.context };
}
if (Object.hasOwn(raw, 'cacheOptions') && is.object(raw.cacheOptions)) {
result.cacheOptions = { ...raw.cacheOptions };
}
if (Object.hasOwn(raw, 'https') && is.object(raw.https)) {
result.https = { ...raw.https };
}
if (Object.hasOwn(raw, 'agent') && is.object(raw.agent)) {
result.agent = { ...raw.agent };
}
if (Object.hasOwn(raw, 'headers') && is.object(raw.headers)) {
result.headers = { ...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, 'timeout') && is.object(raw.timeout)) {
result.timeout = { ...raw.timeout };
}
if (Object.hasOwn(raw, 'hooks') && is.object(raw.hooks)) {
const { hooks } = raw;
result.hooks = {
...hooks,
};
if (is.array(hooks.init)) {
result.hooks.init = [...hooks.init];
}
if (is.array(hooks.beforeRequest)) {
result.hooks.beforeRequest = [...hooks.beforeRequest];
}
if (is.array(hooks.beforeError)) {
result.hooks.beforeError = [...hooks.beforeError];
}
if (is.array(hooks.beforeRedirect)) {
result.hooks.beforeRedirect = [...hooks.beforeRedirect];
}
if (is.array(hooks.beforeRetry)) {
result.hooks.beforeRetry = [...hooks.beforeRetry];
}
if (is.array(hooks.beforeCache)) {
result.hooks.beforeCache = [...hooks.beforeCache];
}
if (is.array(hooks.afterResponse)) {
result.hooks.afterResponse = [...hooks.afterResponse];
}
}
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 if (is.object(raw.searchParams)) {
result.searchParams = { ...raw.searchParams };
}
}
if (Object.hasOwn(raw, 'pagination') && is.object(raw.pagination)) {
result.pagination = { ...raw.pagination };
}
return result;
};
const getHttp2TimeoutOption = (internals) => {
const delays = [internals.timeout.socket, internals.timeout.connect, internals.timeout.lookup, internals.timeout.request, internals.timeout.secureConnect].filter(delay => typeof delay === 'number');
return delays.length > 0 ? Math.min(...delays) : undefined;
};
const trackStateMutation = (trackedStateMutations, name) => {
trackedStateMutations?.add(name);
};
const addExplicitHeader = (explicitHeaders, name) => {
explicitHeaders.add(name);
};
const markHeaderAsExplicit = (explicitHeaders, trackedStateMutations, name) => {
addExplicitHeader(explicitHeaders, name);
trackStateMutation(trackedStateMutations, name);
};
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]) {
trackStateMutation(trackedStateMutations, 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__']);
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)) {
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) {
error.options = this;
throw error;
}
/* 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 (!(key in this)) {
throw new Error(`Unexpected option: ${key}`);
}
// @ts-expect-error Type 'unknown' is not assignable to type 'never'.
const value = options[key];
if (value === undefined) {
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.
The main purpose of this is to [support HTTP2 using a wrapper](https://github.com/szmarczak/http2-wrapper).
@default http.request | https.request
*/
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 [`http2wrapper.Agent`](https://github.com/szmarczak/http2-wrapper#new-http2agentoptions) instance.
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.
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 (!(key in this.#internals.agent)) {
throw new TypeError(`Unexpected agent option: ${key}`);
}
// @ts-expect-error - No idea why `value[key]` doesn't work here.
assertAny(`agent.${key}`, [is.object, is.undefined, (v) => v === false], value[key]);
}
if (this.#merging) {
safeObjectAssign(this.#internals.agent, value);
}
else {
this.#internals.agent = { ...value };
}
}
get h2session() {
return this.#internals.h2session;
}
set h2session(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 (!(key in this.#internals.timeout)) {
throw new Error(`Unexpected timeout option: ${key}`);
}
// @ts-expect-error - No idea why `value[key]` doesn't work here.
assertAny(`timeout.${key}`, [is.number, is.undefined], value[key]);
}
if (this.#merging) {
safeObjectAssign(this.#internals.timeout, value);
}
else {
this.#internals.timeout = { ...value };
}
}
/**
When specified, `prefixUrl` will be prepended to `url`.
The prefix can be any valid URL, either relative or absolute.
A trailing slash `/` is optional - one will be added automatically.
__Note__: `prefixUrl` will be ignored if the `url` argument is a URL instance.
__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 (!value.endsWith('/')) {
value += '/';
}
if (this.#internals.prefixUrl && this.#internals.url) {
const { href } = this.#internals.url;
this.#internals.url.href = value + href.slice(this.#internals.prefixUrl.length);
}
this.#internals.prefixUrl = value;
}
/**
__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` / `Uint8Array` / typed array, 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, is.typedArray, 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;
trackStateMutation(this.#trackedStateMutations, '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;
trackStateMutation(this.#trackedStateMutations, '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;
trackStateMutation(this.#trackedStateMutations, 'json');
}
/**
The URL to request, as a string, a [`https.request` options object](https://nodejs.org/api/https.html#https_https_request_options_callback), or a [WHATWG `URL`](https://nodejs.org/api/url.html#url_class_url).
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;
trackStateMutation(this.#trackedStateMutations, '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
&& hasHttpProtocolWithoutSlashes(valueString)) {
throw new Error('`url` protocol must be followed by `//`');
}
// Detect if URL is already absolute (has a protocol/scheme)
const isAbsolute = is.urlInstance(value) || hasProtocolSlashes(valueString);
// Only concatenate prefixUrl if the URL is relative
const urlString = isAbsolute ? valueString : `${this.prefixUrl}${valueString}`;
const url = new URL(urlString);
this.#internals.url = url;
trackStateMutation(this.#trackedStateMutations, 'url');
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;
}
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 (setCookie.length === 4 && getCookieString.length === 0) {
this.#internals.cookieJar = {
setCookie: promisify(setCookie.bind(value)),
getCookieString: promisify(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 (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);
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)) {
updated = new URLSearchParams(value);
}
else if (value instanceof URLSearchParams) {
updated = value;
}
else {
validateSearchParameters(value);
updated = new URLSearchParams();
for (const key of Object.keys(value)) {
if (key === '__proto__') {
continue;
}
const entry = value[key];
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) {
url.search = searchParameters.toString();
}
else {
this.#internals.searchParams = searchParameters;
}
}
get searchParameters() {
throw new Error('The `searchParameters` option does not exist. Use `searchParams` instead.');
}
set searchParameters(_value) {
throw new Error('The `searchParameters` option does not exist. Use `searchParams` instead.');
}
get dnsLookup() {
return this.#internals.dnsLookup;
}
set dnsLookup(value) {
assertAny('dnsLookup', [is.function, is.undefined], value);
this.#internals.dnsLookup = value;
}
/**
An instance of [`CacheableLookup`](https://github.com/szmarczak/cacheable-lookup) used for making DNS lookups.
Useful when making lots of requests to different *public* hostnames.
`CacheableLookup` uses `dns.resolver4(..)` and `dns.resolver6(...)` under the hood and fall backs to `dns.lookup(...)` when the first two fail, which may lead to additional delay.
__Note__: This should stay disabled when making requests to internal hostnames such as `localhost`, `database.local` etc.
@default false
*/
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 {
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);
if (this.#merging) {
safeObjectAssign(this.#internals.context, value);
}
else {
this.#internals.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 (!(knownHookEvent in this.#internals.hooks)) {
throw new Error(`Unexpected hook event: ${knownHookEvent}`);
}
const typedKnownHookEvent = knownHookEvent;
const hooks = value[typedKnownHookEvent];
assertAny(`hooks.${knownHookEvent}`, [is.array, is.undefined], hooks);
if (hooks) {
for (const hook of hooks) {
assert.function(hook);
}
}
if (this.#merging) {
if (hooks) {
// @ts-expect-error FIXME
this.#internals.hooks[typedKnownHookEvent].push(...hooks);
}
}
else {
if (!hooks) {
throw new Error(`Missing hook event: ${knownHookEvent}`);
}
// @ts-expect-error FIXME
this.#internals.hooks[knownHookEvent] = [...hooks];
}
}
}
/**
Whether redirect responses should be followed automatically.
Optionally, pass a function to dynamically decide based on the response object.
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;
}
get followRedirects() {
throw new TypeError('The `followRedirects` option does not exist. Use `followRedirect` instead.');
}
set followRedirects(_value) {
throw new TypeError('The `followRedirects` option does not exist. Use `followRedirect` instead.');
}
/**
If exceeded, the request will be aborted and a `MaxRedirectsError` will be thrown.
@default 10
*/
get maxRedirects() {
return this.#internals.maxRedirects;
}
set maxRedirects(value) {
assert.number(value);
this.#internals.maxRedirects = value;
}
/**
A cache adapter instance for storing cached response data.
@default false
*/
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() {
const url = this.#internals.url;
const value = url ? url.username : this.#internals.username;
return decodeURIComponent(value);
}
set username(value) {
assert.string(value);
const url = this.#internals.url;
const fixedValue = encodeURIComponent(value);
if (url) {
url.username = fixedValue;
}
else {
this.#internals.username = fixedValue;
}
trackStateMutation(this.#trackedStateMutations, 'username');
}
get password() {
const url = this.#internals.url;
const value = url ? url.password : this.#internals.password;
return decodeURIComponent(value);
}
set password(value) {
assert.string(value);
const url = this.#internals.url;
const fixedValue = encodeURIComponent(value);
if (url) {
url.password = fixedValue;
}
else {
this.#internals.password = fixedValue;
}
trackStateMutation(this.#trackedStateMutations, 'password');
}
/**
If set to `true`, Got will additionally accept HTTP2 requests.
It will choose either HTTP/1.1 or HTTP/2 depending on the ALPN protocol.
__Note__: This option requires Node.js 15.10.0 or newer as HTTP/2 support on older Node.js versions is very buggy.
__Note__: Overriding `options.request` will disable HTTP2 support.
@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.
However, the [HTTP/2 specification](https://tools.ietf.org/html/rfc7540#section-8.1.3) says that `An HTTP GET request includes request header fields and no payload body`, therefore when using the HTTP/2 protocol this option will have no effect.
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;
}
/**
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, but you may still want to filter out headers like `Host`, `Connection`, `Authorization`, etc.
@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
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) {
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);
}
}