got 15.0.6 → 15.0.7
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/core/index.d.ts +8 -0
- package/dist/source/core/index.js +125 -42
- package/package.json +1 -1
|
@@ -99,6 +99,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
|
|
|
99
99
|
private _request?;
|
|
100
100
|
private _responseSize?;
|
|
101
101
|
private _bodySize?;
|
|
102
|
+
private _nativeFormDataBody?;
|
|
102
103
|
private _unproxyEvents?;
|
|
103
104
|
private _triggerRead;
|
|
104
105
|
private readonly _jobs;
|
|
@@ -109,6 +110,8 @@ export default class Request extends Duplex implements RequestEvents<Request> {
|
|
|
109
110
|
private _expectedContentLength?;
|
|
110
111
|
private _compressedBytesCount?;
|
|
111
112
|
private _skipRequestEndInFinal;
|
|
113
|
+
private _hasWrittenBody;
|
|
114
|
+
private _hasWritableBody;
|
|
112
115
|
private _incrementalDecode?;
|
|
113
116
|
private readonly _requestId;
|
|
114
117
|
private _requestInitialized;
|
|
@@ -140,7 +143,12 @@ export default class Request extends Duplex implements RequestEvents<Request> {
|
|
|
140
143
|
private _endWritableRequest;
|
|
141
144
|
private _stripCrossOriginState;
|
|
142
145
|
private _stripUnchangedCrossOriginState;
|
|
146
|
+
private get _methodCanHaveBody();
|
|
147
|
+
private _canWriteBody;
|
|
148
|
+
private _hasBodyForRedirect;
|
|
149
|
+
private _hasUnchangedBodyForRedirect;
|
|
143
150
|
private _dropBody;
|
|
151
|
+
private _destroyBody;
|
|
144
152
|
private readonly _onBodyError;
|
|
145
153
|
private _writeChunksToRequest;
|
|
146
154
|
private _prepareCache;
|
|
@@ -16,7 +16,7 @@ import timedOut, { TimeoutError as TimedOutTimeoutError } from './timed-out.js';
|
|
|
16
16
|
import stripUrlAuth from './utils/strip-url-auth.js';
|
|
17
17
|
import WeakableMap from './utils/weakable-map.js';
|
|
18
18
|
import calculateRetryDelay from './calculate-retry-delay.js';
|
|
19
|
-
import Options, { crossOriginStripHeaders, hasExplicitCredentialInUrlChange,
|
|
19
|
+
import Options, { crossOriginStripHeaders, hasExplicitCredentialInUrlChange, isBodyUnchanged, isCrossOriginCredentialChanged, isSameOrigin, snapshotCrossOriginState, } from './options.js';
|
|
20
20
|
import { cacheDecodedBody, decodeUint8Array, isResponseOk, isUtf8Encoding, } from './response.js';
|
|
21
21
|
import isClientRequest from './utils/is-client-request.js';
|
|
22
22
|
import { getUnixSocketPath } from './utils/is-unix-socket-url.js';
|
|
@@ -56,6 +56,20 @@ const proxiedRequestEvents = [
|
|
|
56
56
|
'upgrade',
|
|
57
57
|
];
|
|
58
58
|
const noop = () => { };
|
|
59
|
+
const serializeNativeFormDataBody = (form) => {
|
|
60
|
+
const response = new globalThis.Response(form);
|
|
61
|
+
return {
|
|
62
|
+
body: response.body,
|
|
63
|
+
contentType: response.headers.get('content-type') ?? 'multipart/form-data',
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
// A body is replayable only if iterating it again restarts from the beginning.
|
|
67
|
+
// Node streams, Web `ReadableStream`s, generators, and self-iterating (one-shot) iterators all yield their data only once, so they cannot be replayed on a redirect.
|
|
68
|
+
const isNonReplayableBody = (body) => is.nodeStream(body)
|
|
69
|
+
|| (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream)
|
|
70
|
+
|| is.generator(body)
|
|
71
|
+
|| (is.asyncIterable(body) && body[Symbol.asyncIterator]() === body)
|
|
72
|
+
|| (is.iterable(body) && body[Symbol.iterator]() === body);
|
|
59
73
|
const isTransientWriteError = (error) => {
|
|
60
74
|
const { code } = error;
|
|
61
75
|
return typeof code === 'string' && transientWriteErrorCodes.has(code);
|
|
@@ -132,6 +146,7 @@ export default class Request extends Duplex {
|
|
|
132
146
|
_request;
|
|
133
147
|
_responseSize;
|
|
134
148
|
_bodySize;
|
|
149
|
+
_nativeFormDataBody;
|
|
135
150
|
_unproxyEvents;
|
|
136
151
|
_triggerRead = false;
|
|
137
152
|
_jobs = [];
|
|
@@ -142,6 +157,8 @@ export default class Request extends Duplex {
|
|
|
142
157
|
_expectedContentLength;
|
|
143
158
|
_compressedBytesCount;
|
|
144
159
|
_skipRequestEndInFinal = false;
|
|
160
|
+
_hasWrittenBody = false;
|
|
161
|
+
_hasWritableBody = false;
|
|
145
162
|
_incrementalDecode;
|
|
146
163
|
_requestId = generateRequestId();
|
|
147
164
|
// We need this because `this._request` if `undefined` when using cache
|
|
@@ -233,6 +250,7 @@ export default class Request extends Duplex {
|
|
|
233
250
|
if (this.destroyed) {
|
|
234
251
|
return;
|
|
235
252
|
}
|
|
253
|
+
this._hasWritableBody = this._canWriteBody();
|
|
236
254
|
await this._makeRequest();
|
|
237
255
|
if (this.destroyed) {
|
|
238
256
|
this._request?.destroy();
|
|
@@ -453,6 +471,7 @@ export default class Request extends Duplex {
|
|
|
453
471
|
}
|
|
454
472
|
}
|
|
455
473
|
_write(chunk, encoding, callback) {
|
|
474
|
+
this._hasWrittenBody = true;
|
|
456
475
|
const write = () => {
|
|
457
476
|
this._writeRequest(chunk, encoding, callback);
|
|
458
477
|
};
|
|
@@ -474,6 +493,7 @@ export default class Request extends Duplex {
|
|
|
474
493
|
// We need to check if `this._request` is present,
|
|
475
494
|
// because it isn't when we use cache.
|
|
476
495
|
if (!request || request.destroyed) {
|
|
496
|
+
this._hasWritableBody = false;
|
|
477
497
|
callback();
|
|
478
498
|
return;
|
|
479
499
|
}
|
|
@@ -486,6 +506,7 @@ export default class Request extends Duplex {
|
|
|
486
506
|
if (!error) {
|
|
487
507
|
this._emitUploadComplete(request);
|
|
488
508
|
}
|
|
509
|
+
this._hasWritableBody = false;
|
|
489
510
|
callback(error);
|
|
490
511
|
});
|
|
491
512
|
};
|
|
@@ -604,7 +625,7 @@ export default class Request extends Duplex {
|
|
|
604
625
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
605
626
|
const isJSON = !is.undefined(options.json);
|
|
606
627
|
const isBody = !is.undefined(options.body);
|
|
607
|
-
const cannotHaveBody =
|
|
628
|
+
const cannotHaveBody = !this._methodCanHaveBody;
|
|
608
629
|
if (isForm || isJSON || isBody) {
|
|
609
630
|
if (cannotHaveBody) {
|
|
610
631
|
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
|
|
@@ -614,11 +635,16 @@ export default class Request extends Duplex {
|
|
|
614
635
|
if (isBody) {
|
|
615
636
|
// Native FormData
|
|
616
637
|
if (options.body instanceof FormData) {
|
|
617
|
-
const
|
|
638
|
+
const { body, contentType } = serializeNativeFormDataBody(options.body);
|
|
639
|
+
this._nativeFormDataBody = {
|
|
640
|
+
form: options.body,
|
|
641
|
+
body,
|
|
642
|
+
contentTypeWasGenerated: noContentType,
|
|
643
|
+
};
|
|
618
644
|
if (noContentType) {
|
|
619
|
-
headers['content-type'] =
|
|
645
|
+
headers['content-type'] = contentType;
|
|
620
646
|
}
|
|
621
|
-
options.body =
|
|
647
|
+
options.body = body;
|
|
622
648
|
}
|
|
623
649
|
else if (Object.prototype.toString.call(options.body) === '[object FormData]') {
|
|
624
650
|
throw new TypeError('Non-native FormData is not supported. Use globalThis.FormData instead.');
|
|
@@ -874,14 +900,17 @@ export default class Request extends Duplex {
|
|
|
874
900
|
updatedOptions.method = 'GET';
|
|
875
901
|
this._dropBody(updatedOptions);
|
|
876
902
|
}
|
|
903
|
+
else if (isDifferentOrigin
|
|
904
|
+
&& canRewrite
|
|
905
|
+
&& this._hasBodyForRedirect(updatedOptions)) {
|
|
906
|
+
this._dropBody(updatedOptions);
|
|
907
|
+
}
|
|
877
908
|
if (isDifferentOrigin) {
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
// to prevent a malicious server from injecting auth to third parties.
|
|
884
|
-
this._stripCrossOriginState(updatedOptions, redirectUrl, shouldDropBody);
|
|
909
|
+
// On cross-origin redirects, strip sensitive headers and any credentials
|
|
910
|
+
// embedded in the redirect URL itself to prevent a malicious server from
|
|
911
|
+
// leaking them to a third party. The request body is preserved per RFC:
|
|
912
|
+
// 307/308 keep the method and replayable bodies, even cross-origin.
|
|
913
|
+
this._stripCrossOriginState(updatedOptions, redirectUrl);
|
|
885
914
|
}
|
|
886
915
|
else {
|
|
887
916
|
redirectUrl.username = updatedOptions.username;
|
|
@@ -889,6 +918,7 @@ export default class Request extends Duplex {
|
|
|
889
918
|
}
|
|
890
919
|
updatedOptions.url = redirectUrl;
|
|
891
920
|
this.redirectUrls.push(redirectUrl);
|
|
921
|
+
const bodyBeforeRedirectHooks = updatedOptions.body;
|
|
892
922
|
const preHookState = isDifferentOrigin
|
|
893
923
|
? undefined
|
|
894
924
|
: {
|
|
@@ -903,14 +933,55 @@ export default class Request extends Duplex {
|
|
|
903
933
|
return changedState;
|
|
904
934
|
});
|
|
905
935
|
updatedOptions.clearUnchangedCookieHeader(preHookState, changedState);
|
|
936
|
+
const nativeFormDataBody = this._nativeFormDataBody;
|
|
937
|
+
if (statusCode === 307 || statusCode === 308) {
|
|
938
|
+
const bodyUnchangedByHooks = updatedOptions.body === bodyBeforeRedirectHooks;
|
|
939
|
+
const wasNonReplayable = isNonReplayableBody(bodyBeforeRedirectHooks);
|
|
940
|
+
if (!bodyUnchangedByHooks && wasNonReplayable) {
|
|
941
|
+
// A hook supplied a fresh body, so dispose of the original non-replayable one.
|
|
942
|
+
this._destroyBody(bodyBeforeRedirectHooks);
|
|
943
|
+
}
|
|
944
|
+
else if (bodyUnchangedByHooks
|
|
945
|
+
&& nativeFormDataBody !== undefined
|
|
946
|
+
&& updatedOptions.body === nativeFormDataBody.body) {
|
|
947
|
+
// Native FormData generates a fresh stream and boundary, so re-serialize it to replay the upload.
|
|
948
|
+
const { body, contentType } = serializeNativeFormDataBody(nativeFormDataBody.form);
|
|
949
|
+
nativeFormDataBody.body = body;
|
|
950
|
+
updatedOptions.body = body;
|
|
951
|
+
if (changedState.has('content-type')) {
|
|
952
|
+
nativeFormDataBody.contentTypeWasGenerated = false;
|
|
953
|
+
}
|
|
954
|
+
else if (nativeFormDataBody.contentTypeWasGenerated) {
|
|
955
|
+
updatedOptions.setInternalHeader('content-type', contentType);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
else if (bodyUnchangedByHooks
|
|
959
|
+
&& (wasNonReplayable || (is.undefined(updatedOptions.body) && (this._hasWrittenBody || this._hasWritableBody)))) {
|
|
960
|
+
// 307/308 redirects must replay the body, so follow the HTTP spec and other clients by failing for unchanged non-replayable bodies. Hooks may supply a fresh body.
|
|
961
|
+
this._dropBody(updatedOptions);
|
|
962
|
+
this._beforeError(new RequestError('Cannot follow redirect with a non-replayable body', {}, this));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
906
966
|
// If a beforeRedirect hook changed the URL to a different origin,
|
|
907
967
|
// strip sensitive headers that were preserved for the original origin.
|
|
908
968
|
// When isDifferentOrigin was already true, headers were already stripped above.
|
|
909
969
|
if (!isDifferentOrigin) {
|
|
910
970
|
const state = preHookState;
|
|
911
971
|
const hookUrl = updatedOptions.url;
|
|
912
|
-
|
|
913
|
-
|
|
972
|
+
const hookChangedOrigin = !isSameOrigin(state.url, hookUrl);
|
|
973
|
+
if (hookChangedOrigin
|
|
974
|
+
&& (statusCode === 301 || statusCode === 302)
|
|
975
|
+
&& updatedOptions.method === 'POST') {
|
|
976
|
+
updatedOptions.method = 'GET';
|
|
977
|
+
this._dropBody(updatedOptions);
|
|
978
|
+
}
|
|
979
|
+
if (hookChangedOrigin) {
|
|
980
|
+
if (canRewrite
|
|
981
|
+
&& this._hasUnchangedBodyForRedirect(updatedOptions, state, changedState)) {
|
|
982
|
+
this._dropBody(updatedOptions);
|
|
983
|
+
}
|
|
984
|
+
this._stripUnchangedCrossOriginState(updatedOptions, hookUrl, {
|
|
914
985
|
...state,
|
|
915
986
|
changedState,
|
|
916
987
|
preserveUsername: hasExplicitCredentialInUrlChange(changedState, hookUrl, 'username')
|
|
@@ -1173,17 +1244,18 @@ export default class Request extends Duplex {
|
|
|
1173
1244
|
else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
|
|
1174
1245
|
(async () => {
|
|
1175
1246
|
const isInitialRequest = currentRequest === this;
|
|
1247
|
+
const bodyOptions = this.options;
|
|
1176
1248
|
try {
|
|
1177
1249
|
for await (const chunk of body) {
|
|
1178
|
-
if (this.options.body !== body) {
|
|
1250
|
+
if (this.options !== bodyOptions || this.options.body !== body) {
|
|
1179
1251
|
return;
|
|
1180
1252
|
}
|
|
1181
1253
|
await this._asyncWrite(chunk, currentRequest);
|
|
1182
|
-
if (this.options.body !== body) {
|
|
1254
|
+
if (this.options !== bodyOptions || this.options.body !== body) {
|
|
1183
1255
|
return;
|
|
1184
1256
|
}
|
|
1185
1257
|
}
|
|
1186
|
-
if (this.options.body === body) {
|
|
1258
|
+
if (this.options === bodyOptions && this.options.body === body) {
|
|
1187
1259
|
if (isInitialRequest) {
|
|
1188
1260
|
super.end();
|
|
1189
1261
|
return;
|
|
@@ -1192,7 +1264,7 @@ export default class Request extends Duplex {
|
|
|
1192
1264
|
}
|
|
1193
1265
|
}
|
|
1194
1266
|
catch (error) {
|
|
1195
|
-
if (this.options.body !== body) {
|
|
1267
|
+
if (this.options !== bodyOptions || this.options.body !== body) {
|
|
1196
1268
|
return;
|
|
1197
1269
|
}
|
|
1198
1270
|
this._beforeError(normalizeError(error));
|
|
@@ -1201,8 +1273,7 @@ export default class Request extends Duplex {
|
|
|
1201
1273
|
}
|
|
1202
1274
|
else if (is.undefined(body)) {
|
|
1203
1275
|
// No body to send, end the request
|
|
1204
|
-
|
|
1205
|
-
if ((this._noPipe ?? false) || cannotHaveBody || currentRequest !== this) {
|
|
1276
|
+
if ((this._noPipe ?? false) || !this._methodCanHaveBody || currentRequest !== this) {
|
|
1206
1277
|
currentRequest.end();
|
|
1207
1278
|
}
|
|
1208
1279
|
}
|
|
@@ -1305,7 +1376,7 @@ export default class Request extends Duplex {
|
|
|
1305
1376
|
});
|
|
1306
1377
|
});
|
|
1307
1378
|
}
|
|
1308
|
-
_stripCrossOriginState(options, urlToClear
|
|
1379
|
+
_stripCrossOriginState(options, urlToClear) {
|
|
1309
1380
|
for (const header of crossOriginStripHeaders) {
|
|
1310
1381
|
options.deleteInternalHeader(header);
|
|
1311
1382
|
}
|
|
@@ -1313,11 +1384,8 @@ export default class Request extends Duplex {
|
|
|
1313
1384
|
options.password = '';
|
|
1314
1385
|
urlToClear.username = '';
|
|
1315
1386
|
urlToClear.password = '';
|
|
1316
|
-
if (!bodyAlreadyDropped) {
|
|
1317
|
-
this._dropBody(options);
|
|
1318
|
-
}
|
|
1319
1387
|
}
|
|
1320
|
-
_stripUnchangedCrossOriginState(options, urlToClear,
|
|
1388
|
+
_stripUnchangedCrossOriginState(options, urlToClear, state) {
|
|
1321
1389
|
const headers = options.getInternalHeaders();
|
|
1322
1390
|
for (const header of crossOriginStripHeaders) {
|
|
1323
1391
|
if (!state.changedState.has(header) && headers[header] === state.headers[header]) {
|
|
@@ -1332,23 +1400,44 @@ export default class Request extends Duplex {
|
|
|
1332
1400
|
options.password = '';
|
|
1333
1401
|
urlToClear.password = '';
|
|
1334
1402
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1403
|
+
}
|
|
1404
|
+
get _methodCanHaveBody() {
|
|
1405
|
+
return !methodsWithoutBody.has(this.options.method) || (this.options.method === 'GET' && this.options.allowGetBody);
|
|
1406
|
+
}
|
|
1407
|
+
_canWriteBody() {
|
|
1408
|
+
return !this._noPipe && !this.isReadonly && this._methodCanHaveBody;
|
|
1409
|
+
}
|
|
1410
|
+
_hasBodyForRedirect(options) {
|
|
1411
|
+
return !is.undefined(options.body) || !is.undefined(options.json) || !is.undefined(options.form) || this._hasWrittenBody || this._hasWritableBody;
|
|
1412
|
+
}
|
|
1413
|
+
_hasUnchangedBodyForRedirect(options, state, changedState) {
|
|
1414
|
+
return !changedState.has('body')
|
|
1415
|
+
&& !changedState.has('json')
|
|
1416
|
+
&& !changedState.has('form')
|
|
1417
|
+
&& this._hasBodyForRedirect(options)
|
|
1418
|
+
&& isBodyUnchanged(options, state);
|
|
1342
1419
|
}
|
|
1343
1420
|
_dropBody(updatedOptions) {
|
|
1344
1421
|
const { body } = this.options;
|
|
1345
1422
|
const hadOptionBody = !is.undefined(body) || !is.undefined(this.options.json) || !is.undefined(this.options.form);
|
|
1346
1423
|
this.options.clearBody();
|
|
1424
|
+
this._destroyBody(body);
|
|
1425
|
+
if (!hadOptionBody && !this.writableEnded) {
|
|
1426
|
+
this._skipRequestEndInFinal = true;
|
|
1427
|
+
super.end();
|
|
1428
|
+
}
|
|
1429
|
+
updatedOptions.clearBody();
|
|
1430
|
+
this._bodySize = undefined;
|
|
1431
|
+
this._hasWrittenBody = false;
|
|
1432
|
+
this._hasWritableBody = false;
|
|
1433
|
+
}
|
|
1434
|
+
_destroyBody(body) {
|
|
1347
1435
|
if (is.nodeStream(body)) {
|
|
1348
|
-
body
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1436
|
+
const bodyStream = body;
|
|
1437
|
+
bodyStream.off('error', this._onBodyError);
|
|
1438
|
+
bodyStream.unpipe();
|
|
1439
|
+
bodyStream.on('error', noop);
|
|
1440
|
+
bodyStream.destroy();
|
|
1352
1441
|
}
|
|
1353
1442
|
else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
|
|
1354
1443
|
const iterableBody = body;
|
|
@@ -1366,12 +1455,6 @@ export default class Request extends Duplex {
|
|
|
1366
1455
|
catch { }
|
|
1367
1456
|
}
|
|
1368
1457
|
}
|
|
1369
|
-
else if (!hadOptionBody && !this.writableEnded) {
|
|
1370
|
-
this._skipRequestEndInFinal = true;
|
|
1371
|
-
super.end();
|
|
1372
|
-
}
|
|
1373
|
-
updatedOptions.clearBody();
|
|
1374
|
-
this._bodySize = undefined;
|
|
1375
1458
|
}
|
|
1376
1459
|
_onBodyError = (error) => {
|
|
1377
1460
|
if (this._flushed) {
|