diff --git a/deps/undici/src/docs/best-practices/mocking-request.md b/deps/undici/src/docs/best-practices/mocking-request.md index b98a450a32e29b..695439274449a5 100644 --- a/deps/undici/src/docs/best-practices/mocking-request.md +++ b/deps/undici/src/docs/best-practices/mocking-request.md @@ -1,6 +1,6 @@ # Mocking Request -Undici have its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP request and return mocked value instead. It can be useful for testing purposes. +Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes. Example: @@ -8,7 +8,7 @@ Example: // bank.mjs import { request } from 'undici' -export async function bankTransfer(recepient, amount) { +export async function bankTransfer(recipient, amount) { const { body } = await request('http://localhost:3000/bank-transfer', { method: 'POST', @@ -16,7 +16,7 @@ export async function bankTransfer(recepient, amount) { 'X-TOKEN-SECRET': 'SuperSecretToken', }, body: JSON.stringify({ - recepient, + recipient, amount }) } @@ -48,7 +48,7 @@ mockPool.intercept({ 'X-TOKEN-SECRET': 'SuperSecretToken', }, body: JSON.stringify({ - recepient: '1234567890', + recipient: '1234567890', amount: '100' }) }).reply(200, { @@ -77,7 +77,7 @@ Explore other MockAgent functionality [here](../api/MockAgent.md) ## Debug Mock Value -When the interceptor we wrote are not the same undici will automatically call real HTTP request. To debug our mock value use `mockAgent.disableNetConnect()` +When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`: ```js const mockAgent = new MockAgent(); @@ -89,7 +89,7 @@ mockAgent.disableNetConnect() const mockPool = mockAgent.get('http://localhost:3000'); mockPool.intercept({ - path: '/bank-tanfer', + path: '/bank-transfer', method: 'POST', }).reply(200, { message: 'transaction processed' @@ -103,7 +103,7 @@ const badRequest = await bankTransfer('1234567890', '100') ## Reply with data based on request -If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply` +If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`: ```js mockPool.intercept({ @@ -113,7 +113,7 @@ mockPool.intercept({ 'X-TOKEN-SECRET': 'SuperSecretToken', }, body: JSON.stringify({ - recepient: '1234567890', + recipient: '1234567890', amount: '100' }) }).reply(200, (opts) => { @@ -129,7 +129,7 @@ in this case opts will be { method: 'POST', headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' }, - body: '{"recepient":"1234567890","amount":"100"}', + body: '{"recipient":"1234567890","amount":"100"}', origin: 'http://localhost:3000', path: '/bank-transfer' } diff --git a/deps/undici/src/lib/core/connect.js b/deps/undici/src/lib/core/connect.js index 57667a1314afa0..e9b456f8831922 100644 --- a/deps/undici/src/lib/core/connect.js +++ b/deps/undici/src/lib/core/connect.js @@ -75,14 +75,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { }) } - const timeoutId = timeout - ? setTimeout(onConnectTimeout, timeout, socket) - : null + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) socket .setNoDelay(true) .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { - clearTimeout(timeoutId) + cancelTimeout() if (callback) { const cb = callback @@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { } }) .on('error', function (err) { - clearTimeout(timeoutId) + cancelTimeout() if (callback) { const cb = callback @@ -104,6 +102,31 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { } } +function setupTimeout (onConnectTimeout, timeout) { + if (!timeout) { + return () => {} + } + + let s1 = null + let s2 = null + const timeoutId = setTimeout(() => { + // setImmediate is added to make sure that we priotorise socket error events over timeouts + s1 = setImmediate(() => { + if (process.platform === 'win32') { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout()) + } else { + onConnectTimeout() + } + }) + }, timeout) + return () => { + clearTimeout(timeoutId) + clearImmediate(s1) + clearImmediate(s2) + } +} + function onConnectTimeout (socket) { util.destroy(socket, new ConnectTimeoutError()) } diff --git a/deps/undici/src/lib/fetch/body.js b/deps/undici/src/lib/fetch/body.js index 2a9f1c83d888d2..08d22310a38db5 100644 --- a/deps/undici/src/lib/fetch/body.js +++ b/deps/undici/src/lib/fetch/body.js @@ -404,7 +404,18 @@ function bodyMixinMethods (instance) { // 1. Let entries be the result of parsing bytes. let entries try { - entries = new URLSearchParams(await this.text()) + let text = '' + // application/x-www-form-urlencoded parser will keep the BOM. + // https://url.spec.whatwg.org/#concept-urlencoded-parser + const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError('Expected Uint8Array chunk') + } + text += textDecoder.decode(chunk, { stream: true }) + } + text += textDecoder.decode() + entries = new URLSearchParams(text) } catch (err) { // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. // 2. If entries is failure, then throw a TypeError. diff --git a/deps/undici/src/lib/fetch/dataURL.js b/deps/undici/src/lib/fetch/dataURL.js index 5eb0a514aed232..cad44853e1657e 100644 --- a/deps/undici/src/lib/fetch/dataURL.js +++ b/deps/undici/src/lib/fetch/dataURL.js @@ -255,7 +255,7 @@ function percentDecode (input) { } // 3. Return output. - return Uint8Array.of(...output) + return Uint8Array.from(output) } // https://mimesniff.spec.whatwg.org/#parse-a-mime-type diff --git a/deps/undici/src/lib/fetch/index.js b/deps/undici/src/lib/fetch/index.js index cf91a5d378e98b..f9b09547edbc82 100644 --- a/deps/undici/src/lib/fetch/index.js +++ b/deps/undici/src/lib/fetch/index.js @@ -33,7 +33,8 @@ const { isBlobLike, sameOrigin, isCancelled, - isAborted + isAborted, + isErrorLike } = require('./util') const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const assert = require('assert') @@ -1854,7 +1855,7 @@ async function httpNetworkFetch ( timingInfo.decodedBodySize += bytes?.byteLength ?? 0 // 6. If bytes is failure, then terminate fetchParams’s controller. - if (bytes instanceof Error) { + if (isErrorLike(bytes)) { fetchParams.controller.terminate(bytes) return } @@ -1894,7 +1895,7 @@ async function httpNetworkFetch ( // 3. Otherwise, if stream is readable, error stream with a TypeError. if (isReadable(stream)) { fetchParams.controller.controller.error(new TypeError('terminated', { - cause: reason instanceof Error ? reason : undefined + cause: isErrorLike(reason) ? reason : undefined })) } } @@ -1942,14 +1943,17 @@ async function httpNetworkFetch ( } let codings = [] + let location = '' const headers = new Headers() for (let n = 0; n < headersList.length; n += 2) { - const key = headersList[n + 0].toString() - const val = headersList[n + 1].toString() + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') if (key.toLowerCase() === 'content-encoding') { codings = val.split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val } headers.append(key, val) @@ -1960,7 +1964,7 @@ async function httpNetworkFetch ( const decoders = [] // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding - if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) { + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !(request.redirect === 'follow' && location)) { for (const coding of codings) { if (/(x-)?gzip/.test(coding)) { decoders.push(zlib.createGunzip()) @@ -1980,7 +1984,7 @@ async function httpNetworkFetch ( statusText, headersList: headers[kHeadersList], body: decoders.length - ? pipeline(this.body, ...decoders, () => {}) + ? pipeline(this.body, ...decoders, () => { }) : this.body.on('error', () => {}) }) diff --git a/deps/undici/src/lib/fetch/request.js b/deps/undici/src/lib/fetch/request.js index 7e1b3d8eb1d009..7fda8d90b28bd9 100644 --- a/deps/undici/src/lib/fetch/request.js +++ b/deps/undici/src/lib/fetch/request.js @@ -367,9 +367,9 @@ class Request { } if (signal.aborted) { - ac.abort() + ac.abort(signal.reason) } else { - const abort = () => ac.abort() + const abort = () => ac.abort(signal.reason) signal.addEventListener('abort', abort, { once: true }) requestFinalizer.register(this, { signal, abort }) } @@ -726,12 +726,12 @@ class Request { // 4. Make clonedRequestObject’s signal follow this’s signal. const ac = new AbortController() if (this.signal.aborted) { - ac.abort() + ac.abort(this.signal.reason) } else { this.signal.addEventListener( 'abort', - function () { - ac.abort() + () => { + ac.abort(this.signal.reason) }, { once: true } ) diff --git a/deps/undici/src/lib/fetch/response.js b/deps/undici/src/lib/fetch/response.js index 4649a5da907029..526259478d4099 100644 --- a/deps/undici/src/lib/fetch/response.js +++ b/deps/undici/src/lib/fetch/response.js @@ -10,7 +10,8 @@ const { isCancelled, isAborted, isBlobLike, - serializeJavascriptValueToJSONString + serializeJavascriptValueToJSONString, + isErrorLike } = require('./util') const { redirectStatus, @@ -347,15 +348,15 @@ function makeResponse (init) { } function makeNetworkError (reason) { + const isError = isErrorLike(reason) return makeResponse({ type: 'error', status: 0, - error: - reason instanceof Error - ? reason - : new Error(reason ? String(reason) : reason, { - cause: reason instanceof Error ? reason : undefined - }), + error: isError + ? reason + : new Error(reason ? String(reason) : reason, { + cause: isError ? reason : undefined + }), aborted: reason && reason.name === 'AbortError' }) } diff --git a/deps/undici/src/lib/fetch/util.js b/deps/undici/src/lib/fetch/util.js index 17c68162980f76..9806e331871c90 100644 --- a/deps/undici/src/lib/fetch/util.js +++ b/deps/undici/src/lib/fetch/util.js @@ -82,6 +82,13 @@ function isFileLike (object) { ) } +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + // Check whether |statusText| is a ByteString and // matches the Reason-Phrase token production. // RFC 2616: https://tools.ietf.org/html/rfc2616 @@ -469,5 +476,6 @@ module.exports = { makeIterator, isValidHeaderName, isValidHeaderValue, - hasOwn + hasOwn, + isErrorLike } diff --git a/deps/undici/src/lib/fetch/webidl.js b/deps/undici/src/lib/fetch/webidl.js index f9a780ccaa74b6..293199e8f9b4f5 100644 --- a/deps/undici/src/lib/fetch/webidl.js +++ b/deps/undici/src/lib/fetch/webidl.js @@ -388,8 +388,9 @@ webidl.converters.DOMString = function (V, opts = {}) { return String(V) } +// Check for 0 or more characters outside of the latin1 range. // eslint-disable-next-line no-control-regex -const isNotLatin1 = /[^\u0000-\u00ff]/ +const isLatin1 = /^[\u0000-\u00ff]{0,}$/ // https://webidl.spec.whatwg.org/#es-ByteString webidl.converters.ByteString = function (V) { @@ -399,7 +400,7 @@ webidl.converters.ByteString = function (V) { // 2. If the value of any element of x is greater than // 255, then throw a TypeError. - if (isNotLatin1.test(x)) { + if (!isLatin1.test(x)) { throw new TypeError('Argument is not a ByteString') } diff --git a/deps/undici/src/lib/mock/mock-utils.js b/deps/undici/src/lib/mock/mock-utils.js index 80052223f8fb40..7e115f83b4d535 100644 --- a/deps/undici/src/lib/mock/mock-utils.js +++ b/deps/undici/src/lib/mock/mock-utils.js @@ -38,7 +38,7 @@ function lowerCaseEntries (headers) { function getHeaderByName (headers, key) { if (Array.isArray(headers)) { for (let i = 0; i < headers.length; i += 2) { - if (headers[i] === key) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { return headers[i + 1] } } @@ -47,19 +47,24 @@ function getHeaderByName (headers, key) { } else if (typeof headers.get === 'function') { return headers.get(key) } else { - return headers[key] + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] } } +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + function matchHeaders (mockDispatch, headers) { if (typeof mockDispatch.headers === 'function') { if (Array.isArray(headers)) { // fetch HeadersList - const clone = headers.slice() - const entries = [] - for (let index = 0; index < clone.length; index += 2) { - entries.push([clone[index], clone[index + 1]]) - } - headers = Object.fromEntries(entries) + headers = buildHeadersFromArray(headers) } return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) } @@ -284,7 +289,13 @@ function mockDispatch (opts, handler) { } function handleReply (mockDispatches) { - const responseData = getResponseData(typeof data === 'function' ? data(opts) : data) + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const responseData = getResponseData( + typeof data === 'function' ? data({ ...opts, headers: optsHeaders }) : data + ) const responseHeaders = generateKeyValues(headers) const responseTrailers = generateKeyValues(trailers) diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 1fde040055fadc..4ac0eb8863d149 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.8.0", + "version": "5.8.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -67,6 +67,7 @@ "@sinonjs/fake-timers": "^9.1.2", "@types/node": "^17.0.29", "abort-controller": "^3.0.0", + "atomic-sleep": "^1.0.0", "busboy": "^1.6.0", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", diff --git a/deps/undici/src/types/mock-interceptor.d.ts b/deps/undici/src/types/mock-interceptor.d.ts index 8812960573f3e3..87eedcd406053f 100644 --- a/deps/undici/src/types/mock-interceptor.d.ts +++ b/deps/undici/src/types/mock-interceptor.d.ts @@ -74,7 +74,7 @@ declare namespace MockInterceptor { origin: string; method: string; body?: BodyInit | Dispatcher.DispatchOptions['body']; - headers: Headers; + headers: Headers | Record; maxRedirections: number; } diff --git a/deps/undici/src/types/pool-stats.d.ts b/deps/undici/src/types/pool-stats.d.ts new file mode 100644 index 00000000000000..807e68f1b81dad --- /dev/null +++ b/deps/undici/src/types/pool-stats.d.ts @@ -0,0 +1,19 @@ +import Pool = require("./pool") + +export = PoolStats + +declare class PoolStats { + constructor(pool: Pool); + /** Number of open socket connections in this pool. */ + connected: number; + /** Number of open socket connections in this pool that do not have an active request. */ + free: number; + /** Number of pending requests across all clients in this pool. */ + pending: number; + /** Number of queued requests across all clients in this pool. */ + queued: number; + /** Number of currently active requests across all clients in this pool. */ + running: number; + /** Number of active, pending, or queued requests across all clients in this pool. */ + size: number; +} diff --git a/deps/undici/src/types/pool.d.ts b/deps/undici/src/types/pool.d.ts index 82aeb376cd23fc..af7fb94a9a68d8 100644 --- a/deps/undici/src/types/pool.d.ts +++ b/deps/undici/src/types/pool.d.ts @@ -1,5 +1,6 @@ import Client = require('./client') import Dispatcher = require('./dispatcher') +import TPoolStats = require('./pool-stats') import { URL } from 'url' export = Pool @@ -10,9 +11,12 @@ declare class Pool extends Dispatcher { closed: boolean; /** `true` after `pool.destroyed()` has been called or `pool.close()` has been called and the pool shutdown has completed. */ destroyed: boolean; + /** Aggregate stats for a Pool. */ + readonly stats: TPoolStats; } declare namespace Pool { + export type PoolStats = TPoolStats; export interface Options extends Client.Options { /** Default: `(origin, opts) => new Client(origin, opts)`. */ factory?(origin: URL, opts: object): Dispatcher; diff --git a/deps/undici/undici.js b/deps/undici/undici.js index f59f177536a324..123b51b4d3064c 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -1281,10 +1281,10 @@ var require_webidl = __commonJS({ } return String(V); }; - var isNotLatin1 = /[^\u0000-\u00ff]/; + var isLatin1 = /^[\u0000-\u00ff]{0,}$/; webidl.converters.ByteString = function(V) { const x = webidl.converters.DOMString(V); - if (isNotLatin1.test(x)) { + if (!isLatin1.test(x)) { throw new TypeError("Argument is not a ByteString"); } return x; @@ -1671,6 +1671,9 @@ var require_util2 = __commonJS({ } return object instanceof File || object && (typeof object.stream === "function" || typeof object.arrayBuffer === "function") && /^(File)$/.test(object[Symbol.toStringTag]); } + function isErrorLike(object) { + return object instanceof Error || (object?.constructor?.name === "Error" || object?.constructor?.name === "DOMException"); + } function isValidReasonPhrase(statusText) { for (let i = 0; i < statusText.length; ++i) { const c = statusText.charCodeAt(i); @@ -1877,7 +1880,8 @@ var require_util2 = __commonJS({ makeIterator, isValidHeaderName, isValidHeaderValue, - hasOwn + hasOwn, + isErrorLike }; } }); @@ -2300,7 +2304,16 @@ Content-Type: ${value.type || "application/octet-stream"}\r } else if (/application\/x-www-form-urlencoded/.test(contentType)) { let entries; try { - entries = new URLSearchParams(await this.text()); + let text = ""; + const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }); + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError("Expected Uint8Array chunk"); + } + text += textDecoder.decode(chunk, { stream: true }); + } + text += textDecoder.decode(); + entries = new URLSearchParams(text); } catch (err) { throw Object.assign(new TypeError(), { cause: err }); } @@ -2792,16 +2805,16 @@ var require_connect = __commonJS({ host: hostname }); } - const timeoutId = timeout ? setTimeout(onConnectTimeout, timeout, socket) : null; + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout); socket.setNoDelay(true).once(protocol === "https:" ? "secureConnect" : "connect", function() { - clearTimeout(timeoutId); + cancelTimeout(); if (callback) { const cb = callback; callback = null; cb(null, this); } }).on("error", function(err) { - clearTimeout(timeoutId); + cancelTimeout(); if (callback) { const cb = callback; callback = null; @@ -2811,6 +2824,28 @@ var require_connect = __commonJS({ return socket; }; } + function setupTimeout(onConnectTimeout2, timeout) { + if (!timeout) { + return () => { + }; + } + let s1 = null; + let s2 = null; + const timeoutId = setTimeout(() => { + s1 = setImmediate(() => { + if (process.platform === "win32") { + s2 = setImmediate(() => onConnectTimeout2()); + } else { + onConnectTimeout2(); + } + }); + }, timeout); + return () => { + clearTimeout(timeoutId); + clearImmediate(s1); + clearImmediate(s2); + }; + } function onConnectTimeout(socket) { util.destroy(socket, new ConnectTimeoutError()); } @@ -5047,7 +5082,8 @@ var require_response = __commonJS({ isCancelled, isAborted, isBlobLike, - serializeJavascriptValueToJSONString + serializeJavascriptValueToJSONString, + isErrorLike } = require_util2(); var { redirectStatus, @@ -5245,11 +5281,12 @@ var require_response = __commonJS({ }; } function makeNetworkError(reason) { + const isError = isErrorLike(reason); return makeResponse({ type: "error", status: 0, - error: reason instanceof Error ? reason : new Error(reason ? String(reason) : reason, { - cause: reason instanceof Error ? reason : void 0 + error: isError ? reason : new Error(reason ? String(reason) : reason, { + cause: isError ? reason : void 0 }), aborted: reason && reason.name === "AbortError" }); @@ -5586,9 +5623,9 @@ var require_request2 = __commonJS({ throw new TypeError("Failed to construct 'Request': member signal is not of type AbortSignal."); } if (signal.aborted) { - ac.abort(); + ac.abort(signal.reason); } else { - const abort = () => ac.abort(); + const abort = () => ac.abort(signal.reason); signal.addEventListener("abort", abort, { once: true }); requestFinalizer.register(this, { signal, abort }); } @@ -5767,10 +5804,10 @@ var require_request2 = __commonJS({ clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]; const ac = new AbortController(); if (this.signal.aborted) { - ac.abort(); + ac.abort(this.signal.reason); } else { - this.signal.addEventListener("abort", function() { - ac.abort(); + this.signal.addEventListener("abort", () => { + ac.abort(this.signal.reason); }, { once: true }); } clonedRequestObject[kSignal] = ac.signal; @@ -6035,7 +6072,7 @@ var require_dataURL = __commonJS({ i += 2; } } - return Uint8Array.of(...output); + return Uint8Array.from(output); } function parseMIMEType(input) { input = input.trim(); @@ -6182,7 +6219,8 @@ var require_fetch = __commonJS({ isBlobLike, sameOrigin, isCancelled, - isAborted + isAborted, + isErrorLike } = require_util2(); var { kState, kHeaders, kGuard, kRealm } = require_symbols2(); var assert = require("assert"); @@ -6945,7 +6983,7 @@ var require_fetch = __commonJS({ return; } timingInfo.decodedBodySize += bytes?.byteLength ?? 0; - if (bytes instanceof Error) { + if (isErrorLike(bytes)) { fetchParams.controller.terminate(bytes); return; } @@ -6968,7 +7006,7 @@ var require_fetch = __commonJS({ } else { if (isReadable(stream)) { fetchParams.controller.controller.error(new TypeError("terminated", { - cause: reason instanceof Error ? reason : void 0 + cause: isErrorLike(reason) ? reason : void 0 })); } } @@ -7003,18 +7041,21 @@ var require_fetch = __commonJS({ return; } let codings = []; + let location = ""; const headers = new Headers(); for (let n = 0; n < headersList.length; n += 2) { - const key = headersList[n + 0].toString(); - const val = headersList[n + 1].toString(); + const key = headersList[n + 0].toString("latin1"); + const val = headersList[n + 1].toString("latin1"); if (key.toLowerCase() === "content-encoding") { codings = val.split(",").map((x) => x.trim()); + } else if (key.toLowerCase() === "location") { + location = val; } headers.append(key, val); } this.body = new Readable({ read: resume }); const decoders = []; - if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status)) { + if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !(request.redirect === "follow" && location)) { for (const coding of codings) { if (/(x-)?gzip/.test(coding)) { decoders.push(zlib.createGunzip());