got 15.0.1 → 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.
@@ -69,12 +69,18 @@ export default function asPromise(firstRequest) {
69
69
  : (Object.hasOwn(updatedOptions, 'body') && updatedOptions.body !== undefined)
70
70
  || (Object.hasOwn(updatedOptions, 'json') && updatedOptions.json !== undefined)
71
71
  || (Object.hasOwn(updatedOptions, 'form') && updatedOptions.form !== undefined);
72
+ const clearsCookieJar = Object.hasOwn(updatedOptions, 'cookieJar') && updatedOptions.cookieJar === undefined;
72
73
  if (hasExplicitBody && !reusesRequestOptions) {
73
74
  options.clearBody();
74
75
  }
76
+ if (!reusesRequestOptions && clearsCookieJar) {
77
+ options.cookieJar = undefined;
78
+ }
75
79
  if (!reusesRequestOptions) {
76
80
  options.merge(updatedOptions);
81
+ options.syncCookieHeaderAfterMerge(previousState, updatedOptions.headers);
77
82
  }
83
+ options.clearUnchangedCookieHeader(previousState, reusesRequestOptions ? changedState : undefined);
78
84
  if (updatedOptions.url) {
79
85
  const nextUrl = reusesRequestOptions
80
86
  ? options.url
@@ -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()) {
@@ -725,6 +749,40 @@ export default class Request extends Duplex {
725
749
  }, this));
726
750
  }
727
751
  });
752
+ let canFinalizeResponse = false;
753
+ const handleResponseEnd = () => {
754
+ if (!canFinalizeResponse
755
+ || !response.readableEnded) {
756
+ return;
757
+ }
758
+ canFinalizeResponse = false;
759
+ if (this._stopReading) {
760
+ return;
761
+ }
762
+ // Validate content-length if it was provided
763
+ // Per RFC 9112: "If the sender closes the connection before the indicated number
764
+ // of octets are received, the recipient MUST consider the message to be incomplete"
765
+ if (this._checkContentLengthMismatch()) {
766
+ return;
767
+ }
768
+ this._responseSize = this._downloadedSize;
769
+ this.emit('downloadProgress', this.downloadProgress);
770
+ // Publish response end event
771
+ publishResponseEnd({
772
+ requestId: this._requestId,
773
+ url: typedResponse.url,
774
+ statusCode,
775
+ bodySize: this._downloadedSize,
776
+ timings: this.timings,
777
+ });
778
+ this.push(null);
779
+ };
780
+ if (!shouldFollowRedirect) {
781
+ // `set-cookie` handling below awaits the cookie jar. A fast response can fully
782
+ // end during that await, so we need to observe `end` early without completing
783
+ // the outward stream until cookie handling has finished.
784
+ response.once('end', handleResponseEnd);
785
+ }
728
786
  const noPipeCookieJarRawBodyPromise = this._noPipe
729
787
  && is.object(options.cookieJar)
730
788
  && !isRedirect
@@ -828,6 +886,7 @@ export default class Request extends Duplex {
828
886
  }
829
887
  return changedState;
830
888
  });
889
+ updatedOptions.clearUnchangedCookieHeader(preHookState, changedState);
831
890
  // If a beforeRedirect hook changed the URL to a different origin,
832
891
  // strip sensitive headers that were preserved for the original origin.
833
892
  // When isDifferentOrigin was already true, headers were already stripped above.
