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
|
-
|
|
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)
|
|
1614
|
-
|
|
1615
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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.
|
|
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 {
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "got",
|
|
3
|
-
"version": "15.0.
|
|
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": "^
|
|
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",
|