got 15.0.2 → 15.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,11 @@ import { generateRequestId, publishRequestCreate, publishRequestStart, publishRe
25
25
  const supportsBrotli = is.string(process.versions.brotli);
26
26
  const supportsZstd = is.string(process.versions.zstd);
27
27
  const methodsWithoutBody = new Set(['GET', 'HEAD']);
28
+ const singleValueRequestHeaders = new Set([
29
+ 'authorization',
30
+ 'content-length',
31
+ 'proxy-authorization',
32
+ ]);
28
33
  const cacheableStore = new WeakableMap();
29
34
  const redirectCodes = new Set([301, 302, 303, 307, 308]);
30
35
  export { crossOriginStripHeaders } from './options.js';
@@ -451,7 +456,13 @@ export default class Request extends Duplex {
451
456
  if (progress.percent < 1) {
452
457
  this.emit('downloadProgress', progress);
453
458
  }
459
+ if (this._stopReading) {
460
+ return;
461
+ }
454
462
  this.push(data);
463
+ if (this._stopReading) {
464
+ return;
465
+ }
455
466
  }
456
467
  }
457
468
  }
@@ -698,6 +709,9 @@ export default class Request extends Duplex {
698
709
  response = decompressResponse(response);
699
710
  typedResponse = prepareResponse(response);
700
711
  }
712
+ // `decompressResponse` wraps the response stream when it decompresses,
713
+ // so `response !== nativeResponse` indicates decompression happened.
714
+ const wasDecompressed = response !== nativeResponse;
701
715
  this._responseSize = Number(response.headers['content-length']) || undefined;
702
716
  this.response = typedResponse;
703
717
  // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -711,10 +725,26 @@ export default class Request extends Duplex {
711
725
  isFromCache: typedResponse.isFromCache,
712
726
  });
713
727
  response.once('error', (error) => {
728
+ // Node synthesizes ECONNRESET for close-delimited responses after all body
729
+ // bytes have been delivered. Only ignore that late synthetic error on the
730
+ // native response. Wrapped decompression streams surface real checksum and
731
+ // truncation failures after the underlying response has completed.
732
+ if (!wasDecompressed
733
+ && response.complete
734
+ && this._responseSize === undefined
735
+ && error.code === 'ECONNRESET') {
736
+ return;
737
+ }
714
738
  this._aborted = true;
715
739
  this._beforeError(new ReadError(error, this));
716
740
  });
