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.
@@ -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, isCrossOriginCredentialChanged, isBodyUnchanged, isSameOrigin, snapshotCrossOriginState, } from './options.js';
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 = methodsWithoutBody.has(options.method) && !(options.method === 'GET' && options.allowGetBody);
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 response = new Response(options.body);
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'] = response.headers.get('content-type') ?? 'multipart/form-data';
645
+ headers['content-type'] = contentType;
620
646
  }
621
- options.body = response.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
- // Also strip body on cross-origin redirects to prevent data leakage.
879
- // 301/302 POST already drops the body (converted to GET above).
880
- // 307/308 preserve the method per RFC, but the body must not be
881
- // forwarded to a different origin.
882
- // Strip credentials embedded in the redirect URL itself
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
- if (!isSameOrigin(state.url, hookUrl)) {
913
- this._stripUnchangedCrossOriginState(updatedOptions, hookUrl, shouldDropBody, {
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
- const cannotHaveBody = methodsWithoutBody.has(this.options.method) && !(this.options.method === 'GET' && this.options.allowGetBody);
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, bodyAlreadyDropped) {
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, bodyAlreadyDropped, state) {
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
- if (!bodyAlreadyDropped
1336
- && !state.changedState.has('body')
1337
- && !state.changedState.has('json')
1338
- && !state.changedState.has('form')
1339
- && isBodyUnchanged(options, state)) {
1340
- this._dropBody(options);
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.off('error', this._onBodyError);
1349
- body.unpipe();
1350
- body.on('error', noop);
1351
- body.destroy();
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "got",
3
- "version": "15.0.6",
3
+ "version": "15.0.7",
4
4
  "description": "Human-friendly and powerful HTTP request library for Node.js",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/got",