got 15.0.2 → 15.0.3

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';
@@ -698,6 +703,9 @@ export default class Request extends Duplex {
698
703
  response = decompressResponse(response);
699
704
  typedResponse = prepareResponse(response);
700
705
  }
706
+ // `decompressResponse` wraps the response stream when it decompresses,
707
+ // so `response !== nativeResponse` indicates decompression happened.
708
+ const wasDecompressed = response !== nativeResponse;
701
709
  this._responseSize = Number(response.headers['content-length']) || undefined;
702
710
  this.response = typedResponse;
703
711
  // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -711,10 +719,26 @@ export default class Request extends Duplex {
711
719
  isFromCache: typedResponse.isFromCache,
712
720
  });
713
721
  response.once('error', (error) => {
722
+ // Node synthesizes ECONNRESET for close-delimited responses after all body
723
+ // bytes have been delivered. Only ignore that late synthetic error on the
724
+ // native response. Wrapped decompression streams surface real checksum and
725
+ // truncation failures after the underlying response has completed.
726
+ if (!wasDecompressed
727
+ && response.complete
728
+ && this._responseSize === undefined
729
+ && error.code === 'ECONNRESET') {
730
+ return;
731
+ }
714
732
  this._aborted = true;
715
733
  this._beforeError(new ReadError(error, this));
716
734
  });
