got
Version:
Human-friendly and powerful HTTP request library for Node.js
1,061 lines • 86.1 kB
JavaScript
import process from 'node:process';
import { Buffer } from 'node:buffer';
import { Duplex } from 'node:stream';
import { addAbortListener } from 'node:events';
import http, { ServerResponse } from 'node:http';
import { byteLength } from 'byte-counter';
import { chunk } from 'chunk-data';
import { concatUint8Arrays, stringToBase64, stringToUint8Array } from 'uint8array-extras';
import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
import decompressResponse from 'decompress-response';
import is, { isBuffer } from '@sindresorhus/is';
import timer from './utils/timer.js';
import getBodySize from './utils/get-body-size.js';
import proxyEvents from './utils/proxy-events.js';
import timedOut, { TimeoutError as TimedOutTimeoutError } from './timed-out.js';
import stripUrlAuth from './utils/strip-url-auth.js';
import WeakableMap from './utils/weakable-map.js';
import calculateRetryDelay from './calculate-retry-delay.js';
import Options, { crossOriginStripHeaders, hasExplicitCredentialInUrlChange, isCrossOriginCredentialChanged, isBodyUnchanged, isSameOrigin, snapshotCrossOriginState, } from './options.js';
import { cacheDecodedBody, decodeUint8Array, isResponseOk, isUtf8Encoding, } from './response.js';
import isClientRequest from './utils/is-client-request.js';
import { getUnixSocketPath } from './utils/is-unix-socket-url.js';
import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js';
import { generateRequestId, publishRequestCreate, publishRequestStart, publishResponseStart, publishResponseEnd, publishRetry, publishError, publishRedirect, } from './diagnostics-channel.js';
const supportsBrotli = is.string(process.versions.brotli);
const supportsZstd = is.string(process.versions.zstd);
const methodsWithoutBody = new Set(['GET', 'HEAD']);
const cacheableStore = new WeakableMap();
const redirectCodes = new Set([301, 302, 303, 307, 308]);
export { crossOriginStripHeaders } from './options.js';
const transientWriteErrorCodes = new Set(['EPIPE', 'ECONNRESET']);
const omittedPipedHeaders = new Set([
'host',
'connection',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'proxy-connection',
'te',
'trailer',
'transfer-encoding',
'upgrade',
]);
// Track errors that have been processed by beforeError hooks to preserve custom error types
const errorsProcessedByHooks = new WeakSet();
const proxiedRequestEvents = [
'socket',
'connect',
'continue',
'information',
'upgrade',
];
const noop = () => { };
const isTransientWriteError = (error) => {
const { code } = error;
return typeof code === 'string' && transientWriteErrorCodes.has(code);
};
const getConnectionListedHeaders = (headers) => {
const connectionListedHeaders = new Set();
for (const [header, connectionHeader] of Object.entries(headers)) {
const normalizedHeader = header.toLowerCase();
if (normalizedHeader !== 'connection' && normalizedHeader !== 'proxy-connection') {
continue;
}
const connectionHeaderValues = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader];
for (const value of connectionHeaderValues) {
if (typeof value !== 'string') {
continue;
}
for (const token of value.split(',')) {
const normalizedToken = token.trim().toLowerCase();
if (normalizedToken.length > 0) {
connectionListedHeaders.add(normalizedToken);
}
}
}
}
return connectionListedHeaders;
};
export const normalizeError = (error) => {
if (error instanceof globalThis.Error) {
return error;
}
if (is.object(error)) {
const errorLike = error;
const message = typeof errorLike.message === 'string' ? errorLike.message : 'Non-error object thrown';
const normalizedError = new globalThis.Error(message, { cause: error });
if (typeof errorLike.stack === 'string') {
normalizedError.stack = errorLike.stack;
}
if (typeof errorLike.code === 'string') {
normalizedError.code = errorLike.code;
}
if (typeof errorLike.input === 'string') {
normalizedError.input = errorLike.input;
}
return normalizedError;
}
return new globalThis.Error(String(error));
};
const getSanitizedUrl = (options) => options?.url ? stripUrlAuth(options.url) : '';
const makeProgress = (transferred, total) => {
let percent = 0;
if (total) {
percent = transferred / total;
}
else if (total === transferred) {
percent = 1;
}
return { percent, transferred, total };
};
export default class Request extends Duplex {
// @ts-expect-error - Ignoring for now.
['constructor'];
_noPipe;
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/9568
options;
response;
requestUrl;
redirectUrls = [];
retryCount = 0;
_stopReading = false;
_stopRetry;
_downloadedSize = 0;
_uploadedSize = 0;
_pipedServerResponses = new Set();
_request;
_responseSize;
_bodySize;
_unproxyEvents;
_triggerRead = false;
_jobs = [];
_cancelTimeouts;
_abortListenerDisposer;
_flushed = false;
_aborted = false;
_expectedContentLength;
_compressedBytesCount;
_skipRequestEndInFinal = false;
_incrementalDecode;
_requestId = generateRequestId();
// We need this because `this._request` if `undefined` when using cache
_requestInitialized = false;
constructor(url, options, defaults) {
super({
// Don't destroy immediately, as the error may be emitted on unsuccessful retry
autoDestroy: false,
// It needs to be zero because we're just proxying the data to another stream
highWaterMark: 0,
});
this.on('pipe', (source) => {
if (this.options.copyPipedHeaders && source?.headers) {
const connectionListedHeaders = getConnectionListedHeaders(source.headers);
for (const [header, value] of Object.entries(source.headers)) {
const normalizedHeader = header.toLowerCase();
if (omittedPipedHeaders.has(normalizedHeader) || connectionListedHeaders.has(normalizedHeader)) {
continue;
}
if (!this.options.shouldCopyPipedHeader(normalizedHeader)) {
continue;
}
this.options.setPipedHeader(normalizedHeader, value);
}
}
});
this.on('newListener', event => {
if (event === 'retry' && this.listenerCount('retry') > 0) {
throw new Error('A retry listener has been attached already.');
}
});
try {
this.options = new Options(url, options, defaults);
if (!this.options.url) {
if (this.options.prefixUrl === '') {
throw new TypeError('Missing `url` property');
}
this.options.url = '';
}
this.requestUrl = this.options.url;
// Publish request creation event
publishRequestCreate({
requestId: this._requestId,
url: getSanitizedUrl(this.options),
method: this.options.method,
});
}
catch (error) {
const { options } = error;
if (options) {
this.options = options;
}
this.flush = async () => {
this.flush = async () => { };
// Defer error emission to next tick to allow user to attach error handlers
process.nextTick(() => {
// _beforeError requires options to access retry logic and hooks
if (this.options) {
this._beforeError(normalizeError(error));
}
else {
// Options is undefined, skip _beforeError and destroy directly
const normalizedError = normalizeError(error);
const requestError = normalizedError instanceof RequestError ? normalizedError : new RequestError(normalizedError.message, normalizedError, this);
this.destroy(requestError);
}
});
};
return;
}
// Important! If you replace `body` in a handler with another stream, make sure it's readable first.
// The below is run only once.
const { body } = this.options;
if (is.nodeStream(body)) {
body.once('error', this._onBodyError);
}
if (this.options.signal) {
const abort = () => {
// See https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static#return_value
if (this.options.signal?.reason?.name === 'TimeoutError') {
this.destroy(new TimeoutError(this.options.signal.reason, this.timings, this));
}
else {
this.destroy(new AbortError(this));
}
};
if (this.options.signal.aborted) {
abort();
}
else {
const abortListenerDisposer = addAbortListener(this.options.signal, abort);
this._abortListenerDisposer = abortListenerDisposer;
}
}
}
async flush() {
if (this._flushed) {
return;
}
this._flushed = true;
try {
await this._finalizeBody();
if (this.destroyed) {
return;
}
await this._makeRequest();
if (this.destroyed) {
this._request?.destroy();
return;
}
// Queued writes etc.
for (const job of this._jobs) {
job();
}
// Prevent memory leak
this._jobs.length = 0;
this._requestInitialized = true;
}
catch (error) {
this._beforeError(normalizeError(error));
}
}
_beforeError(error) {
if (this._stopReading) {
return;
}
const { response, options } = this;
const attemptCount = this.retryCount + (error.name === 'RetryError' ? 0 : 1);
this._stopReading = true;
if (!(error instanceof RequestError)) {
error = new RequestError(error.message, error, this);
}
const typedError = error;
void (async () => {
// Node.js parser is really weird.
// It emits post-request Parse Errors on the same instance as previous request. WTF.
// Therefore, we need to check if it has been destroyed as well.
//
// Furthermore, Node.js 16 `response.destroy()` doesn't immediately destroy the socket,
// but makes the response unreadable. So we additionally need to check `response.readable`.
if (response?.readable && !response.rawBody && !this._request?.socket?.destroyed) {
// @types/node has incorrect typings. `setEncoding` accepts `null` as well.
response.setEncoding(this.readableEncoding);
const success = await this._setRawBody(response);
if (success) {
response.body = decodeUint8Array(response.rawBody);
}
}
if (this.listenerCount('retry') !== 0) {
let backoff;
try {
let retryAfter;
if (response && 'retry-after' in response.headers) {
retryAfter = Number(response.headers['retry-after']);
if (Number.isNaN(retryAfter)) {
retryAfter = Date.parse(response.headers['retry-after']) - Date.now();
if (retryAfter <= 0) {
retryAfter = 1;
}
}
else {
retryAfter *= 1000;
}
}
const retryOptions = options.retry;
const computedValue = calculateRetryDelay({
attemptCount,
retryOptions,
error: typedError,
retryAfter,
computedValue: retryOptions.maxRetryAfter ?? options.timeout.request ?? Number.POSITIVE_INFINITY,
});
// When enforceRetryRules is true, respect the retry rules (limit, methods, statusCodes, errorCodes)
// before calling the user's calculateDelay function. If computedValue is 0 (meaning retry is not allowed
// based on these rules), skip calling calculateDelay entirely.
// When false, always call calculateDelay, allowing it to override retry decisions.
if (retryOptions.enforceRetryRules && computedValue === 0) {
backoff = 0;
}
else {
backoff = await retryOptions.calculateDelay({
attemptCount,
retryOptions,
error: typedError,
retryAfter,
computedValue,
});
}
}
catch (error_) {
const normalizedError = normalizeError(error_);
void this._error(new RequestError(normalizedError.message, normalizedError, this));
return;
}
if (backoff) {
await new Promise(resolve => {
const timeout = setTimeout(resolve, backoff);
this._stopRetry = () => {
clearTimeout(timeout);
resolve();
};
});
// Something forced us to abort the retry
if (this.destroyed) {
return;
}
// Capture body BEFORE hooks run to detect reassignment
const bodyBeforeHooks = this.options.body;
try {
for (const hook of this.options.hooks.beforeRetry) {
// eslint-disable-next-line no-await-in-loop
await hook(typedError, this.retryCount + 1);
}
}
catch (error_) {
const normalizedError = normalizeError(error_);
void this._error(new RequestError(normalizedError.message, normalizedError, this));
return;
}
// Something forced us to abort the retry
if (this.destroyed) {
return;
}
// Preserve stream body reassigned in beforeRetry hooks.
const bodyAfterHooks = this.options.body;
const bodyWasReassigned = bodyBeforeHooks !== bodyAfterHooks;
// Resource cleanup and preservation logic for retry with body reassignment.
// The Promise wrapper (as-promise/index.ts) compares body identity to detect consumed streams,
// so we must preserve the body reference across destroy(). However, destroy() calls _destroy()
// which destroys this.options.body, creating a complex dance of clear/restore operations.
//
// Key constraints:
// 1. If body was reassigned, we must NOT destroy the NEW stream (it will be used for retry)
// 2. If body was reassigned, we MUST destroy the OLD stream to prevent memory leaks
// 3. We must restore the body reference after destroy() for identity checks in promise wrapper
// 4. We cannot use the normal setter after destroy() because it validates stream readability
try {
if (bodyWasReassigned) {
const oldBody = bodyBeforeHooks;
// Temporarily clear body to prevent destroy() from destroying the new stream
this.options.body = undefined;
this.destroy();
// Clean up the old stream resource if it's a stream and different from new body
// (edge case: if old and new are same stream object, don't destroy it)
if (is.nodeStream(oldBody) && oldBody !== bodyAfterHooks) {
oldBody.destroy();
}
// Restore new body for promise wrapper's identity check
if (is.nodeStream(bodyAfterHooks) && (bodyAfterHooks.readableEnded || bodyAfterHooks.destroyed)) {
throw new TypeError('The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.');
}
this.options.body = bodyAfterHooks;
}
else {
// Body wasn't reassigned - use normal destroy flow which handles body cleanup
this.destroy();
// Note: We do NOT restore the body reference here. The stream was destroyed by _destroy()
// and should not be accessed. The promise wrapper will see that body identity hasn't changed
// and will detect it's a consumed stream, which is the correct behavior.
}
}
catch (error_) {
const normalizedError = normalizeError(error_);
void this._error(new RequestError(normalizedError.message, normalizedError, this));
return;
}
// Publish retry event
publishRetry({
requestId: this._requestId,
retryCount: this.retryCount + 1,
error: typedError,
delay: backoff,
});
this.emit('retry', this.retryCount + 1, error, (updatedOptions) => {
const request = new Request(options.url, updatedOptions, options);
request.retryCount = this.retryCount + 1;
process.nextTick(() => {
void request.flush();
});
return request;
});
return;
}
}
void this._error(typedError);
})();
}
_read() {
this._triggerRead = true;
const { response } = this;
if (response && !this._stopReading) {
// We cannot put this in the `if` above
// because `.read()` also triggers the `end` event
if (response.readableLength) {
this._triggerRead = false;
}
let data;
while ((data = response.read()) !== null) {
this._downloadedSize += data.length; // eslint-disable-line @typescript-eslint/restrict-plus-operands
if (this._incrementalDecode) {
try {
const decodedChunk = typeof data === 'string' ? data : this._incrementalDecode.decoder.decode(data, { stream: true });
if (decodedChunk.length > 0) {
this._incrementalDecode.chunks.push(decodedChunk);
}
}
catch {
this._incrementalDecode = undefined;
}
}
const progress = this.downloadProgress;
if (progress.percent < 1) {
this.emit('downloadProgress', progress);
}
this.push(data);
}
}
}
_write(chunk, encoding, callback) {
const write = () => {
this._writeRequest(chunk, encoding, callback);
};
if (this._requestInitialized) {
write();
}
else {
this._jobs.push(write);
}
}
_final(callback) {
const endRequest = () => {
if (this._skipRequestEndInFinal) {
this._skipRequestEndInFinal = false;
callback();
return;
}
const request = this._request;
// We need to check if `this._request` is present,
// because it isn't when we use cache.
if (!request || request.destroyed) {
callback();
return;
}
request.end((error) => {
// The request has been destroyed before `_final` finished.
// See https://github.com/nodejs/node/issues/39356
if (request?._writableState?.errored) {
return;
}
if (!error) {
this._emitUploadComplete(request);
}
callback(error);
});
};
if (this._requestInitialized) {
endRequest();
}
else {
this._jobs.push(endRequest);
}
}
_destroy(error, callback) {
this._stopReading = true;
this.flush = async () => { };
// Prevent further retries
this._stopRetry?.();
this._cancelTimeouts?.();
this._abortListenerDisposer?.[Symbol.dispose]();
if (this.options) {
const { body } = this.options;
if (is.nodeStream(body)) {
body.destroy();
}
}
if (this._request) {
this._request.destroy();
}
// Workaround: http-timer only sets timings.end when the response emits 'end'.
// When a stream is destroyed before completion, the 'end' event may not fire,
// leaving timings.end undefined. This should ideally be fixed in http-timer
// by listening to the 'close' event, but we handle it here for now.
// Only set timings.end if there was no error or abort (to maintain semantic correctness).
const timings = this._request?.timings;
if (timings && is.undefined(timings.end) && !is.undefined(timings.response) && is.undefined(timings.error) && is.undefined(timings.abort)) {
timings.end = Date.now();
if (is.undefined(timings.phases.total)) {
timings.phases.download = timings.end - timings.response;
timings.phases.total = timings.end - timings.start;
}
}
// Preserve custom errors returned by beforeError hooks.
// For other errors, wrap non-RequestError instances for consistency.
if (error !== null && !is.undefined(error)) {
const processedByHooks = error instanceof Error && errorsProcessedByHooks.has(error);
if (!processedByHooks && !(error instanceof RequestError)) {
error = error instanceof Error
? new RequestError(error.message, error, this)
: new RequestError(String(error), {}, this);
}
}
callback(error);
}
pipe(destination, options) {
if (destination instanceof ServerResponse) {
this._pipedServerResponses.add(destination);
}
return super.pipe(destination, options);
}
unpipe(destination) {
if (destination instanceof ServerResponse) {
this._pipedServerResponses.delete(destination);
}
super.unpipe(destination);
return this;
}
_shouldIncrementallyDecodeBody() {
const { responseType, encoding } = this.options;
return Boolean(this._noPipe)
&& (responseType === 'text' || responseType === 'json')
&& isUtf8Encoding(encoding)
&& typeof globalThis.TextDecoder === 'function';
}
_checkContentLengthMismatch() {
if (this.options.strictContentLength && this._expectedContentLength !== undefined) {
// Use compressed bytes count when available (for compressed responses),
// otherwise use _downloadedSize (for uncompressed responses)
const actualSize = this._compressedBytesCount ?? this._downloadedSize;
if (actualSize !== this._expectedContentLength) {
this._beforeError(new ReadError({
message: `Content-Length mismatch: expected ${this._expectedContentLength} bytes, received ${actualSize} bytes`,
name: 'Error',
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH',
}, this));
return true;
}
}
return false;
}
async _finalizeBody() {
const { options } = this;
const headers = options.getInternalHeaders();
const isForm = !is.undefined(options.form);
// eslint-disable-next-line @typescript-eslint/naming-convention
const isJSON = !is.undefined(options.json);
const isBody = !is.undefined(options.body);
const cannotHaveBody = methodsWithoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody);
if (isForm || isJSON || isBody) {
if (cannotHaveBody) {
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
}
// Serialize body
const noContentType = !is.string(headers['content-type']);
if (isBody) {
// Native FormData
if (options.body instanceof FormData) {
const response = new Response(options.body);
if (noContentType) {
headers['content-type'] = response.headers.get('content-type') ?? 'multipart/form-data';
}
options.body = response.body;
}
else if (Object.prototype.toString.call(options.body) === '[object FormData]') {
throw new TypeError('Non-native FormData is not supported. Use globalThis.FormData instead.');
}
}
else if (isForm) {
if (noContentType) {
headers['content-type'] = 'application/x-www-form-urlencoded';
}
const { form } = options;
options.form = undefined;
options.body = (new URLSearchParams(form)).toString();
}
else {
if (noContentType) {
headers['content-type'] = 'application/json';
}
const { json } = options;
options.json = undefined;
options.body = options.stringifyJson(json);
}
const uploadBodySize = getBodySize(options.body, headers);
// See https://tools.ietf.org/html/rfc7230#section-3.3.2
// A user agent SHOULD send a Content-Length in a request message when
// no Transfer-Encoding is sent and the request method defines a meaning
// for an enclosed payload body. For example, a Content-Length header
// field is normally sent in a POST request even when the value is 0
// (indicating an empty payload body). A user agent SHOULD NOT send a
// Content-Length header field when the request message does not contain
// a payload body and the method semantics do not anticipate such a
// body.
if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding']) && !cannotHaveBody && !is.undefined(uploadBodySize)) {
headers['content-length'] = String(uploadBodySize);
}
}
if (options.responseType === 'json' && !('accept' in headers)) {
headers.accept = 'application/json';
}
this._bodySize = Number(headers['content-length']) || undefined;
}
async _onResponseBase(response) {
// This will be called e.g. when using cache so we need to check if this request has been aborted.
if (this.isAborted) {
return;
}
const { options } = this;
const { url } = options;
const nativeResponse = response;
const statusCode = response.statusCode;
const { method } = options;
const redirectLocationHeader = response.headers.location;
const redirectLocation = Array.isArray(redirectLocationHeader) ? redirectLocationHeader[0] : redirectLocationHeader;
const isRedirect = Boolean(redirectLocation && redirectCodes.has(statusCode));
// Skip decompression for responses that must not have bodies per RFC 9110:
// - HEAD responses (any status code)
// - 1xx (Informational): 100, 101, 102, 103, etc.
// - 204 (No Content)
// - 205 (Reset Content)
// - 304 (Not Modified)
const hasNoBody = method === 'HEAD'
|| (statusCode >= 100 && statusCode < 200)
|| statusCode === 204
|| statusCode === 205
|| statusCode === 304;
const prepareResponse = (response) => {
if (!Object.hasOwn(response, 'headers')) {
Object.defineProperty(response, 'headers', {
value: response.headers,
enumerable: true,
writable: true,
configurable: true,
});
}
response.statusMessage ||= http.STATUS_CODES[statusCode]; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- The status message can be empty.
response.url = stripUrlAuth(options.url);
response.requestUrl = this.requestUrl;
response.redirectUrls = this.redirectUrls;
response.request = this;
response.isFromCache = nativeResponse.fromCache ?? false;
response.ip = this.ip;
response.retryCount = this.retryCount;
response.ok = isResponseOk(response);
return response;
};
let typedResponse = prepareResponse(response);
// Redirect responses that will be followed are drained raw. Decompressing them can
// turn an irrelevant redirect body into a client-side failure or decompression DoS.
const shouldFollowRedirect = isRedirect && (typeof options.followRedirect === 'function' ? options.followRedirect(typedResponse) : options.followRedirect);
if (options.decompress && !hasNoBody && !shouldFollowRedirect) {
// When strictContentLength is enabled, track compressed bytes by listening to
// the native response's data events before decompression
if (options.strictContentLength) {
this._compressedBytesCount = 0;
nativeResponse.on('data', (chunk) => {
this._compressedBytesCount += byteLength(chunk);
});
}
response = decompressResponse(response);
typedResponse = prepareResponse(response);
}
this._responseSize = Number(response.headers['content-length']) || undefined;
this.response = typedResponse;
// eslint-disable-next-line @typescript-eslint/naming-convention
this._incrementalDecode = this._shouldIncrementallyDecodeBody() ? { decoder: new globalThis.TextDecoder('utf8', { ignoreBOM: true }), chunks: [] } : undefined;
// Publish response start event
publishResponseStart({
requestId: this._requestId,
url: typedResponse.url,
statusCode,
headers: response.headers,
isFromCache: typedResponse.isFromCache,
});
response.once('error', (error) => {
this._aborted = true;
this._beforeError(new ReadError(error, this));
});
response.once('aborted', () => {
this._aborted = true;
// Check if there's a content-length mismatch to provide a more specific error
if (!this._checkContentLengthMismatch()) {
this._beforeError(new ReadError({
name: 'Error',
message: 'The server aborted pending request',
code: 'ECONNRESET',
}, this));
}
});
let canFinalizeResponse = false;
const handleResponseEnd = () => {
if (!canFinalizeResponse
|| !response.readableEnded) {
return;
}
canFinalizeResponse = false;
if (this._stopReading) {
return;
}
// Validate content-length if it was provided
// Per RFC 9112: "If the sender closes the connection before the indicated number
// of octets are received, the recipient MUST consider the message to be incomplete"
if (this._checkContentLengthMismatch()) {
return;
}
this._responseSize = this._downloadedSize;
this.emit('downloadProgress', this.downloadProgress);
// Publish response end event
publishResponseEnd({
requestId: this._requestId,
url: typedResponse.url,
statusCode,
bodySize: this._downloadedSize,
timings: this.timings,
});
this.push(null);
};
if (!shouldFollowRedirect) {
// `set-cookie` handling below awaits the cookie jar. A fast response can fully
// end during that await, so we need to observe `end` early without completing
// the outward stream until cookie handling has finished.
response.once('end', handleResponseEnd);
}
const noPipeCookieJarRawBodyPromise = this._noPipe
&& is.object(options.cookieJar)
&& !isRedirect
? this._setRawBody(response)
: undefined;
const rawCookies = response.headers['set-cookie'];
if (is.object(options.cookieJar) && rawCookies) {
let promises = rawCookies.map(async (rawCookie) => options.cookieJar.setCookie(rawCookie, url.toString()));
if (options.ignoreInvalidCookies) {
promises = promises.map(async (promise) => {
try {
await promise;
}
catch { }
});
}
try {
await Promise.all(promises);
}
catch (error) {
this._beforeError(normalizeError(error));
return;
}
}
// The above is running a promise, therefore we need to check if this request has been aborted yet again.
if (this.isAborted) {
return;
}
if (shouldFollowRedirect) {
// We're being redirected, we don't care about the response.
// It'd be best to abort the request, but we can't because
// we would have to sacrifice the TCP connection. We don't want that.
response.resume();
this._cancelTimeouts?.();
this._unproxyEvents?.();
if (this.redirectUrls.length >= options.maxRedirects) {
this._beforeError(new MaxRedirectsError(this));
return;
}
this._request = undefined;
// Reset progress for the new request.
this._downloadedSize = 0;
this._uploadedSize = 0;
const updatedOptions = new Options(undefined, undefined, this.options);
try {
// We need this in order to support UTF-8
const redirectBuffer = Buffer.from(redirectLocation, 'binary').toString();
const redirectUrl = new URL(redirectBuffer, url);
const currentUnixSocketPath = getUnixSocketPath(url);
const redirectUnixSocketPath = getUnixSocketPath(redirectUrl);
if (redirectUrl.protocol === 'unix:' && redirectUnixSocketPath === undefined) {
this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this));
return;
}
// Relative redirects on the same socket are fine, but a redirect must not switch to a different local socket.
if (redirectUnixSocketPath !== undefined && currentUnixSocketPath !== redirectUnixSocketPath) {
this._beforeError(new RequestError('Cannot redirect to UNIX socket', {}, this));
return;
}
// Redirecting to a different site, clear sensitive data.
// For UNIX sockets, different socket paths are also different origins.
const isDifferentOrigin = redirectUrl.origin !== url.origin
|| currentUnixSocketPath !== redirectUnixSocketPath;
const serverRequestedGet = statusCode === 303 && updatedOptions.method !== 'GET' && updatedOptions.method !== 'HEAD';
// Avoid forwarding a POST body to a different origin on historical 301/302 redirects.
const crossOriginRequestedGet = isDifferentOrigin
&& (statusCode === 301 || statusCode === 302)
&& updatedOptions.method === 'POST';
const canRewrite = statusCode !== 307 && statusCode !== 308;
const userRequestedGet = updatedOptions.methodRewriting && canRewrite;
const shouldDropBody = serverRequestedGet || crossOriginRequestedGet || userRequestedGet;
if (shouldDropBody) {
updatedOptions.method = 'GET';
this._dropBody(updatedOptions);
}
if (isDifferentOrigin) {
// Also strip body on cross-origin redirects to prevent data leakage.
// 301/302 POST already drops the body (converted to GET above).
// 307/308 preserve the method per RFC, but the body must not be
// forwarded to a different origin.
// Strip credentials embedded in the redirect URL itself
// to prevent a malicious server from injecting auth to third parties.
this._stripCrossOriginState(updatedOptions, redirectUrl, shouldDropBody);
}
else {
redirectUrl.username = updatedOptions.username;
redirectUrl.password = updatedOptions.password;
}
updatedOptions.url = redirectUrl;
this.redirectUrls.push(redirectUrl);
const preHookState = isDifferentOrigin
? undefined
: {
...snapshotCrossOriginState(updatedOptions),
url: new URL(updatedOptions.url),
};
const changedState = await updatedOptions.trackStateMutations(async (changedState) => {
for (const hook of updatedOptions.hooks.beforeRedirect) {
// eslint-disable-next-line no-await-in-loop
await hook(updatedOptions, typedResponse);
}
return changedState;
});
updatedOptions.clearUnchangedCookieHeader(preHookState, changedState);
// If a beforeRedirect hook changed the URL to a different origin,
// strip sensitive headers that were preserved for the original origin.
// When isDifferentOrigin was already true, headers were already stripped above.
if (!isDifferentOrigin) {
const state = preHookState;
const hookUrl = updatedOptions.url;
if (!isSameOrigin(state.url, hookUrl)) {
this._stripUnchangedCrossOriginState(updatedOptions, hookUrl, shouldDropBody, {
...state,
changedState,
preserveUsername: hasExplicitCredentialInUrlChange(changedState, hookUrl, 'username')
|| isCrossOriginCredentialChanged(state.url, hookUrl, 'username'),
preservePassword: hasExplicitCredentialInUrlChange(changedState, hookUrl, 'password')
|| isCrossOriginCredentialChanged(state.url, hookUrl, 'password'),
});
}
}
// Publish redirect event
publishRedirect({
requestId: this._requestId,
fromUrl: url.toString(),
toUrl: (updatedOptions.url).toString(),
statusCode,
});
this.emit('redirect', updatedOptions, typedResponse);
this.options = updatedOptions;
await this._makeRequest();
}
catch (error) {
this._beforeError(normalizeError(error));
return;
}
return;
}
canFinalizeResponse = true;
handleResponseEnd();
// `HTTPError`s always have `error.response.body` defined.
// Therefore, we cannot retry if `options.throwHttpErrors` is false.
// On the last retry, if `options.throwHttpErrors` is false, we would need to return the body,
// but that wouldn't be possible since the body would be already read in `error.response.body`.
if (options.isStream && options.throwHttpErrors && !isResponseOk(typedResponse)) {
this._beforeError(new HTTPError(typedResponse));
return;
}
// `decompressResponse` wraps the response stream when it decompresses,
// so `response !== nativeResponse` indicates decompression happened.
const wasDecompressed = response !== nativeResponse;
// Store the expected content-length from the native response for validation.
// This is the content-length before decompression, which is what actually gets transferred.
// Skip storing for responses that shouldn't have bodies per RFC 9110.
// When decompression occurs, only store if strictContentLength is enabled.
if (!hasNoBody && (!wasDecompressed || options.strictContentLength)) {
const contentLengthHeader = nativeResponse.headers['content-length'];
if (contentLengthHeader !== undefined) {
const expectedLength = Number(contentLengthHeader);
if (!Number.isNaN(expectedLength) && expectedLength >= 0) {
this._expectedContentLength = expectedLength;
}
}
}
this.emit('downloadProgress', this.downloadProgress);
response.on('readable', () => {
if (this._triggerRead) {
this._read();
}
});
this.on('resume', () => {
response.resume();
});
this.on('pause', () => {
response.pause();
});
if (this._noPipe) {
const captureFromResponse = response.readableEnded || noPipeCookieJarRawBodyPromise !== undefined;
const success = noPipeCookieJarRawBodyPromise
? await noPipeCookieJarRawBodyPromise
: await this._setRawBody(captureFromResponse ? response : this);
if (captureFromResponse) {
handleResponseEnd();
}
if (success) {
this.emit('response', response);
}
return;
}
this.emit('response', response);
for (const destination of this._pipedServerResponses) {
if (destination.headersSent) {
continue;
}
for (const key in response.headers) {
if (Object.hasOwn(response.headers, key)) {
const value = response.headers[key];
// When decompression occurred, skip content-encoding and content-length
// as they refer to the compressed data, not the decompressed stream.
if (wasDecompressed && (key === 'content-encoding' || key === 'content-length')) {
continue;
}
// Skip if value is undefined
if (value !== undefined) {
destination.setHeader(key, value);
}
}
}
destination.statusCode = statusCode;
}
}
async _setRawBody(from = this) {
try {
// Errors are emitted via the `error` event
const fromArray = await from.toArray();
const hasNonStringChunk = fromArray.some(chunk => typeof chunk !== 'string');
const rawBody = hasNonStringChunk
? concatUint8Arrays(fromArray.map(chunk => typeof chunk === 'string' ? stringToUint8Array(chunk) : chunk))
: stringToUint8Array(fromArray.join(''));
const shouldUseIncrementalDecodedBody = from === this && this._incrementalDecode !== undefined;
// On retry Request is destroyed with no error, therefore the above will successfully resolve.
// So in order to check if this was really successful, we need to check if it has been properly ended.
if (!this.isAborted && this.response) {
this.response.rawBody = rawBody;
if (from !== this) {
this._downloadedSize = rawBody.byteLength;
}
if (shouldUseIncrementalDecodedBody) {
try {
const { decoder, chunks } = this._incrementalDecode;
const finalDecodedChunk = decoder.decode();
if (finalDecodedChunk.length > 0) {
chunks.push(finalDecodedChunk);
}
cacheDecodedBody(this.response, chunks.join(''));
}
catch { }
}
return true;
}
}
catch { }
finally {
this._incrementalDecode = undefined;
}
return false;
}
async _onResponse(response) {
try {
await this._onResponseBase(response);
}
catch (error) {
/* istanbul ignore next: better safe than sorry */
this._beforeError(normalizeError(error));
}
}
_onRequest(request) {
const { options } = this;
const { timeout, url } = options;
// Publish request start event
publishRequestStart({
requestId: this._requestId,
url: getSanitizedUrl(this.options),
method: options.method,
headers: options.headers,
});
timer(request);
this._cancelTimeouts = timedOut(request, timeout, url);
if (this.options.http2) {
// Unset stream timeout, as the `timeout` option was used only for connection timeout.
// We remove all 'timeout' listeners instead of calling setTimeout(0) because:
// 1. setTimeout(0) causes a memory leak (see https://github.com/sindresorhus/got/issues/690)
// 2. With HTTP/2 connection reuse, setTimeout(0) accumulates listeners on the socket
// 3. removeAllListeners('timeout') properly cleans up without the memory leak
request.removeAllListeners('timeout');
// For HTTP/2, wait for socket and remove timeout listeners from it
request.once('socket', (socket) => {
socket.removeAllListeners('timeout');
});
}
let lastRequestError;
const responseEventName = options.cache ? 'cacheableResponse' : 'response';
request.once(responseEventName, (response) => {
void this._onResponse(response);
});
const emitRequestError = (error) => {
this._aborted = true;
// Force clean-up, because some packages (e.g. nock) don't do this.
request.destroy();
const wrappedError = error instanceof TimedOutTimeoutError ? new TimeoutError(error, this.timings, this) : new RequestError(error.message, error, this);
this._beforeError(wrappedError);
};
request.once('error', (error) => {
lastRequestError = error;
// Ignore errors from requests superseded by a redirect.
if (this._request !== request) {
return;
}
/*
Tra