717
741
  response.once('aborted', () => {
742
+ // Without Content-Length, connection close is the intended EOF signal (RFC 9110 §8.6),
743
+ // not a premature abort. For wrapped decompression streams, rely on the native
744
+ // response completion state because the wrapper strips `content-length`.
745
+ if (this._responseSize === undefined && nativeResponse.complete) {
746
+ return;
747
+ }
718
748
  this._aborted = true;
719
749
  // Check if there's a content-length mismatch to provide a more specific error
720
750
  if (!this._checkContentLengthMismatch()) {
@@ -907,9 +937,6 @@ export default class Request extends Duplex {
907
937
  this._beforeError(new HTTPError(typedResponse));
908
938
  return;
909
939
  }
910
- // `decompressResponse` wraps the response stream when it decompresses,
911
- // so `response !== nativeResponse` indicates decompression happened.
912
- const wasDecompressed = response !== nativeResponse;
913
940
  // Store the expected content-length from the native response for validation.
914
941
  // This is the content-length before decompression, which is what actually gets transferred.
915
942
  // Skip storing for responses that shouldn't have bodies per RFC 9110.
@@ -1528,11 +1555,6 @@ export default class Request extends Duplex {
1528
1555
  }
1529
1556
  async _makeRequest() {
1530
1557
  const { options } = this;
1531
- const initialHeaders = options.getInternalHeaders();
1532
- const explicitAuthorizationHeader = options.isHeaderExplicitlySet('authorization') ? initialHeaders.authorization : undefined;
1533
- const explicitCookieHeader = options.isHeaderExplicitlySet('cookie') ? initialHeaders.cookie : undefined;
1534
- const authorizationWasInitiallyOmitted = options.isHeaderExplicitlySet('authorization') && is.undefined(initialHeaders.authorization);
1535
- const cookieWasInitiallyOmitted = options.isHeaderExplicitlySet('cookie') && is.undefined(initialHeaders.cookie);
1536
1558
  const shouldDeleteGeneratedHeader = (currentHeader, generatedHeader) => currentHeader === generatedHeader || is.undefined(currentHeader);
1537
1559
  const syncGeneratedHeader = (name, { currentHeader, explicitHeader, nextHeader, staleGeneratedHeader, }) => {
1538
1560
  if (!is.undefined(nextHeader)) {
@@ -1557,6 +1579,22 @@ export default class Request extends Duplex {
1557
1579
  else if (is.null(currentHeaders[key])) {
1558
1580
  throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
1559
1581
  }
1582
+ else if (Array.isArray(currentHeaders[key]) && key === 'transfer-encoding') {
1583
+ // Node serializes request header arrays as repeated field lines. Keep framing
1584
+ // unambiguous by allowing only one transfer-encoding value here.
1585
+ if (currentHeaders[key].length !== 1) {
1586
+ throw new TypeError(`The \`${key}\` header must be a single value`);
1587
+ }
1588
+ options.setInternalHeader(key, currentHeaders[key][0]);
1589
+ }
1590
+ else if (Array.isArray(currentHeaders[key]) && singleValueRequestHeaders.has(key)) {
1591
+ // Duplicate credential and content-length lines are not allowed on requests.
1592
+ // Normalize a single-element array to match the long-supported string path.
1593
+ if (currentHeaders[key].length !== 1) {
1594
+ throw new TypeError(`The \`${key}\` header must be a single value`);
1595
+ }
1596
+ options.setInternalHeader(key, currentHeaders[key][0]);
1597
+ }
1560
1598
  }
1561
1599
  return currentHeaders;
1562
1600
  };
@@ -1568,6 +1606,12 @@ export default class Request extends Duplex {
1568
1606
  return is.nonEmptyString(cookieString) ? cookieString : undefined;
1569
1607
  };
1570
1608
  const headers = sanitizeHeaders();
1609
+ const initialHeaders = options.getInternalHeaders();
1610
+ const authorizationWasInitiallyExplicit = options.isHeaderExplicitlySet('authorization');
1611
+ const explicitAuthorizationHeader = authorizationWasInitiallyExplicit ? initialHeaders.authorization : undefined;
1612
+ const explicitCookieHeader = options.isHeaderExplicitlySet('cookie') ? initialHeaders.cookie : undefined;
1613
+ const authorizationWasInitiallyOmitted = options.isHeaderExplicitlySet('authorization') && is.undefined(initialHeaders.authorization);
1614
+ const cookieWasInitiallyOmitted = options.isHeaderExplicitlySet('cookie') && is.undefined(initialHeaders.cookie);
1571
1615
  if (options.decompress && is.undefined(headers['accept-encoding'])) {
1572
1616
  const encodings = ['gzip', 'deflate'];
1573
1617
  if (supportsBrotli) {
@@ -1580,7 +1624,11 @@ export default class Request extends Duplex {
1580
1624
  }
1581
1625
  const { username, password } = options;
1582
1626
  const cookieJar = options.cookieJar;
1583
- const generatedAuthorizationHeader = getAuthorizationHeader(username, password, authorizationWasInitiallyOmitted);
1627
+ // Preserve an explicit Authorization header over URL-derived Basic auth. This keeps
1628
+ // normalized single-element arrays aligned with the long-supported string behavior.
1629
+ const generatedAuthorizationHeader = is.undefined(explicitAuthorizationHeader)
1630
+ ? getAuthorizationHeader(username, password, authorizationWasInitiallyOmitted)
1631
+ : undefined;
1584
1632
  let generatedCookieHeader;
1585
1633
  if (!is.undefined(generatedAuthorizationHeader)) {
1586
1634
  options.setInternalHeader('authorization', generatedAuthorizationHeader);
@@ -1610,11 +1658,21 @@ export default class Request extends Duplex {
1610
1658
  // `headers.authorization = undefined` / `headers.cookie = undefined` is an
1611
1659
  // explicit opt-out. Respect that instead of regenerating values from URL
1612
1660
  // credentials or the cookie jar later in request setup.
1613
- const isHeaderExplicitlyOmitted = (header) => options.isHeaderExplicitlySet(header) && is.undefined(currentHeaders[header]);
1614
- const authorizationWasExplicitlyOmitted = isHeaderExplicitlyOmitted('authorization');
1615
- const cookieWasExplicitlyOmitted = isHeaderExplicitlyOmitted('cookie');
1661
+ const isHeaderExplicitlyOmitted = (header) => options.isHeaderExplicitlySet(header)
1662
+ && Object.hasOwn(currentHeaders, header)
1663
+ && is.undefined(currentHeaders[header]);
1616
1664
  const currentAuthorizationHeader = currentHeaders.authorization;
1617
1665
  const currentCookieHeader = currentHeaders.cookie;
1666
+ // Authorization follows a small contract:
1667
+ // - A concrete Authorization header is sent as-is.
1668
+ // - `authorization = undefined` means omit Authorization entirely, including URL auth.
1669
+ // - Deleting an Authorization header that started explicit also means omit it.
1670
+ // - Otherwise, if the request did not start with explicit Authorization, Got may
1671
+ // generate Basic auth from the current username/password.
1672
+ const authorizationWasExplicitlyOmitted = isHeaderExplicitlyOmitted('authorization')
1673
+ || (authorizationWasInitiallyExplicit && is.undefined(currentAuthorizationHeader));
1674
+ const cookieWasExplicitlyOmitted = is.undefined(currentCookieHeader)
1675
+ && (cookieWasInitiallyOmitted || isHeaderExplicitlyOmitted('cookie'));
1618
1676
  sanitizeHeaders();
1619
1677
  if (!is.undefined(currentHeaders['transfer-encoding']) && !is.undefined(currentHeaders['content-length'])) {
1620
1678
  options.deleteInternalHeader('content-length');
@@ -1626,15 +1684,22 @@ export default class Request extends Duplex {
1626
1684
  delete options.headers.authorization;
1627
1685
  }
1628
1686
  }
1629
- const authorizationHeader = getAuthorizationHeader(options.username, options.password, authorizationWasExplicitlyOmitted);
1687
+ const authorizationHeader = !authorizationWasInitiallyExplicit
1688
+ && !authorizationWasInitiallyOmitted
1689
+ && !authorizationWasExplicitlyOmitted
1690
+ ? getAuthorizationHeader(options.username, options.password, authorizationWasExplicitlyOmitted)
1691
+ : undefined;
1630
1692
  const cookieJar = options.cookieJar;
1631
- if (changedState.has('authorization')) {
1693
+ if (changedState.has('authorization') && !is.undefined(currentAuthorizationHeader)) {
1632
1694
  // A beforeRequest hook intentionally set the outgoing Authorization header.
1633
1695
  }
1634
1696
  else {
1697
+ const restorableAuthorizationHeader = changedState.has('authorization') && is.undefined(currentAuthorizationHeader)
1698
+ ? undefined
1699
+ : explicitAuthorizationHeader;
1635
1700
  syncGeneratedHeader('authorization', {
1636
1701
  currentHeader: currentAuthorizationHeader,
1637
- explicitHeader: explicitAuthorizationHeader,
1702
+ explicitHeader: restorableAuthorizationHeader,
1638
1703
  nextHeader: authorizationHeader,
1639
1704
  staleGeneratedHeader: generatedAuthorizationHeader,
1640
1705
  });
@@ -1649,10 +1714,13 @@ export default class Request extends Duplex {
1649
1714
  // A beforeRequest hook intentionally set the outgoing Cookie header.
1650
1715
  }
1651
1716
  else {
1717
+ const cookieHeader = !cookieWasInitiallyOmitted && !cookieWasExplicitlyOmitted
1718
+ ? await getCookieHeader(cookieJar)
1719
+ : undefined;
1652
1720
  syncGeneratedHeader('cookie', {
1653
1721
  currentHeader: currentCookieHeader,
1654
1722
  explicitHeader: explicitCookieHeader,
1655
- nextHeader: await getCookieHeader(cookieJar),
1723
+ nextHeader: cookieHeader,
1656
1724
  staleGeneratedHeader: generatedCookieHeader,
1657
1725
  });
1658
1726
  }
@@ -1464,6 +1464,7 @@ export default class Options {
1464
1464
  set strictContentLength(value: boolean);
1465
1465
  toJSON(): {
1466
1466
  timeout: Delays;
1467
+ localAddress: string | undefined;
1467
1468
  headers: Headers;
1468
1469
  request: RequestFunction | undefined;
1469
1470
  json: unknown;
@@ -1496,7 +1497,6 @@ export default class Options {
1496
1497
  dnsLookupIpVersion: DnsLookupIpVersion;
1497
1498
  parseJson: ParseJsonFunction;
1498
1499
  stringifyJson: StringifyJsonFunction;
1499
- localAddress: string | undefined;
1500
1500
  method: Method;
1501
1501
  createConnection: CreateConnectionFunction | undefined;
1502
1502
  cacheOptions: CacheOptions;
@@ -93,13 +93,14 @@ function assertValidHeaderName(name) {
93
93
  Safely assign own properties from source to target, skipping `__proto__` to prevent prototype pollution from JSON.parse'd input.
94
94
  */
95
95
  function safeObjectAssign(target, source) {
96
- for (const key of Object.keys(source)) {
96
+ for (const [key, value] of Object.entries(source)) {
97
97
  if (key === '__proto__') {
98
98
  continue;
99
99
  }
100
- target[key] = source[key];
100
+ Reflect.set(target, key, value);
101
101
  }
102
102
  }
103
+ const isToughCookieJar = (cookieJar) => cookieJar.setCookie.length === 4 && cookieJar.getCookieString.length === 0;
103
104
  function validateSearchParameters(searchParameters) {
104
105
  for (const key of Object.keys(searchParameters)) {
105
106
  if (key === '__proto__') {
@@ -916,10 +917,10 @@ export default class Options {
916
917
  assert.function(setCookie);
917
918
  assert.function(getCookieString);
918
919
  /* istanbul ignore next: Horrible `tough-cookie` v3 check */
919
- if (setCookie.length === 4 && getCookieString.length === 0) {
920
+ if (isToughCookieJar(value)) {
920
921
  this.#internals.cookieJar = {
921
- setCookie: promisify(setCookie.bind(value)),
922
- getCookieString: promisify(getCookieString.bind(value)),
922
+ setCookie: promisify(value.setCookie.bind(value)),
923
+ getCookieString: promisify(value.getCookieString.bind(value)),
923
924
  };
924
925
  }
925
926
  else {
@@ -1,6 +1,10 @@
1
1
  import { errorMonitor } from 'node:events';
2
2
  import { types } from 'node:util';
3
3
  import deferToConnect from './defer-to-connect.js';
4
+ const getInitialConnectionTimings = (socket) => Reflect.get(socket, '__initial_connection_timings__');
5
+ const setInitialConnectionTimings = (socket, timings) => {
6
+ Reflect.set(socket, '__initial_connection_timings__', timings);
7
+ };
4
8
  const timer = (request) => {
5
9
  if (request.timings) {
6
10
  return request.timings;
@@ -58,11 +62,12 @@ const timer = (request) => {
58
62
  // original connection so they're not lost.
59
63
  timings.lookup = timings.socket;
60
64
  timings.connect = timings.socket;
61
- if (socket.__initial_connection_timings__) {
65
+ const initialConnectionTimings = getInitialConnectionTimings(socket);
66
+ if (initialConnectionTimings) {
62
67
  // Restore the phase timings from the initial connection
63
- timings.phases.dns = socket.__initial_connection_timings__.dnsPhase;
64
- timings.phases.tcp = socket.__initial_connection_timings__.tcpPhase;
65
- timings.phases.tls = socket.__initial_connection_timings__.tlsPhase;
68
+ timings.phases.dns = initialConnectionTimings.dnsPhase;
69
+ timings.phases.tcp = initialConnectionTimings.tcpPhase;
70
+ timings.phases.tls = initialConnectionTimings.tlsPhase;
66
71
  // Set secureConnect timestamp if there was TLS
67
72
  if (timings.phases.tls !== undefined) {
68
73
  timings.secureConnect = timings.socket;
@@ -100,18 +105,21 @@ const timer = (request) => {
100
105
  timings.phases.dns = 0;
101
106
  }
102
107
  // Store connection phase timings on socket for potential reuse
103
- socket.__initial_connection_timings__ ??= {
104
- dnsPhase: timings.phases.dns,
105
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript can't prove this is defined due to callback structure
106
- tcpPhase: timings.phases.tcp,
107
- };
108
+ if (!getInitialConnectionTimings(socket)) {
109
+ setInitialConnectionTimings(socket, {
110
+ dnsPhase: timings.phases.dns,
111
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript can't prove this is defined due to callback structure
112
+ tcpPhase: timings.phases.tcp,
113
+ });
114
+ }
108
115
  },
109
116
  secureConnect() {
110
117
  timings.secureConnect = Date.now();
111
118
  timings.phases.tls = timings.secureConnect - timings.connect;
112
119
  // Update stored timings with TLS phase timing
113
- if (socket.__initial_connection_timings__) {
114
- socket.__initial_connection_timings__.tlsPhase = timings.phases.tls;
120
+ const initialConnectionTimings = getInitialConnectionTimings(socket);
121
+ if (initialConnectionTimings) {
122
+ initialConnectionTimings.tlsPhase = timings.phases.tls;
115
123
  }
116
124
  },
117
125
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "got",
3
- "version": "15.0.2",
3
+ "version": "15.0.4",
4
4
  "description": "Human-friendly and powerful HTTP request library for Node.js",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/got",
@@ -51,7 +51,7 @@
51
51
  "ky"
52
52
  ],
53
53
  "dependencies": {
54
- "@sindresorhus/is": "^7.2.0",
54
+ "@sindresorhus/is": "^8.0.0",
55
55
  "byte-counter": "^0.1.0",
56
56
  "cacheable-lookup": "^7.0.0",
57
57
  "cacheable-request": "^13.0.18",
@@ -61,27 +61,27 @@
61
61
  "keyv": "^5.6.0",
62
62
  "lowercase-keys": "^4.0.1",
63
63
  "responselike": "^4.0.2",
64
- "type-fest": "^5.4.4",
64
+ "type-fest": "^5.6.0",
65
65
  "uint8array-extras": "^1.5.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@hapi/bourne": "^3.0.0",
69
69
  "@sindresorhus/tsconfig": "^8.1.0",
70
- "@sinonjs/fake-timers": "^15.1.0",
70
+ "@sinonjs/fake-timers": "^15.3.2",
71
71
  "@types/benchmark": "^2.1.5",
72
72
  "@types/express": "^5.0.6",
73
- "@types/node": "^25.3.0",
73
+ "@types/node": "^25.6.0",
74
74
  "@types/pem": "^1.14.4",
75
75
  "@types/readable-stream": "^4.0.23",
76
76
  "@types/request": "^2.48.13",
77
77
  "@types/sinon": "^21.0.0",
78
78
  "@types/sinonjs__fake-timers": "^15.0.1",
79
79
  "ava": "^6.4.1",
80
- "axios": "^1.13.5",
80
+ "axios": "^1.15.1",
81
81
  "benchmark": "^2.1.4",
82
82
  "bluebird": "^3.7.2",
83
83
  "body-parser": "^2.2.2",
84
- "c8": "^10.1.3",
84
+ "c8": "^11.0.0",
85
85
  "create-cert": "^1.0.6",
86
86
  "create-test-server": "^3.0.1",
87
87
  "del-cli": "^7.0.0",
@@ -90,14 +90,14 @@
90
90
  "express": "^5.2.1",
91
91
  "get-stream": "^9.0.1",
92
92
  "node-fetch": "^3.3.2",
93
- "np": "^11.0.2",
93
+ "np": "^11.2.0",
94
94
  "p-event": "^7.1.0",
95
95
  "pem": "^1.14.8",
96
96
  "pify": "^6.1.0",
97
97
  "quick-lru": "^7.3.0",
98
98
  "readable-stream": "^4.7.0",
99
99
  "request": "^2.88.2",
100
- "sinon": "^21.0.1",
100
+ "sinon": "^21.1.2",
101
101
  "slow-stream": "0.0.4",
102
102
  "tempy": "^3.2.0",
103
103
  "then-busboy": "^5.2.1",