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.
- package/dist/source/as-promise/index.js +6 -0
- package/dist/source/core/index.js +220 -67
- package/dist/source/core/options.d.ts +7 -2
- package/dist/source/core/options.js +37 -5
- package/dist/source/core/parse-link-header.js +52 -2
- package/dist/source/core/utils/timer.js +19 -11
- package/dist/source/create.js +6 -0
- package/package.json +9 -9
|
@@ -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
|
-
|
|
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
|
|
1529
|
-
const {
|
|
1530
|
-
|
|
1531
|
-
|
|
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.
|
|
1536
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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 =
|
|
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.
|
|
96
|
+
for (const [key, value] of Object.entries(source)) {
|
|
97
97
|
if (key === '__proto__') {
|
|
98
98
|
continue;
|
|
99
99
|
}
|
|
100
|
-
target
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
65
|
+
const initialConnectionTimings = getInitialConnectionTimings(socket);
|
|
66
|
+
if (initialConnectionTimings) {
|
|
62
67
|
// Restore the phase timings from the initial connection
|
|
63
|
-
timings.phases.dns =
|
|
64
|
-
timings.phases.tcp =
|
|
65
|
-
timings.phases.tls =
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
114
|
-
|
|
120
|
+
const initialConnectionTimings = getInitialConnectionTimings(socket);
|
|
121
|
+
if (initialConnectionTimings) {
|
|
122
|
+
initialConnectionTimings.tlsPhase = timings.phases.tls;
|
|
115
123
|
}
|
|
116
124
|
},
|
|
117
125
|
});
|
package/dist/source/create.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
|
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.
|
|
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",
|