717
735
  response.once('aborted', () => {
736
+ // Without Content-Length, connection close is the intended EOF signal (RFC 9110 §8.6),
737
+ // not a premature abort. For wrapped decompression streams, rely on the native
738
+ // response completion state because the wrapper strips `content-length`.
739
+ if (this._responseSize === undefined && nativeResponse.complete) {
740
+ return;
741
+ }
718
742
  this._aborted = true;
719
743
  // Check if there's a content-length mismatch to provide a more specific error
720
744
  if (!this._checkContentLengthMismatch()) {
@@ -907,9 +931,6 @@ export default class Request extends Duplex {
907
931
  this._beforeError(new HTTPError(typedResponse));
908
932
  return;
909
933
  }
910
- // `decompressResponse` wraps the response stream when it decompresses,
911
- // so `response !== nativeResponse` indicates decompression happened.
912
- const wasDecompressed = response !== nativeResponse;
913
934
  // Store the expected content-length from the native response for validation.
914
935
  // This is the content-length before decompression, which is what actually gets transferred.
915
936
  // Skip storing for responses that shouldn't have bodies per RFC 9110.
@@ -1528,11 +1549,6 @@ export default class Request extends Duplex {
1528
1549
  }
1529
1550
  async _makeRequest() {
1530
1551
  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
1552
  const shouldDeleteGeneratedHeader = (currentHeader, generatedHeader) => currentHeader === generatedHeader || is.undefined(currentHeader);
1537
1553
  const syncGeneratedHeader = (name, { currentHeader, explicitHeader, nextHeader, staleGeneratedHeader, }) => {
1538
1554
  if (!is.undefined(nextHeader)) {
@@ -1557,6 +1573,22 @@ export default class Request extends Duplex {
1557
1573
  else if (is.null(currentHeaders[key])) {
1558
1574
  throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
1559
1575
  }
1576
+ else if (Array.isArray(currentHeaders[key]) && key === 'transfer-encoding') {
1577
+ // Node serializes request header arrays as repeated field lines. Keep framing
1578
+ // unambiguous by allowing only one transfer-encoding value here.
1579
+ if (currentHeaders[key].length !== 1) {
1580
+ throw new TypeError(`The \`${key}\` header must be a single value`);
1581
+ }
1582
+ options.setInternalHeader(key, currentHeaders[key][0]);
1583
+ }
1584
+ else if (Array.isArray(currentHeaders[key]) && singleValueRequestHeaders.has(key)) {
1585
+ // Duplicate credential and content-length lines are not allowed on requests.
1586
+ // Normalize a single-element array to match the long-supported string path.
1587
+ if (currentHeaders[key].length !== 1) {
1588
+ throw new TypeError(`The \`${key}\` header must be a single value`);
1589
+ }
1590
+ options.setInternalHeader(key, currentHeaders[key][0]);
1591
+ }
1560
1592
  }
1561
1593
  return currentHeaders;
1562
1594
  };
@@ -1568,6 +1600,12 @@ export default class Request extends Duplex {
1568
1600
  return is.nonEmptyString(cookieString) ? cookieString : undefined;
1569
1601
  };
1570
1602
  const headers = sanitizeHeaders();
1603
+ const initialHeaders = options.getInternalHeaders();
1604
+ const authorizationWasInitiallyExplicit = options.isHeaderExplicitlySet('authorization');
1605
+ const explicitAuthorizationHeader = authorizationWasInitiallyExplicit ? initialHeaders.authorization : undefined;
1606
+ const explicitCookieHeader = options.isHeaderExplicitlySet('cookie') ? initialHeaders.cookie : undefined;
1607
+ const authorizationWasInitiallyOmitted = options.isHeaderExplicitlySet('authorization') && is.undefined(initialHeaders.authorization);
1608
+ const cookieWasInitiallyOmitted = options.isHeaderExplicitlySet('cookie') && is.undefined(initialHeaders.cookie);
1571
1609
  if (options.decompress && is.undefined(headers['accept-encoding'])) {
1572
1610
  const encodings = ['gzip', 'deflate'];
1573
1611
  if (supportsBrotli) {
@@ -1580,7 +1618,11 @@ export default class Request extends Duplex {
1580
1618
  }
1581
1619
  const { username, password } = options;
1582
1620
  const cookieJar = options.cookieJar;
1583
- const generatedAuthorizationHeader = getAuthorizationHeader(username, password, authorizationWasInitiallyOmitted);
1621
+ // Preserve an explicit Authorization header over URL-derived Basic auth. This keeps
1622
+ // normalized single-element arrays aligned with the long-supported string behavior.
1623
+ const generatedAuthorizationHeader = is.undefined(explicitAuthorizationHeader)
1624
+ ? getAuthorizationHeader(username, password, authorizationWasInitiallyOmitted)
1625
+ : undefined;
1584
1626
  let generatedCookieHeader;
1585
1627
  if (!is.undefined(generatedAuthorizationHeader)) {
1586
1628
  options.setInternalHeader('authorization', generatedAuthorizationHeader);
@@ -1610,11 +1652,21 @@ export default class Request extends Duplex {
1610
1652
  // `headers.authorization = undefined` / `headers.cookie = undefined` is an
1611
1653
  // explicit opt-out. Respect that instead of regenerating values from URL
1612
1654
  // 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');
1655
+ const isHeaderExplicitlyOmitted = (header) => options.isHeaderExplicitlySet(header)
1656
+ && Object.hasOwn(currentHeaders, header)
1657
+ && is.undefined(currentHeaders[header]);
1616
1658
  const currentAuthorizationHeader = currentHeaders.authorization;
1617
1659
  const currentCookieHeader = currentHeaders.cookie;
1660
+ // Authorization follows a small contract:
1661
+ // - A concrete Authorization header is sent as-is.
1662
+ // - `authorization = undefined` means omit Authorization entirely, including URL auth.
1663
+ // - Deleting an Authorization header that started explicit also means omit it.
1664
+ // - Otherwise, if the request did not start with explicit Authorization, Got may
1665
+ // generate Basic auth from the current username/password.
1666
+ const authorizationWasExplicitlyOmitted = isHeaderExplicitlyOmitted('authorization')
1667
+ || (authorizationWasInitiallyExplicit && is.undefined(currentAuthorizationHeader));
1668
+ const cookieWasExplicitlyOmitted = is.undefined(currentCookieHeader)
1669
+ && (cookieWasInitiallyOmitted || isHeaderExplicitlyOmitted('cookie'));
1618
1670
  sanitizeHeaders();
1619
1671
  if (!is.undefined(currentHeaders['transfer-encoding']) && !is.undefined(currentHeaders['content-length'])) {
1620
1672
  options.deleteInternalHeader('content-length');
@@ -1626,15 +1678,22 @@ export default class Request extends Duplex {
1626
1678
  delete options.headers.authorization;
1627
1679
  }
1628
1680
  }
1629
- const authorizationHeader = getAuthorizationHeader(options.username, options.password, authorizationWasExplicitlyOmitted);
1681
+ const authorizationHeader = !authorizationWasInitiallyExplicit
1682
+ && !authorizationWasInitiallyOmitted
1683
+ && !authorizationWasExplicitlyOmitted
1684
+ ? getAuthorizationHeader(options.username, options.password, authorizationWasExplicitlyOmitted)
1685
+ : undefined;
1630
1686
  const cookieJar = options.cookieJar;
1631
- if (changedState.has('authorization')) {
1687
+ if (changedState.has('authorization') && !is.undefined(currentAuthorizationHeader)) {
1632
1688
  // A beforeRequest hook intentionally set the outgoing Authorization header.
1633
1689
  }
1634
1690
  else {
1691
+ const restorableAuthorizationHeader = changedState.has('authorization') && is.undefined(currentAuthorizationHeader)
1692
+ ? undefined
1693
+ : explicitAuthorizationHeader;
1635
1694
  syncGeneratedHeader('authorization', {
1636
1695
  currentHeader: currentAuthorizationHeader,
1637
- explicitHeader: explicitAuthorizationHeader,
1696
+ explicitHeader: restorableAuthorizationHeader,
1638
1697
  nextHeader: authorizationHeader,
1639
1698
  staleGeneratedHeader: generatedAuthorizationHeader,
1640
1699
  });
@@ -1649,10 +1708,13 @@ export default class Request extends Duplex {
1649
1708
  // A beforeRequest hook intentionally set the outgoing Cookie header.
1650
1709
  }
1651
1710
  else {
1711
+ const cookieHeader = !cookieWasInitiallyOmitted && !cookieWasExplicitlyOmitted
1712
+ ? await getCookieHeader(cookieJar)
1713
+ : undefined;
1652
1714
  syncGeneratedHeader('cookie', {
1653
1715
  currentHeader: currentCookieHeader,
1654
1716
  explicitHeader: explicitCookieHeader,
1655
- nextHeader: await getCookieHeader(cookieJar),
1717
+ nextHeader: cookieHeader,
1656
1718
  staleGeneratedHeader: generatedCookieHeader,
1657
1719
  });
1658
1720
  }
@@ -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.3",
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",