@@ -836,15 +895,7 @@ export default class Request extends Duplex {
836
895
  const hookUrl = updatedOptions.url;
837
896
  if (!isSameOrigin(state.url, hookUrl)) {
838
897
  this._stripUnchangedCrossOriginState(updatedOptions, hookUrl, shouldDropBody, {
839
- headers: state.headers,
840
- username: state.username,
841
- password: state.password,
842
- body: state.body,
843
- json: state.json,
844
- form: state.form,
845
- bodySnapshot: state.bodySnapshot,
846
- jsonSnapshot: state.jsonSnapshot,
847
- formSnapshot: state.formSnapshot,
898
+ ...state,
848
899
  changedState,
849
900
  preserveUsername: hasExplicitCredentialInUrlChange(changedState, hookUrl, 'username')
850
901
  || isCrossOriginCredentialChanged(state.url, hookUrl, 'username'),
@@ -870,6 +921,8 @@ export default class Request extends Duplex {
870
921
  }
871
922
  return;
872
923
  }
924
+ canFinalizeResponse = true;
925
+ handleResponseEnd();
873
926
  // `HTTPError`s always have `error.response.body` defined.
874
927
  // Therefore, we cannot retry if `options.throwHttpErrors` is false.
875
928
  // On the last retry, if `options.throwHttpErrors` is false, we would need to return the body,
@@ -878,9 +931,6 @@ export default class Request extends Duplex {
878
931
  this._beforeError(new HTTPError(typedResponse));
879
932
  return;
880
933
  }
881
- // `decompressResponse` wraps the response stream when it decompresses,
882
- // so `response !== nativeResponse` indicates decompression happened.
883
- const wasDecompressed = response !== nativeResponse;
884
934
  // Store the expected content-length from the native response for validation.
885
935
  // This is the content-length before decompression, which is what actually gets transferred.
886
936
  // Skip storing for responses that shouldn't have bodies per RFC 9110.
@@ -894,32 +944,6 @@ export default class Request extends Duplex {
894
944
  }
895
945
  }
896
946
  }
897
- // Set up end listener AFTER redirect check to avoid emitting progress for redirect responses
898
- let responseEndHandled = false;
899
- const handleResponseEnd = () => {
900
- if (responseEndHandled) {
901
- return;
902
- }
903
- responseEndHandled = true;
904
- // Validate content-length if it was provided
905
- // Per RFC 9112: "If the sender closes the connection before the indicated number
906
- // of octets are received, the recipient MUST consider the message to be incomplete"
907
- if (this._checkContentLengthMismatch()) {
908
- return;
909
- }
910
- this._responseSize = this._downloadedSize;
911
- this.emit('downloadProgress', this.downloadProgress);
912
- // Publish response end event
913
- publishResponseEnd({
914
- requestId: this._requestId,
915
- url: typedResponse.url,
916
- statusCode,
917
- bodySize: this._downloadedSize,
918
- timings: this.timings,
919
- });
920
- this.push(null);
921
- };
922
- response.once('end', handleResponseEnd);
923
947
  this.emit('downloadProgress', this.downloadProgress);
924
948
  response.on('readable', () => {
925
949
  if (this._triggerRead) {
@@ -1525,17 +1549,63 @@ export default class Request extends Duplex {
1525
1549
  }
1526
1550
  async _makeRequest() {
1527
1551
  const { options } = this;
1528
- const headers = options.getInternalHeaders();
1529
- const { username, password } = options;
1530
- const cookieJar = options.cookieJar;
1531
- for (const key in headers) {
1532
- if (is.undefined(headers[key])) {
1533
- options.deleteInternalHeader(key);
1552
+ const shouldDeleteGeneratedHeader = (currentHeader, generatedHeader) => currentHeader === generatedHeader || is.undefined(currentHeader);
1553
+ const syncGeneratedHeader = (name, { currentHeader, explicitHeader, nextHeader, staleGeneratedHeader, }) => {
1554
+ if (!is.undefined(nextHeader)) {
1555
+ options.setInternalHeader(name, nextHeader);
1534
1556
  }
1535
- else if (is.null(headers[key])) {
1536
- throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
1557
+ else if (!is.undefined(explicitHeader) && currentHeader === staleGeneratedHeader) {
1558
+ options.setInternalHeader(name, explicitHeader);
1537
1559
  }
1538
- }
1560
+ else if (shouldDeleteGeneratedHeader(currentHeader, staleGeneratedHeader)) {
1561
+ options.deleteInternalHeader(name);
1562
+ }
1563
+ };
1564
+ const getAuthorizationHeader = (username, password, isExplicitlyOmitted) => !isExplicitlyOmitted && (username || password)
1565
+ ? `Basic ${stringToBase64(`${username}:${password}`)}`
1566
+ : undefined;
1567
+ const sanitizeHeaders = () => {
1568
+ const currentHeaders = options.getInternalHeaders();
1569
+ for (const key in currentHeaders) {
1570
+ if (is.undefined(currentHeaders[key])) {
1571
+ options.deleteInternalHeader(key);
1572
+ }
1573
+ else if (is.null(currentHeaders[key])) {
1574
+ throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
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
+ }
1592
+ }
1593
+ return currentHeaders;
1594
+ };
1595
+ const getCookieHeader = async (cookieJar) => {
1596
+ if (!cookieJar) {
1597
+ return undefined;
1598
+ }
1599
+ const cookieString = await cookieJar.getCookieString(options.url.toString());
1600
+ return is.nonEmptyString(cookieString) ? cookieString : undefined;
1601
+ };
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);
1539
1609
  if (options.decompress && is.undefined(headers['accept-encoding'])) {
1540
1610
  const encodings = ['gzip', 'deflate'];
1541
1611
  if (supportsBrotli) {
@@ -1546,34 +1616,117 @@ export default class Request extends Duplex {
1546
1616
  }
1547
1617
  options.setInternalHeader('accept-encoding', encodings.join(', '));
1548
1618
  }
1549
- if (username || password) {
1550
- const credentials = stringToBase64(`${username}:${password}`);
1551
- options.setInternalHeader('authorization', `Basic ${credentials}`);
1619
+ const { username, password } = options;
1620
+ const cookieJar = options.cookieJar;
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;
1626
+ let generatedCookieHeader;
1627
+ if (!is.undefined(generatedAuthorizationHeader)) {
1628
+ options.setInternalHeader('authorization', generatedAuthorizationHeader);
1552
1629
  }
1553
- // Set cookies
1554
- if (cookieJar) {
1555
- const cookieString = await cookieJar.getCookieString(options.url.toString());
1556
- if (is.nonEmptyString(cookieString)) {
1557
- options.setInternalHeader('cookie', cookieString);
1630
+ if (!cookieWasInitiallyOmitted) {
1631
+ generatedCookieHeader = await getCookieHeader(cookieJar);
1632
+ if (!is.undefined(generatedCookieHeader)) {
1633
+ options.setInternalHeader('cookie', generatedCookieHeader);
1558
1634
  }
1559
1635
  }
1560
1636
  let request;
1561
- for (const hook of options.hooks.beforeRequest) {
1562
- // eslint-disable-next-line no-await-in-loop
1563
- const result = await hook(options, { retryCount: this.retryCount });
1564
- if (!is.undefined(result)) {
1565
- // @ts-expect-error Skip the type mismatch to support abstract responses
1566
- request = () => result;
1567
- break;
1637
+ let shouldOmitRequestUrlCredentials = false;
1638
+ const changedState = await options.trackStateMutations(async (changedState) => {
1639
+ for (const hook of options.hooks.beforeRequest) {
1640
+ // eslint-disable-next-line no-await-in-loop
1641
+ const result = await hook(options, { retryCount: this.retryCount });
1642
+ if (!is.undefined(result)) {
1643
+ // @ts-expect-error Skip the type mismatch to support abstract responses
1644
+ request = () => result;
1645
+ break;
1646
+ }
1647
+ }
1648
+ return changedState;
1649
+ });
1650
+ if (request === undefined) {
1651
+ const currentHeaders = options.getInternalHeaders();
1652
+ // `headers.authorization = undefined` / `headers.cookie = undefined` is an
1653
+ // explicit opt-out. Respect that instead of regenerating values from URL
1654
+ // credentials or the cookie jar later in request setup.
1655
+ const isHeaderExplicitlyOmitted = (header) => options.isHeaderExplicitlySet(header)
1656
+ && Object.hasOwn(currentHeaders, header)
1657
+ && is.undefined(currentHeaders[header]);
1658
+ const currentAuthorizationHeader = currentHeaders.authorization;
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'));
1670
+ sanitizeHeaders();
1671
+ if (!is.undefined(currentHeaders['transfer-encoding']) && !is.undefined(currentHeaders['content-length'])) {
1672
+ options.deleteInternalHeader('content-length');
1673
+ }
1674
+ if (authorizationWasExplicitlyOmitted) {
1675
+ shouldOmitRequestUrlCredentials = true;
1676
+ options.deleteInternalHeader('authorization');
1677
+ if (changedState.has('authorization') && is.undefined(explicitAuthorizationHeader) && !authorizationWasInitiallyOmitted) {
1678
+ delete options.headers.authorization;
1679
+ }
1680
+ }
1681
+ const authorizationHeader = !authorizationWasInitiallyExplicit
1682
+ && !authorizationWasInitiallyOmitted
1683
+ && !authorizationWasExplicitlyOmitted
1684
+ ? getAuthorizationHeader(options.username, options.password, authorizationWasExplicitlyOmitted)
1685
+ : undefined;
1686
+ const cookieJar = options.cookieJar;
1687
+ if (changedState.has('authorization') && !is.undefined(currentAuthorizationHeader)) {
1688
+ // A beforeRequest hook intentionally set the outgoing Authorization header.
1689
+ }
1690
+ else {
1691
+ const restorableAuthorizationHeader = changedState.has('authorization') && is.undefined(currentAuthorizationHeader)
1692
+ ? undefined
1693
+ : explicitAuthorizationHeader;
1694
+ syncGeneratedHeader('authorization', {
1695
+ currentHeader: currentAuthorizationHeader,
1696
+ explicitHeader: restorableAuthorizationHeader,
1697
+ nextHeader: authorizationHeader,
1698
+ staleGeneratedHeader: generatedAuthorizationHeader,
1699
+ });
1700
+ }
1701
+ if (cookieWasExplicitlyOmitted) {
1702
+ options.deleteInternalHeader('cookie');
1703
+ if (changedState.has('cookie') && is.undefined(explicitCookieHeader) && !cookieWasInitiallyOmitted) {
1704
+ delete options.headers.cookie;
1705
+ }
1706
+ }
1707
+ else if (changedState.has('cookie')) {
1708
+ // A beforeRequest hook intentionally set the outgoing Cookie header.
1709
+ }
1710
+ else {
1711
+ const cookieHeader = !cookieWasInitiallyOmitted && !cookieWasExplicitlyOmitted
1712
+ ? await getCookieHeader(cookieJar)
1713
+ : undefined;
1714
+ syncGeneratedHeader('cookie', {
1715
+ currentHeader: currentCookieHeader,
1716
+ explicitHeader: explicitCookieHeader,
1717
+ nextHeader: cookieHeader,
1718
+ staleGeneratedHeader: generatedCookieHeader,
1719
+ });
1568
1720
  }
1569
- }
1570
- if (!is.undefined(headers['transfer-encoding']) && !is.undefined(headers['content-length'])) {
1571
- // TODO: Throw instead of silently dropping `content-length` in the next major version.
1572
- options.deleteInternalHeader('content-length');
1573
1721
  }
1574
1722
  request ??= options.getRequestFunction();
1575
- const url = options.url;
1723
+ const url = shouldOmitRequestUrlCredentials
1724
+ ? new URL(stripUrlAuth(options.url))
1725
+ : options.url;
1576
1726
  this._requestOptions = options.createNativeRequestOptions();
1727
+ if (shouldOmitRequestUrlCredentials) {
1728
+ this._requestOptions.auth = undefined;
1729
+ }
1577
1730
  if (options.cache) {
1578
1731
  this._requestOptions._request = request;
1579
1732
  this._requestOptions.cache = options.cache;
@@ -31,6 +31,8 @@ export type Agents = {
31
31
  export type Headers = Record<string, string | string[] | undefined>;
32
32
  export type CrossOriginState = {
33
33
  headers: Headers;
34
+ hadCookieJar: boolean;
35
+ cookieWasExplicitlySet: boolean;
34
36
  username: string;
35
37
  password: string;
36
38
  body: unknown;
@@ -788,7 +790,7 @@ export declare function applyUrlOverride(options: Options, url: string | URL, {
788
790
  All parsing methods supported by Got.
789
791
  */
790
792
  export type ResponseType = 'json' | 'buffer' | 'text';
791
- type OptionsToSkip = 'searchParameters' | 'followRedirects' | 'auth' | 'toJSON' | 'merge' | 'isHeaderExplicitlySet' | 'shouldCopyPipedHeader' | 'setPipedHeader' | 'getInternalHeaders' | 'setInternalHeader' | 'deleteInternalHeader' | 'trackStateMutations' | 'clearBody' | 'stripUnchangedCrossOriginState' | 'stripSensitiveHeaders' | 'createNativeRequestOptions' | 'getRequestFunction' | 'freeze';
793
+ type OptionsToSkip = 'searchParameters' | 'followRedirects' | 'auth' | 'toJSON' | 'merge' | 'isHeaderExplicitlySet' | 'shouldCopyPipedHeader' | 'setPipedHeader' | 'getInternalHeaders' | 'setInternalHeader' | 'deleteInternalHeader' | 'trackStateMutations' | 'clearBody' | 'clearUnchangedCookieHeader' | 'restoreCookieHeader' | 'syncCookieHeaderAfterMerge' | 'stripUnchangedCrossOriginState' | 'stripSensitiveHeaders' | 'createNativeRequestOptions' | 'getRequestFunction' | 'freeze';
792
794
  export type InternalsType = Except<Options, OptionsToSkip>;
793
795
  export type OptionsError = NodeJS.ErrnoException & {
794
796
  options?: Options;
@@ -1226,6 +1228,9 @@ export default class Options {
1226
1228
  deleteInternalHeader(name: string): void;
1227
1229
  trackStateMutations<Value>(operation: (changedState: Set<string>) => Promisable<Value>): Promise<Value>;
1228
1230
  clearBody(): void;
1231
+ clearUnchangedCookieHeader(previousState: CrossOriginState | undefined, changedState?: Set<string>): void;
1232
+ restoreCookieHeader(previousState: CrossOriginState | undefined, headers?: Headers): void;
1233
+ syncCookieHeaderAfterMerge(previousState: CrossOriginState | undefined, headers?: Headers): void;
1229
1234
  stripUnchangedCrossOriginState(previousState: CrossOriginState, changedState: Set<string>, { clearBody }?: {
1230
1235
  clearBody?: boolean;
1231
1236
  }): void;
@@ -1459,6 +1464,7 @@ export default class Options {
1459
1464
  set strictContentLength(value: boolean);
1460
1465
  toJSON(): {
1461
1466
  timeout: Delays;
1467
+ localAddress: string | undefined;
1462
1468
  headers: Headers;
1463
1469
  request: RequestFunction | undefined;
1464
1470
  json: unknown;
@@ -1491,7 +1497,6 @@ export default class Options {
1491
1497
  dnsLookupIpVersion: DnsLookupIpVersion;
1492
1498
  parseJson: ParseJsonFunction;
1493
1499
  stringifyJson: StringifyJsonFunction;
1494
- localAddress: string | undefined;
1495
1500
  method: Method;
1496
1501
  createConnection: CreateConnectionFunction | undefined;
1497
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 {
@@ -1406,6 +1407,35 @@ export default class Options {
1406
1407
  this.deleteInternalHeader(header);
1407
1408
  }
1408
1409
  }
1410
+ clearUnchangedCookieHeader(previousState, changedState) {
1411
+ if (previousState?.hadCookieJar
1412
+ && this.cookieJar === undefined
1413
+ && !this.isHeaderExplicitlySet('cookie')
1414
+ && !changedState?.has('cookie')
1415
+ && this.headers.cookie === previousState.headers.cookie) {
1416
+ this.deleteInternalHeader('cookie');
1417
+ }
1418
+ }
1419
+ restoreCookieHeader(previousState, headers) {
1420
+ if (!previousState) {
1421
+ return;
1422
+ }
1423
+ if (Object.hasOwn(headers ?? {}, 'cookie')) {
1424
+ return;
1425
+ }
1426
+ if (previousState.cookieWasExplicitlySet) {
1427
+ this.headers.cookie = previousState.headers.cookie;
1428
+ return;
1429
+ }
1430
+ delete this.headers.cookie;
1431
+ if (previousState.headers.cookie !== undefined) {
1432
+ this.setInternalHeader('cookie', previousState.headers.cookie);
1433
+ }
1434
+ }
1435
+ syncCookieHeaderAfterMerge(previousState, headers) {
1436
+ this.restoreCookieHeader(previousState, headers);
1437
+ this.clearUnchangedCookieHeader(previousState);
1438
+ }
1409
1439
  stripUnchangedCrossOriginState(previousState, changedState, { clearBody = true } = {}) {
1410
1440
  const headers = this.getInternalHeaders();
1411
1441
  const url = this.#internals.url;
@@ -2111,6 +2141,8 @@ export default class Options {
2111
2141
  }
2112
2142
  export const snapshotCrossOriginState = (options) => ({
2113
2143
  headers: { ...options.getInternalHeaders() },
2144
+ hadCookieJar: options.cookieJar !== undefined,
2145
+ cookieWasExplicitlySet: options.isHeaderExplicitlySet('cookie'),
2114
2146
  username: options.username,
2115
2147
  password: options.password,
2116
2148
  body: options.body,
@@ -1,9 +1,56 @@
1
+ const splitHeaderValue = (value, separator) => {
2
+ const values = [];
3
+ let current = '';
4
+ let inQuotes = false;
5
+ let inReference = false;
6
+ let isEscaped = false;
7
+ for (const character of value) {
8
+ if (inQuotes && isEscaped) {
9
+ current += character;
10
+ isEscaped = false;
11
+ continue;
12
+ }
13
+ if (inQuotes && character === '\\') {
14
+ current += character;
15
+ isEscaped = true;
16
+ continue;
17
+ }
18
+ if (character === '"') {
19
+ inQuotes = !inQuotes;
20
+ current += character;
21
+ continue;
22
+ }
23
+ if (!inQuotes && character === '<') {
24
+ inReference = true;
25
+ current += character;
26
+ continue;
27
+ }
28
+ if (!inQuotes && character === '>') {
29
+ inReference = false;
30
+ current += character;
31
+ continue;
32
+ }
33
+ // Link headers use both quoted strings and <URI-reference> values, so raw
34
+ // splitting on `,` / `;` would break valid values containing those characters.
35
+ if (!inQuotes && !inReference && character === separator) {
36
+ values.push(current);
37
+ current = '';
38
+ continue;
39
+ }
40
+ current += character;
41
+ }
42
+ if (inQuotes || isEscaped) {
43
+ throw new Error(`Failed to parse Link header: ${value}`);
44
+ }
45
+ values.push(current);
46
+ return values;
47
+ };
1
48
  export default function parseLinkHeader(link) {
2
49
  const parsed = [];
3
- const items = link.split(',');
50
+ const items = splitHeaderValue(link, ',');
4
51
  for (const item of items) {
5
52
  // https://tools.ietf.org/html/rfc5988#section-5
6
- const [rawUriReference, ...rawLinkParameters] = item.split(';');
53
+ const [rawUriReference, ...rawLinkParameters] = splitHeaderValue(item, ';');
7
54
  const trimmedUriReference = rawUriReference.trim();
8
55
  // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
9
56
  if (trimmedUriReference[0] !== '<' || trimmedUriReference.at(-1) !== '>') {
@@ -11,6 +58,9 @@ export default function parseLinkHeader(link) {
11
58
  }
12
59
  const reference = trimmedUriReference.slice(1, -1);
13
60
  const parameters = {};
61
+ if (reference.includes('<') || reference.includes('>')) {
62
+ throw new Error(`Invalid format of the Link header reference: ${trimmedUriReference}`);
63
+ }
14
64
  if (rawLinkParameters.length === 0) {
15
65
  throw new Error(`Unexpected end of Link header parameters: ${rawLinkParameters.join(';')}`);
16
66
  }
@@ -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
  });
@@ -180,6 +180,7 @@ const create = (defaults) => {
180
180
  }
181
181
  if (optionsToMerge === response.request.options) {
182
182
  normalizedOptions = response.request.options;
183
+ normalizedOptions.clearUnchangedCookieHeader(previousState, changedState);
183
184
  if (previousUrl) {
184
185
  const nextUrl = normalizedOptions.url;
185
186
  if (nextUrl && !isSameOrigin(previousUrl, nextUrl)) {
@@ -192,10 +193,15 @@ const create = (defaults) => {
192
193
  const hasExplicitBody = (Object.hasOwn(optionsToMerge, 'body') && optionsToMerge.body !== undefined)
193
194
  || (Object.hasOwn(optionsToMerge, 'json') && optionsToMerge.json !== undefined)
194
195
  || (Object.hasOwn(optionsToMerge, 'form') && optionsToMerge.form !== undefined);
196
+ const clearsCookieJar = Object.hasOwn(optionsToMerge, 'cookieJar') && optionsToMerge.cookieJar === undefined;
195
197
  if (hasExplicitBody) {
196
198
  normalizedOptions.clearBody();
197
199
  }
200
+ if (clearsCookieJar) {
201
+ normalizedOptions.cookieJar = undefined;
202
+ }
198
203
  normalizedOptions.merge(optionsToMerge);
204
+ normalizedOptions.syncCookieHeaderAfterMerge(previousState, optionsToMerge.headers);
199
205
  try {
200
206
  assert.any([is.string, is.urlInstance, is.undefined], optionsToMerge.url);
201
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "got",
3
- "version": "15.0.1",
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",