got 14.5.0 → 14.6.0

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.
@@ -18,12 +18,14 @@ export default function asPromise(firstRequest) {
18
18
  let globalResponse;
19
19
  let normalizedOptions;
20
20
  const emitter = new EventEmitter();
21
+ let promiseSettled = false;
21
22
  const promise = new PCancelable((resolve, reject, onCancel) => {
22
23
  onCancel(() => {
23
24
  globalRequest.destroy();
24
25
  });
25
26
  onCancel.shouldReject = false;
26
27
  onCancel(() => {
28
+ promiseSettled = true;
27
29
  reject(new CancelError(globalRequest));
28
30
  });
29
31
  const makeRequest = (retryCount) => {
@@ -82,7 +84,7 @@ export default function asPromise(firstRequest) {
82
84
  }
83
85
  throw new RetryError(request);
84
86
  });
85
- if (!(is.object(response) && is.number(response.statusCode) && !is.nullOrUndefined(response.body))) {
87
+ if (!(is.object(response) && is.number(response.statusCode) && 'body' in response)) {
86
88
  throw new TypeError('The `afterResponse` hook returned an invalid value');
87
89
  }
88
90
  }
@@ -97,12 +99,27 @@ export default function asPromise(firstRequest) {
97
99
  return;
98
100
  }
99
101
  request.destroy();
102
+ promiseSettled = true;
100
103
  resolve(request.options.resolveBodyOnly ? response.body : response);
101
104
  });
105
+ let handledFinalError = false;
102
106
  const onError = (error) => {
103
107
  if (promise.isCanceled) {
104
108
  return;
105
109
  }
110
+ // Route errors emitted directly on the stream (e.g., EPIPE from Node.js)
111
+ // through retry logic first, then handle them here after retries are exhausted.
112
+ // See https://github.com/sindresorhus/got/issues/1995
113
+ if (!request._stopReading) {
114
+ request._beforeError(error);
115
+ return;
116
+ }
117
+ // Allow the manual re-emission from Request to land only once.
118
+ if (handledFinalError) {
119
+ return;
120
+ }
121
+ handledFinalError = true;
122
+ promiseSettled = true;
106
123
  const { options } = request;
107
124
  if (error instanceof HTTPError && !options.throwHttpErrors) {
108
125
  const { response } = error;
@@ -112,10 +129,21 @@ export default function asPromise(firstRequest) {
112
129
  }
113
130
  reject(error);
114
131
  };
115
- request.once('error', onError);
132
+ // Use .on() instead of .once() to keep the listener active across retries.
133
+ // When _stopReading is false, we return early and the error gets re-emitted
134
+ // after retry logic completes, so we need this listener to remain active.
135
+ // See https://github.com/sindresorhus/got/issues/1995
136
+ request.on('error', onError);
116
137
  const previousBody = request.options?.body;
117
138
  request.once('retry', (newRetryCount, error) => {
118
139
  firstRequest = undefined;
140
+ // If promise already settled, don't retry
141
+ // This prevents the race condition in #1489 where a late error
142
+ // (e.g., ECONNRESET after successful response) triggers retry
143
+ // after the promise has already resolved/rejected
144
+ if (promiseSettled) {
145
+ return;
146
+ }
119
147
  const newBody = request.options.body;
120
148
  if (previousBody === newBody && is.nodeStream(newBody)) {
121
149
  error.message = 'Cannot retry with consumed body stream';
@@ -88,11 +88,11 @@ export default class Request extends Duplex implements RequestEvents<Request> {
88
88
  requestUrl?: URL;
89
89
  redirectUrls: URL[];
90
90
  retryCount: number;
91
+ _stopReading: boolean;
91
92
  private _requestOptions;
92
93
  private _stopRetry;
93
94
  private _downloadedSize;
94
95
  private _uploadedSize;
95
- private _stopReading;
96
96
  private readonly _pipedServerResponses;
97
97
  private _request?;
98
98
  private _responseSize?;
@@ -106,6 +106,8 @@ export default class Request extends Duplex implements RequestEvents<Request> {
106
106
  private _nativeResponse?;
107
107
  private _flushed;
108
108
  private _aborted;
109
+ private _expectedContentLength?;
110
+ private _byteCounter?;
109
111
  private _requestInitialized;
110
112
  constructor(url: UrlType, options?: OptionsType, defaults?: DefaultsType);
111
113
  flush(): Promise<void>;
@@ -118,6 +120,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
118
120
  end?: boolean;
119
121
  }): T;
120
122
  unpipe<T extends NodeJS.WritableStream>(destination: T): this;
123
+ private _checkContentLengthMismatch;
121
124
  private _finalizeBody;
122
125
  private _onResponseBase;
123
126
  private _setRawBody;
@@ -1,8 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { Buffer } from 'node:buffer';
3
- import { Duplex } from 'node:stream';
4
- import { gunzip, inflate, brotliDecompress } from 'node:zlib';
5
- import { promisify } from 'node:util';
3
+ import { Duplex, Transform } from 'node:stream';
6
4
  import http, { ServerResponse } from 'node:http';
7
5
  import timer from '@szmarczak/http-timer';
8
6
  import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
@@ -19,7 +17,7 @@ import calculateRetryDelay from './calculate-retry-delay.js';
19
17
  import Options from './options.js';
20
18
  import { isResponseOk } from './response.js';
21
19
  import isClientRequest from './utils/is-client-request.js';
22
- import isUnixSocketURL from './utils/is-unix-socket-url.js';
20
+ import isUnixSocketURL, { getUnixSocketPath } from './utils/is-unix-socket-url.js';
23
21
  import { RequestError, ReadError, MaxRedirectsError, HTTPError, TimeoutError, UploadError, CacheError, AbortError, } from './errors.js';
24
22
  const supportsBrotli = is.string(process.versions.brotli);
25
23
  const methodsWithoutBody = new Set(['GET', 'HEAD']);
@@ -35,6 +33,17 @@ const proxiedRequestEvents = [
35
33
  'upgrade',
36
34
  ];
37
35
  const noop = () => { };
36
+ /**
37
+ Stream transform that counts bytes passing through.
38
+ Used to track compressed bytes before decompression for content-length validation.
39
+ */
40
+ class ByteCounter extends Transform {
41
+ count = 0;
42
+ _transform(chunk, _encoding, callback) {
43
+ this.count += chunk.length;
44
+ callback(null, chunk);
45
+ }
46
+ }
38
47
  export default class Request extends Duplex {
39
48
  // @ts-expect-error - Ignoring for now.
40
49
  ['constructor'];
@@ -45,10 +54,10 @@ export default class Request extends Duplex {
45
54
  requestUrl;
46
55
  redirectUrls;
47
56
  retryCount;
57
+ _stopReading;
48
58
  _stopRetry;
49
59
  _downloadedSize;
50
60
  _uploadedSize;
51
- _stopReading;
52
61
  _pipedServerResponses;
53
62
  _request;
54
63
  _responseSize;
@@ -61,6 +70,8 @@ export default class Request extends Duplex {
61
70
  _nativeResponse;
62
71
  _flushed;
63
72
  _aborted;
73
+ _expectedContentLength;
74
+ _byteCounter;
64
75
  // We need this because `this._request` if `undefined` when using cache
65
76
  _requestInitialized;
66
77
  constructor(url, options, defaults) {
@@ -112,7 +123,18 @@ export default class Request extends Duplex {
112
123
  }
113
124
  this.flush = async () => {
114
125
  this.flush = async () => { };
115
- this.destroy(error);
126
+ // Defer error emission to next tick to allow user to attach error handlers
127
+ process.nextTick(() => {
128
+ // _beforeError requires options to access retry logic and hooks
129
+ if (this.options) {
130
+ this._beforeError(error);
131
+ }
132
+ else {
133
+ // Options is undefined, skip _beforeError and destroy directly
134
+ const requestError = error instanceof RequestError ? error : new RequestError(error.message, error, this);
135
+ this.destroy(requestError);
136
+ }
137
+ });
116
138
  };
117
139
  return;
118
140
  }
@@ -368,6 +390,19 @@ export default class Request extends Duplex {
368
390
  if (this._request) {
369
391
  this._request.destroy();
370
392
  }
393
+ // Workaround: http-timer only sets timings.end when the response emits 'end'.
394
+ // When a stream is destroyed before completion, the 'end' event may not fire,
395
+ // leaving timings.end undefined. This should ideally be fixed in http-timer
396
+ // by listening to the 'close' event, but we handle it here for now.
397
+ // Only set timings.end if there was no error or abort (to maintain semantic correctness).
398
+ const timings = this._request?.timings;
399
+ if (timings && is.undefined(timings.end) && !is.undefined(timings.response) && is.undefined(timings.error) && is.undefined(timings.abort)) {
400
+ timings.end = Date.now();
401
+ if (is.undefined(timings.phases.total)) {
402
+ timings.phases.download = timings.end - timings.response;
403
+ timings.phases.total = timings.end - timings.start;
404
+ }
405
+ }
371
406
  if (error !== null && !is.undefined(error) && !(error instanceof RequestError)) {
372
407
  error = new RequestError(error.message, error, this);
373
408
  }
@@ -386,6 +421,22 @@ export default class Request extends Duplex {
386
421
  super.unpipe(destination);
387
422
  return this;
388
423
  }
424
+ _checkContentLengthMismatch() {
425
+ if (this.options.strictContentLength && this._expectedContentLength !== undefined) {
426
+ // Use ByteCounter's count when available (for compressed responses),
427
+ // otherwise use _downloadedSize (for uncompressed responses)
428
+ const actualSize = this._byteCounter?.count ?? this._downloadedSize;
429
+ if (actualSize !== this._expectedContentLength) {
430
+ this._beforeError(new ReadError({
431
+ message: `Content-Length mismatch: expected ${this._expectedContentLength} bytes, received ${actualSize} bytes`,
432
+ name: 'Error',
433
+ code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH',
434
+ }, this));
435
+ return true;
436
+ }
437
+ }
438
+ return false;
439
+ }
389
440
  async _finalizeBody() {
390
441
  const { options } = this;
391
442
  const { headers } = options;
@@ -473,86 +524,19 @@ export default class Request extends Duplex {
473
524
  || statusCode === 204
474
525
  || statusCode === 205
475
526
  || statusCode === 304;
476
- const nativeResponse = response;
477
527
  if (options.decompress && !hasNoBody) {
528
+ // When strictContentLength is enabled, track compressed bytes by listening to
529
+ // the native response's data events before decompression
530
+ if (options.strictContentLength) {
531
+ this._byteCounter = new ByteCounter();
532
+ this._nativeResponse.on('data', (chunk) => {
533
+ this._byteCounter.count += chunk.length;
534
+ });
535
+ }
478
536
  response = decompressResponse(response);
479
537
  }
480
- // For revalidated cached responses (304 Not Modified), cacheable-request may return
481
- // a ResponseLike object with the body stored in a .body property but not pushed into
482
- // the stream. The stream is created and may be ended without data, and the 'end' event is
483
- // never emitted, causing Got to hang waiting for it. Detect and fix this.
484
- const nativeAsAny = nativeResponse;
485
- if (nativeAsAny.body) {
486
- // Mark the response as complete immediately (ResponseLike doesn't have req.res.complete)
487
- nativeResponse.complete = true;
488
- response.complete = true;
489
- // Use setImmediate to check if this is a stuck revalidated response
490
- setImmediate(() => {
491
- // Check if the stream ended with no data (revalidated response)
492
- if (nativeResponse.readableEnded && !nativeResponse.readableLength) {
493
- // The body is in nativeAsAny.body but was never pushed to the stream.
494
- // We need to push it to the native response so it flows through decompression if needed.
495
- try {
496
- // Push the body to the native stream
497
- nativeResponse.push(nativeAsAny.body);
498
- // eslint-disable-next-line unicorn/no-array-push-push
499
- nativeResponse.push(null);
500
- // Update download size with the cached body length
501
- let bodyLength = 0;
502
- if (Buffer.isBuffer(nativeAsAny.body)) {
503
- bodyLength = nativeAsAny.body.length;
504
- }
505
- else if (typeof nativeAsAny.body === 'string') {
506
- bodyLength = Buffer.byteLength(nativeAsAny.body);
507
- }
508
- this._downloadedSize += bodyLength;
509
- }
510
- catch {
511
- // If push fails (stream already ended), we need to decompress manually for compressed responses
512
- const encoding = nativeResponse.headers['content-encoding'];
513
- if (encoding && Buffer.isBuffer(nativeAsAny.body)) {
514
- // Decompress the body based on the encoding
515
- let decompressAsync;
516
- switch (encoding) {
517
- case 'gzip': {
518
- decompressAsync = promisify(gunzip);
519
- break;
520
- }
521
- case 'deflate': {
522
- decompressAsync = promisify(inflate);
523
- break;
524
- }
525
- case 'br': {
526
- decompressAsync = promisify(brotliDecompress);
527
- break;
528
- }
529
- default: {
530
- break;
531
- }
532
- }
533
- if (decompressAsync) {
534
- // Decompress asynchronously and set rawBody
535
- void decompressAsync(nativeAsAny.body).then((decompressed) => {
536
- response.rawBody = decompressed;
537
- this._downloadedSize += nativeAsAny.body.length;
538
- response.emit('end');
539
- }).catch(() => {
540
- // Decompression failed, use compressed body as-is
541
- response.rawBody = nativeAsAny.body;
542
- response.emit('end');
543
- });
544
- return;
545
- }
546
- }
547
- // Not compressed or decompression not needed, set rawBody directly
548
- response.rawBody = nativeAsAny.body;
549
- response.emit('end');
550
- }
551
- }
552
- });
553
- }
554
538
  const typedResponse = response;
555
- typedResponse.statusMessage = typedResponse.statusMessage ?? http.STATUS_CODES[statusCode];
539
+ typedResponse.statusMessage = typedResponse.statusMessage || http.STATUS_CODES[statusCode]; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- The status message can be empty.
556
540
  typedResponse.url = options.url.toString();
557
541
  typedResponse.requestUrl = this.requestUrl;
558
542
  typedResponse.redirectUrls = this.redirectUrls;
@@ -564,6 +548,18 @@ export default class Request extends Duplex {
564
548
  this._isFromCache = typedResponse.isFromCache;
565
549
  this._responseSize = Number(response.headers['content-length']) || undefined;
566
550
  this.response = typedResponse;
551
+ // Workaround for http-timer bug: when connecting to an IP address (no DNS lookup),
552
+ // http-timer sets lookup = connect instead of lookup = socket, resulting in
553
+ // dns = lookup - socket being a small positive number instead of 0.
554
+ // See https://github.com/sindresorhus/got/issues/2279
555
+ const { timings } = response;
556
+ if (timings?.lookup !== undefined && timings.socket !== undefined && timings.connect !== undefined && timings.lookup === timings.connect && timings.phases.dns !== 0) {
557
+ // Fix the DNS phase to be 0 and set lookup to socket time
558
+ timings.phases.dns = 0;
559
+ timings.lookup = timings.socket;
560
+ // Recalculate TCP time to be the full time from socket to connect
561
+ timings.phases.tcp = timings.connect - timings.socket;
562
+ }
567
563
  response.once('error', (error) => {
568
564
  this._aborted = true;
569
565
  // Force clean-up, because some packages don't do this.
@@ -573,11 +569,14 @@ export default class Request extends Duplex {
573
569
  });
574
570
  response.once('aborted', () => {
575
571
  this._aborted = true;
576
- this._beforeError(new ReadError({
577
- name: 'Error',
578
- message: 'The server aborted pending request',
579
- code: 'ECONNRESET',
580
- }, this));
572
+ // Check if there's a content-length mismatch to provide a more specific error
573
+ if (!this._checkContentLengthMismatch()) {
574
+ this._beforeError(new ReadError({
575
+ name: 'Error',
576
+ message: 'The server aborted pending request',
577
+ code: 'ECONNRESET',
578
+ }, this));
579
+ }
581
580
  });
582
581
  const rawCookies = response.headers['set-cookie'];
583
582
  if (is.object(options.cookieJar) && rawCookies) {
@@ -639,7 +638,11 @@ export default class Request extends Duplex {
639
638
  return;
640
639
  }
641
640
  // Redirecting to a different site, clear sensitive data.
642
- if (redirectUrl.hostname !== url.hostname || redirectUrl.port !== url.port) {
641
+ // For UNIX sockets, different socket paths are also different origins.
642
+ const isDifferentOrigin = redirectUrl.hostname !== url.hostname
643
+ || redirectUrl.port !== url.port
644
+ || getUnixSocketPath(url) !== getUnixSocketPath(redirectUrl);
645
+ if (isDifferentOrigin) {
643
646
  if ('host' in updatedOptions.headers) {
644
647
  delete updatedOptions.headers.host;
645
648
  }
@@ -684,13 +687,32 @@ export default class Request extends Duplex {
684
687
  this._beforeError(new HTTPError(typedResponse));
685
688
  return;
686
689
  }
690
+ // Store the expected content-length from the native response for validation.
691
+ // This is the content-length before decompression, which is what actually gets transferred.
692
+ // Skip storing for responses that shouldn't have bodies per RFC 9110.
693
+ // When decompression occurs, only store if strictContentLength is enabled.
694
+ const wasDecompressed = response !== this._nativeResponse;
695
+ if (!hasNoBody && (!wasDecompressed || options.strictContentLength)) {
696
+ const contentLengthHeader = this._nativeResponse.headers['content-length'];
697
+ if (contentLengthHeader !== undefined) {
698
+ const expectedLength = Number(contentLengthHeader);
699
+ if (!Number.isNaN(expectedLength) && expectedLength >= 0) {
700
+ this._expectedContentLength = expectedLength;
701
+ }
702
+ }
703
+ }
687
704
  // Set up end listener AFTER redirect check to avoid emitting progress for redirect responses
688
- const endStream = () => {
705
+ response.once('end', () => {
706
+ // Validate content-length if it was provided
707
+ // Per RFC 9112: "If the sender closes the connection before the indicated number
708
+ // of octets are received, the recipient MUST consider the message to be incomplete"
709
+ if (this._checkContentLengthMismatch()) {
710
+ return;
711
+ }
689
712
  this._responseSize = this._downloadedSize;
690
713
  this.emit('downloadProgress', this.downloadProgress);
691
714
  this.push(null);
692
- };
693
- response.once('end', endStream);
715
+ });
694
716
  this.emit('downloadProgress', this.downloadProgress);
695
717
  response.on('readable', () => {
696
718
  if (this._triggerRead) {
@@ -715,12 +737,22 @@ export default class Request extends Duplex {
715
737
  if (destination.headersSent) {
716
738
  continue;
717
739
  }
718
- // eslint-disable-next-line guard-for-in
740
+ // Check if decompression actually occurred by comparing stream objects.
741
+ // decompressResponse wraps the response stream when it decompresses,
742
+ // so response !== this._nativeResponse indicates decompression happened.
743
+ const wasDecompressed = response !== this._nativeResponse;
719
744
  for (const key in response.headers) {
720
- const isAllowed = options.decompress ? key !== 'content-encoding' : true;
721
- const value = response.headers[key];
722
- if (isAllowed) {
723
- destination.setHeader(key, value);
745
+ if (Object.hasOwn(response.headers, key)) {
746
+ const value = response.headers[key];
747
+ // When decompression occurred, skip content-encoding and content-length
748
+ // as they refer to the compressed data, not the decompressed stream.
749
+ if (wasDecompressed && (key === 'content-encoding' || key === 'content-length')) {
750
+ continue;
751
+ }
752
+ // Skip if value is undefined
753
+ if (value !== undefined) {
754
+ destination.setHeader(key, value);
755
+ }
724
756
  }
725
757
  }
726
758
  destination.statusCode = statusCode;
@@ -805,6 +837,19 @@ export default class Request extends Duplex {
805
837
  if (is.nodeStream(body)) {
806
838
  body.pipe(currentRequest);
807
839
  }
840
+ else if (is.buffer(body)) {
841
+ // Buffer should be sent directly without conversion
842
+ this._writeRequest(body, undefined, () => { });
843
+ currentRequest.end();
844
+ }
845
+ else if (is.typedArray(body)) {
846
+ // Typed arrays should be treated like buffers, not iterated over
847
+ // Create a Uint8Array view over the data (Node.js streams accept Uint8Array)
848
+ const typedArray = body;
849
+ const uint8View = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
850
+ this._writeRequest(uint8View, undefined, () => { });
851
+ currentRequest.end();
852
+ }
808
853
  else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
809
854
  (async () => {
810
855
  try {
@@ -884,9 +929,15 @@ export default class Request extends Duplex {
884
929
  response._readableState.autoDestroy = false;
885
930
  if (request) {
886
931
  const fix = () => {
932
+ // For ResponseLike objects from cache, set complete to true if not already set.
933
+ // For real HTTP responses, copy from the underlying response.
887
934
  if (response.req) {
888
935
  response.complete = response.req.res.complete;
889
936
  }
937
+ else if (response.complete === undefined) {
938
+ // ResponseLike from cache should have complete = true
939
+ response.complete = true;
940
+ }
890
941
  };
891
942
  response.prependOnceListener('end', fix);
892
943
  fix();
@@ -933,7 +984,7 @@ export default class Request extends Duplex {
933
984
  let request;
934
985
  for (const hook of options.hooks.beforeRequest) {
935
986
  // eslint-disable-next-line no-await-in-loop
936
- const result = await hook(options);
987
+ const result = await hook(options, { retryCount: this.retryCount });
937
988
  if (!is.undefined(result)) {
938
989
  // @ts-expect-error Skip the type mismatch to support abstract responses
939
990
  request = () => result;
@@ -992,12 +1043,12 @@ export default class Request extends Duplex {
992
1043
  }
993
1044
  async _error(error) {
994
1045
  try {
995
- if (error instanceof HTTPError && !this.options.throwHttpErrors) {
1046
+ if (this.options && error instanceof HTTPError && !this.options.throwHttpErrors) {
996
1047
  // This branch can be reached only when using the Promise API
997
1048
  // Skip calling the hooks on purpose.
998
1049
  // See https://github.com/sindresorhus/got/issues/2103
999
1050
  }
1000
- else {
1051
+ else if (this.options) {
1001
1052
  for (const hook of this.options.hooks.beforeError) {
1002
1053
  // eslint-disable-next-line no-await-in-loop
1003
1054
  error = await hook(error);
@@ -1008,10 +1059,22 @@ export default class Request extends Duplex {
1008
1059
  error = new RequestError(error_.message, error_, this);
1009
1060
  }
1010
1061
  this.destroy(error);
1062
+ // Manually emit error for Promise API to ensure it receives it.
1063
+ // Node.js streams may not re-emit if an error was already emitted during retry attempts.
1064
+ // Only emit for Promise API (_noPipe = true) to avoid double emissions in stream mode.
1065
+ // Use process.nextTick to defer emission and allow destroy() to complete first.
1066
+ // See https://github.com/sindresorhus/got/issues/1995
1067
+ if (this._noPipe) {
1068
+ process.nextTick(() => {
1069
+ this.emit('error', error);
1070
+ });
1071
+ }
1011
1072
  }
1012
1073
  _writeRequest(chunk, encoding, callback) {
1013
1074
  if (!this._request || this._request.destroyed) {
1014
- // Probably the `ClientRequest` instance will throw
1075
+ // When there's no request (e.g., using cached response from beforeRequest hook),
1076
+ // we still need to call the callback to allow the stream to finish properly.
1077
+ callback();
1015
1078
  return;
1016
1079
  }
1017
1080
  this._request.write(chunk, encoding, (error) => {
@@ -39,9 +39,34 @@ export type PromiseCookieJar = {
39
39
  getCookieString: (url: string) => Promise<string>;
40
40
  setCookie: (rawCookie: string, url: string) => Promise<unknown>;
41
41
  };
42
+ /**
43
+ Utility type to override specific properties in a type.
44
+
45
+ Uses `Omit` to remove properties before adding them back to ensure proper type replacement rather than intersection, which handles edge cases with optional/required properties correctly.
46
+ */
47
+ type OverrideProperties<T, U> = Omit<T, keyof U> & U;
48
+ /**
49
+ Represents the runtime state of Options as seen by hooks after normalization.
50
+
51
+ Some Options properties accept multiple input types but are normalized to a single type internally by the Options class setters. This type reflects the actual runtime types that hooks receive, ensuring type safety when accessing options within hook functions.
52
+ */
53
+ export type NormalizedOptions = OverrideProperties<Options, {
54
+ url: URL | undefined;
55
+ dnsCache: CacheableLookup | undefined;
56
+ cache: StorageAdapter | undefined;
57
+ prefixUrl: string;
58
+ }>;
42
59
  export type InitHook = (init: OptionsInit, self: Options) => void;
43
- export type BeforeRequestHook = (options: Options) => Promisable<void | Response | ResponseLike>;
44
- export type BeforeRedirectHook = (updatedOptions: Options, plainResponse: PlainResponse) => Promisable<void>;
60
+ export type BeforeRequestHookContext = {
61
+ /**
62
+ The current retry count.
63
+
64
+ It will be `0` for the initial request and increment for each retry.
65
+ */
66
+ retryCount: number;
67
+ };
68
+ export type BeforeRequestHook = (options: NormalizedOptions, context: BeforeRequestHookContext) => Promisable<void | Response | ResponseLike>;
69
+ export type BeforeRedirectHook = (updatedOptions: NormalizedOptions, plainResponse: PlainResponse) => Promisable<void>;
45
70
  export type BeforeErrorHook = (error: RequestError) => Promisable<RequestError>;
46
71
  export type BeforeRetryHook = (error: RequestError, retryCount: number) => Promisable<void>;
47
72
  export type AfterResponseHook<ResponseType = unknown> = (response: Response<ResponseType>, retryWithMergedOptions: (options: OptionsInit) => never) => Promisable<Response | CancelableRequest<Response>>;
@@ -142,6 +167,8 @@ export type Hooks = {
142
167
  /**
143
168
  Called right before making the request with `options.createNativeRequestOptions()`.
144
169
 
170
+ The second argument is a context object containing request state information.
171
+
145
172
  This hook is especially useful in conjunction with `got.extend()` when you want to sign your request.
146
173
 
147
174
  @default []
@@ -162,7 +189,7 @@ export type Hooks = {
162
189
  json: {payload: 'old'},
163
190
  hooks: {
164
191
  beforeRequest: [
165
- options => {
192
+ (options, context) => {
166
193
  options.body = JSON.stringify({payload: 'new'});
167
194
  options.headers['content-length'] = options.body.length.toString();
168
195
  }
@@ -172,6 +199,28 @@ export type Hooks = {
172
199
  );
173
200
  ```
174
201
 
202
+ **Example using `context.retryCount`:**
203
+
204
+ ```
205
+ import got from 'got';
206
+
207
+ await got('https://httpbin.org/status/500', {
208
+ retry: {
209
+ limit: 2
210
+ },
211
+ hooks: {
212
+ beforeRequest: [
213
+ (options, context) => {
214
+ // Only log on the initial request, not on retries
215
+ if (context.retryCount === 0) {
216
+ console.log('Making initial request');
217
+ }
218
+ }
219
+ ]
220
+ }
221
+ });
222
+ ```
223
+
175
224
  **Tip:**
176
225
  > - You can indirectly override the `request` function by early returning a [`ClientRequest`-like](https://nodejs.org/api/http.html#http_class_http_clientrequest) instance or a [`IncomingMessage`-like](https://nodejs.org/api/http.html#http_class_http_incomingmessage) instance. This is very useful when creating a custom cache mechanism.
177
226
  > - [Read more about this tip](https://github.com/sindresorhus/got/blob/main/documentation/cache.md#advanced-caching-mechanisms).
@@ -487,6 +536,27 @@ export type HttpsOptions = {
487
536
  dhparam?: SecureContextOptions['dhparam'];
488
537
  ecdhCurve?: SecureContextOptions['ecdhCurve'];
489
538
  certificateRevocationLists?: SecureContextOptions['crl'];
539
+ /**
540
+ Optionally affect the OpenSSL protocol behavior, which is not usually necessary. This should be used carefully if at all!
541
+
542
+ The value is a numeric bitmask of the `SSL_OP_*` options from OpenSSL.
543
+
544
+ For example, to allow connections to legacy servers that do not support secure renegotiation, you can use `crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT`.
545
+
546
+ @example
547
+ ```
548
+ import crypto from 'node:crypto';
549
+ import got from 'got';
550
+
551
+ // Allow connections to servers with legacy renegotiation
552
+ await got('https://legacy-server.com', {
553
+ https: {
554
+ secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT
555
+ }
556
+ });
557
+ ```
558
+ */
559
+ secureOptions?: number;
490
560
  };
491
561
  export type PaginateData<BodyType, ElementType> = {
492
562
  response: Response<BodyType>;
@@ -735,7 +805,7 @@ export default class Options {
735
805
 
736
806
  __Note #4__: This option is not enumerable and will not be merged with the instance defaults.
737
807
 
738
- The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
808
+ The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / typed array ([`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), etc.) / [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
739
809
 
740
810
  Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
741
811
 
@@ -756,8 +826,8 @@ export default class Options {
756
826
  });
757
827
  ```
758
828
  */
759
- get body(): string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined;
760
- set body(value: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined);
829
+ get body(): string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | ArrayBufferView | undefined;
830
+ set body(value: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | ArrayBufferView | undefined);
761
831
  /**
762
832
  The form body is converted to a query string using [`(new URLSearchParams(object)).toString()`](https://nodejs.org/api/url.html#url_constructor_new_urlsearchparams_obj).
763
833
 
@@ -770,7 +840,9 @@ export default class Options {
770
840
  get form(): Record<string, any> | undefined;
771
841
  set form(value: Record<string, any> | undefined);
772
842
  /**
773
- JSON body. If the `Content-Type` header is not set, it will be set to `application/json`.
843
+ JSON request body. If the `content-type` header is not set, it will be set to `application/json`.
844
+
845
+ __Important__: This option only affects the request body you send to the server. To parse the response as JSON, you must either call `.json()` on the promise or set `responseType: 'json'` in the options.
774
846
 
775
847
  __Note #1__: If you provide this option, `got.stream()` will be read-only.
776
848
 
@@ -1202,6 +1274,18 @@ export default class Options {
1202
1274
  set maxHeaderSize(value: number | undefined);
1203
1275
  get enableUnixSockets(): boolean;
1204
1276
  set enableUnixSockets(value: boolean);
1277
+ /**
1278
+ Throw an error if the server response's `content-length` header value doesn't match the number of bytes received.
1279
+
1280
+ This is useful for detecting truncated responses and follows RFC 9112 requirements for message completeness.
1281
+
1282
+ __Note__: Responses without a `content-length` header are not validated.
1283
+ __Note__: When enabled and validation fails, a `ReadError` with code `ERR_HTTP_CONTENT_LENGTH_MISMATCH` will be thrown.
1284
+
1285
+ @default false
1286
+ */
1287
+ get strictContentLength(): boolean;
1288
+ set strictContentLength(value: boolean);
1205
1289
  toJSON(): {
1206
1290
  headers: Headers;
1207
1291
  timeout: Delays;
@@ -1215,7 +1299,7 @@ export default class Options {
1215
1299
  h2session: ClientHttp2Session | undefined;
1216
1300
  decompress: boolean;
1217
1301
  prefixUrl: string | URL;
1218
- body: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined;
1302
+ body: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | ArrayBufferView | undefined;
1219
1303
  form: Record<string, any> | undefined;
1220
1304
  url: string | URL | undefined;
1221
1305
  cookieJar: PromiseCookieJar | ToughCookieJar | undefined;
@@ -1248,6 +1332,7 @@ export default class Options {
1248
1332
  setHost: boolean;
1249
1333
  maxHeaderSize: number | undefined;
1250
1334
  enableUnixSockets: boolean;
1335
+ strictContentLength: boolean;
1251
1336
  };
1252
1337
  createNativeRequestOptions(): {
1253
1338
  ALPNProtocols: string[] | undefined;
@@ -1268,6 +1353,7 @@ export default class Options {
1268
1353
  dhparam: string | Buffer<ArrayBufferLike> | undefined;
1269
1354
  ecdhCurve: string | undefined;
1270
1355
  crl: string | Buffer<ArrayBufferLike> | (string | Buffer<ArrayBufferLike>)[] | undefined;
1356
+ secureOptions: number | undefined;
1271
1357
  lookup: {
1272
1358
  (hostname: string, family: import("cacheable-lookup").IPFamily, callback: (error: NodeJS.ErrnoException | null, address: string, family: import("cacheable-lookup").IPFamily) => void): void;
1273
1359
  (hostname: string, callback: (error: NodeJS.ErrnoException | null, address: string, family: import("cacheable-lookup").IPFamily) => void): void;
@@ -1314,7 +1400,6 @@ export default class Options {
1314
1400
  clientCertEngine?: string | undefined;
1315
1401
  privateKeyEngine?: string | undefined;
1316
1402
  privateKeyIdentifier?: string | undefined;
1317
- secureOptions?: number | undefined;
1318
1403
  secureProtocol?: string | undefined;
1319
1404
  sessionIdContext?: string | undefined;
1320
1405
  ticketKeys?: Buffer | undefined;
@@ -11,11 +11,43 @@ import http2wrapper from 'http2-wrapper';
11
11
  import { isFormData } from 'form-data-encoder';
12
12
  import parseLinkHeader from './parse-link-header.js';
13
13
  const [major, minor] = process.versions.node.split('.').map(Number);
14
+ /**
15
+ Generic helper that wraps any assertion function to add context to error messages.
16
+ */
17
+ function wrapAssertionWithContext(optionName, assertionFn) {
18
+ try {
19
+ assertionFn();
20
+ }
21
+ catch (error) {
22
+ if (error instanceof Error) {
23
+ error.message = `Option '${optionName}': ${error.message}`;
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ /**
29
+ Helper function that wraps assert.any() to provide better error messages.
30
+ When assertion fails, it includes the option name in the error message.
31
+ */
32
+ function assertAny(optionName, validators, value) {
33
+ wrapAssertionWithContext(optionName, () => {
34
+ assert.any(validators, value);
35
+ });
36
+ }
37
+ /**
38
+ Helper function that wraps assert.plainObject() to provide better error messages.
39
+ When assertion fails, it includes the option name in the error message.
40
+ */
41
+ function assertPlainObject(optionName, value) {
42
+ wrapAssertionWithContext(optionName, () => {
43
+ assert.plainObject(value);
44
+ });
45
+ }
14
46
  function validateSearchParameters(searchParameters) {
15
47
  // eslint-disable-next-line guard-for-in
16
48
  for (const key in searchParameters) {
17
49
  const value = searchParameters[key];
18
- assert.any([is.string, is.number, is.boolean, is.null, is.undefined], value);
50
+ assertAny(`searchParams.${key}`, [is.string, is.number, is.boolean, is.null, is.undefined], value);
19
51
  }
20
52
  }
21
53
  const globalCache = new Map();
@@ -179,6 +211,7 @@ const defaultInternals = {
179
211
  dhparam: undefined,
180
212
  ecdhCurve: undefined,
181
213
  certificateRevocationLists: undefined,
214
+ secureOptions: undefined,
182
215
  },
183
216
  encoding: undefined,
184
217
  resolveBodyOnly: false,
@@ -217,6 +250,7 @@ const defaultInternals = {
217
250
  maxHeaderSize: undefined,
218
251
  signal: undefined,
219
252
  enableUnixSockets: false,
253
+ strictContentLength: false,
220
254
  };
221
255
  const cloneInternals = (internals) => {
222
256
  const { hooks, retry } = internals;
@@ -336,9 +370,9 @@ export default class Options {
336
370
  _merging;
337
371
  _init;
338
372
  constructor(input, options, defaults) {
339
- assert.any([is.string, is.urlInstance, is.object, is.undefined], input);
340
- assert.any([is.object, is.undefined], options);
341
- assert.any([is.object, is.undefined], defaults);
373
+ assertAny('input', [is.string, is.urlInstance, is.object, is.undefined], input);
374
+ assertAny('options', [is.object, is.undefined], options);
375
+ assertAny('defaults', [is.object, is.undefined], defaults);
342
376
  if (input instanceof Options || options instanceof Options) {
343
377
  throw new TypeError('The defaults must be passed as the third argument');
344
378
  }
@@ -456,7 +490,7 @@ export default class Options {
456
490
  return this._internals.request;
457
491
  }
458
492
  set request(value) {
459
- assert.any([is.function, is.undefined], value);
493
+ assertAny('request', [is.function, is.undefined], value);
460
494
  this._internals.request = value;
461
495
  }
462
496
  /**
@@ -485,14 +519,14 @@ export default class Options {
485
519
  return this._internals.agent;
486
520
  }
487
521
  set agent(value) {
488
- assert.plainObject(value);
522
+ assertPlainObject('agent', value);
489
523
  // eslint-disable-next-line guard-for-in
490
524
  for (const key in value) {
491
525
  if (!(key in this._internals.agent)) {
492
526
  throw new TypeError(`Unexpected agent option: ${key}`);
493
527
  }
494
528
  // @ts-expect-error - No idea why `value[key]` doesn't work here.
495
- assert.any([is.object, is.undefined, (v) => v === false], value[key]);
529
+ assertAny(`agent.${key}`, [is.object, is.undefined, (v) => v === false], value[key]);
496
530
  }
497
531
  if (this._merging) {
498
532
  Object.assign(this._internals.agent, value);
@@ -545,14 +579,14 @@ export default class Options {
545
579
  return this._internals.timeout;
546
580
  }
547
581
  set timeout(value) {
548
- assert.plainObject(value);
582
+ assertPlainObject('timeout', value);
549
583
  // eslint-disable-next-line guard-for-in
550
584
  for (const key in value) {
551
585
  if (!(key in this._internals.timeout)) {
552
586
  throw new Error(`Unexpected timeout option: ${key}`);
553
587
  }
554
588
  // @ts-expect-error - No idea why `value[key]` doesn't work here.
555
- assert.any([is.number, is.undefined], value[key]);
589
+ assertAny(`timeout.${key}`, [is.number, is.undefined], value[key]);
556
590
  }
557
591
  if (this._merging) {
558
592
  Object.assign(this._internals.timeout, value);
@@ -606,7 +640,7 @@ export default class Options {
606
640
  return this._internals.prefixUrl;
607
641
  }
608
642
  set prefixUrl(value) {
609
- assert.any([is.string, is.urlInstance], value);
643
+ assertAny('prefixUrl', [is.string, is.urlInstance], value);
610
644
  if (value === '') {
611
645
  this._internals.prefixUrl = '';
612
646
  return;
@@ -630,7 +664,7 @@ export default class Options {
630
664
 
631
665
  __Note #4__: This option is not enumerable and will not be merged with the instance defaults.
632
666
 
633
- The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
667
+ The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / typed array ([`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), etc.) / [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
634
668
 
635
669
  Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
636
670
 
@@ -655,7 +689,7 @@ export default class Options {
655
689
  return this._internals.body;
656
690
  }
657
691
  set body(value) {
658
- assert.any([is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.undefined], value);
692
+ assertAny('body', [is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.typedArray, is.undefined], value);
659
693
  if (is.nodeStream(value)) {
660
694
  assert.truthy(value.readable);
661
695
  }
@@ -678,7 +712,7 @@ export default class Options {
678
712
  return this._internals.form;
679
713
  }
680
714
  set form(value) {
681
- assert.any([is.plainObject, is.undefined], value);
715
+ assertAny('form', [is.plainObject, is.undefined], value);
682
716
  if (value !== undefined) {
683
717
  assert.undefined(this._internals.body);
684
718
  assert.undefined(this._internals.json);
@@ -686,7 +720,9 @@ export default class Options {
686
720
  this._internals.form = value;
687
721
  }
688
722
  /**
689
- JSON body. If the `Content-Type` header is not set, it will be set to `application/json`.
723
+ JSON request body. If the `content-type` header is not set, it will be set to `application/json`.
724
+
725
+ __Important__: This option only affects the request body you send to the server. To parse the response as JSON, you must either call `.json()` on the promise or set `responseType: 'json'` in the options.
690
726
 
691
727
  __Note #1__: If you provide this option, `got.stream()` will be read-only.
692
728
 
@@ -724,7 +760,7 @@ export default class Options {
724
760
  return this._internals.url;
725
761
  }
726
762
  set url(value) {
727
- assert.any([is.string, is.urlInstance, is.undefined], value);
763
+ assertAny('url', [is.string, is.urlInstance, is.undefined], value);
728
764
  if (value === undefined) {
729
765
  this._internals.url = undefined;
730
766
  return;
@@ -784,7 +820,7 @@ export default class Options {
784
820
  return this._internals.cookieJar;
785
821
  }
786
822
  set cookieJar(value) {
787
- assert.any([is.object, is.undefined], value);
823
+ assertAny('cookieJar', [is.object, is.undefined], value);
788
824
  if (value === undefined) {
789
825
  this._internals.cookieJar = undefined;
790
826
  return;
@@ -871,7 +907,7 @@ export default class Options {
871
907
  return this._internals.searchParams;
872
908
  }
873
909
  set searchParams(value) {
874
- assert.any([is.string, is.object, is.undefined], value);
910
+ assertAny('searchParams', [is.string, is.object, is.undefined], value);
875
911
  const url = this._internals.url;
876
912
  if (value === undefined) {
877
913
  this._internals.searchParams = undefined;
@@ -931,7 +967,7 @@ export default class Options {
931
967
  return this._internals.dnsLookup;
932
968
  }
933
969
  set dnsLookup(value) {
934
- assert.any([is.function, is.undefined], value);
970
+ assertAny('dnsLookup', [is.function, is.undefined], value);
935
971
  this._internals.dnsLookup = value;
936
972
  }
937
973
  /**
@@ -948,7 +984,7 @@ export default class Options {
948
984
  return this._internals.dnsCache;
949
985
  }
950
986
  set dnsCache(value) {
951
- assert.any([is.object, is.boolean, is.undefined], value);
987
+ assertAny('dnsCache', [is.object, is.boolean, is.undefined], value);
952
988
  if (value === true) {
953
989
  this._internals.dnsCache = getGlobalDnsCache();
954
990
  }
@@ -1018,7 +1054,7 @@ export default class Options {
1018
1054
  }
1019
1055
  const typedKnownHookEvent = knownHookEvent;
1020
1056
  const hooks = value[typedKnownHookEvent];
1021
- assert.any([is.array, is.undefined], hooks);
1057
+ assertAny(`hooks.${knownHookEvent}`, [is.array, is.undefined], hooks);
1022
1058
  if (hooks) {
1023
1059
  for (const hook of hooks) {
1024
1060
  assert.function(hook);
@@ -1053,7 +1089,7 @@ export default class Options {
1053
1089
  return this._internals.followRedirect;
1054
1090
  }
1055
1091
  set followRedirect(value) {
1056
- assert.any([is.boolean, is.function], value);
1092
+ assertAny('followRedirect', [is.boolean, is.function], value);
1057
1093
  this._internals.followRedirect = value;
1058
1094
  }
1059
1095
  get followRedirects() {
@@ -1083,7 +1119,7 @@ export default class Options {
1083
1119
  return this._internals.cache;
1084
1120
  }
1085
1121
  set cache(value) {
1086
- assert.any([is.object, is.string, is.boolean, is.undefined], value);
1122
+ assertAny('cache', [is.object, is.string, is.boolean, is.undefined], value);
1087
1123
  if (value === true) {
1088
1124
  this._internals.cache = globalCache;
1089
1125
  }
@@ -1196,7 +1232,7 @@ export default class Options {
1196
1232
  return this._internals.headers;
1197
1233
  }
1198
1234
  set headers(value) {
1199
- assert.plainObject(value);
1235
+ assertPlainObject('headers', value);
1200
1236
  if (this._merging) {
1201
1237
  Object.assign(this._internals.headers, lowercaseKeys(value));
1202
1238
  }
@@ -1340,15 +1376,15 @@ export default class Options {
1340
1376
  return this._internals.retry;
1341
1377
  }
1342
1378
  set retry(value) {
1343
- assert.plainObject(value);
1344
- assert.any([is.function, is.undefined], value.calculateDelay);
1345
- assert.any([is.number, is.undefined], value.maxRetryAfter);
1346
- assert.any([is.number, is.undefined], value.limit);
1347
- assert.any([is.array, is.undefined], value.methods);
1348
- assert.any([is.array, is.undefined], value.statusCodes);
1349
- assert.any([is.array, is.undefined], value.errorCodes);
1350
- assert.any([is.number, is.undefined], value.noise);
1351
- assert.any([is.boolean, is.undefined], value.enforceRetryRules);
1379
+ assertPlainObject('retry', value);
1380
+ assertAny('retry.calculateDelay', [is.function, is.undefined], value.calculateDelay);
1381
+ assertAny('retry.maxRetryAfter', [is.number, is.undefined], value.maxRetryAfter);
1382
+ assertAny('retry.limit', [is.number, is.undefined], value.limit);
1383
+ assertAny('retry.methods', [is.array, is.undefined], value.methods);
1384
+ assertAny('retry.statusCodes', [is.array, is.undefined], value.statusCodes);
1385
+ assertAny('retry.errorCodes', [is.array, is.undefined], value.errorCodes);
1386
+ assertAny('retry.noise', [is.number, is.undefined], value.noise);
1387
+ assertAny('retry.enforceRetryRules', [is.boolean, is.undefined], value.enforceRetryRules);
1352
1388
  if (value.noise && Math.abs(value.noise) > 100) {
1353
1389
  throw new Error(`The maximum acceptable retry noise is +/- 100ms, got ${value.noise}`);
1354
1390
  }
@@ -1377,7 +1413,7 @@ export default class Options {
1377
1413
  return this._internals.localAddress;
1378
1414
  }
1379
1415
  set localAddress(value) {
1380
- assert.any([is.string, is.undefined], value);
1416
+ assertAny('localAddress', [is.string, is.undefined], value);
1381
1417
  this._internals.localAddress = value;
1382
1418
  }
1383
1419
  /**
@@ -1396,7 +1432,7 @@ export default class Options {
1396
1432
  return this._internals.createConnection;
1397
1433
  }
1398
1434
  set createConnection(value) {
1399
- assert.any([is.function, is.undefined], value);
1435
+ assertAny('createConnection', [is.function, is.undefined], value);
1400
1436
  this._internals.createConnection = value;
1401
1437
  }
1402
1438
  /**
@@ -1408,11 +1444,11 @@ export default class Options {
1408
1444
  return this._internals.cacheOptions;
1409
1445
  }
1410
1446
  set cacheOptions(value) {
1411
- assert.plainObject(value);
1412
- assert.any([is.boolean, is.undefined], value.shared);
1413
- assert.any([is.number, is.undefined], value.cacheHeuristic);
1414
- assert.any([is.number, is.undefined], value.immutableMinTimeToLive);
1415
- assert.any([is.boolean, is.undefined], value.ignoreCargoCult);
1447
+ assertPlainObject('cacheOptions', value);
1448
+ assertAny('cacheOptions.shared', [is.boolean, is.undefined], value.shared);
1449
+ assertAny('cacheOptions.cacheHeuristic', [is.number, is.undefined], value.cacheHeuristic);
1450
+ assertAny('cacheOptions.immutableMinTimeToLive', [is.number, is.undefined], value.immutableMinTimeToLive);
1451
+ assertAny('cacheOptions.ignoreCargoCult', [is.boolean, is.undefined], value.ignoreCargoCult);
1416
1452
  for (const key in value) {
1417
1453
  if (!(key in this._internals.cacheOptions)) {
1418
1454
  throw new Error(`Cache option \`${key}\` does not exist`);
@@ -1432,25 +1468,26 @@ export default class Options {
1432
1468
  return this._internals.https;
1433
1469
  }
1434
1470
  set https(value) {
1435
- assert.plainObject(value);
1436
- assert.any([is.boolean, is.undefined], value.rejectUnauthorized);
1437
- assert.any([is.function, is.undefined], value.checkServerIdentity);
1438
- assert.any([is.string, is.undefined], value.serverName);
1439
- assert.any([is.string, is.object, is.array, is.undefined], value.certificateAuthority);
1440
- assert.any([is.string, is.object, is.array, is.undefined], value.key);
1441
- assert.any([is.string, is.object, is.array, is.undefined], value.certificate);
1442
- assert.any([is.string, is.undefined], value.passphrase);
1443
- assert.any([is.string, is.buffer, is.array, is.undefined], value.pfx);
1444
- assert.any([is.array, is.undefined], value.alpnProtocols);
1445
- assert.any([is.string, is.undefined], value.ciphers);
1446
- assert.any([is.string, is.buffer, is.undefined], value.dhparam);
1447
- assert.any([is.string, is.undefined], value.signatureAlgorithms);
1448
- assert.any([is.string, is.undefined], value.minVersion);
1449
- assert.any([is.string, is.undefined], value.maxVersion);
1450
- assert.any([is.boolean, is.undefined], value.honorCipherOrder);
1451
- assert.any([is.number, is.undefined], value.tlsSessionLifetime);
1452
- assert.any([is.string, is.undefined], value.ecdhCurve);
1453
- assert.any([is.string, is.buffer, is.array, is.undefined], value.certificateRevocationLists);
1471
+ assertPlainObject('https', value);
1472
+ assertAny('https.rejectUnauthorized', [is.boolean, is.undefined], value.rejectUnauthorized);
1473
+ assertAny('https.checkServerIdentity', [is.function, is.undefined], value.checkServerIdentity);
1474
+ assertAny('https.serverName', [is.string, is.undefined], value.serverName);
1475
+ assertAny('https.certificateAuthority', [is.string, is.object, is.array, is.undefined], value.certificateAuthority);
1476
+ assertAny('https.key', [is.string, is.object, is.array, is.undefined], value.key);
1477
+ assertAny('https.certificate', [is.string, is.object, is.array, is.undefined], value.certificate);
1478
+ assertAny('https.passphrase', [is.string, is.undefined], value.passphrase);
1479
+ assertAny('https.pfx', [is.string, is.buffer, is.array, is.undefined], value.pfx);
1480
+ assertAny('https.alpnProtocols', [is.array, is.undefined], value.alpnProtocols);
1481
+ assertAny('https.ciphers', [is.string, is.undefined], value.ciphers);
1482
+ assertAny('https.dhparam', [is.string, is.buffer, is.undefined], value.dhparam);
1483
+ assertAny('https.signatureAlgorithms', [is.string, is.undefined], value.signatureAlgorithms);
1484
+ assertAny('https.minVersion', [is.string, is.undefined], value.minVersion);
1485
+ assertAny('https.maxVersion', [is.string, is.undefined], value.maxVersion);
1486
+ assertAny('https.honorCipherOrder', [is.boolean, is.undefined], value.honorCipherOrder);
1487
+ assertAny('https.tlsSessionLifetime', [is.number, is.undefined], value.tlsSessionLifetime);
1488
+ assertAny('https.ecdhCurve', [is.string, is.undefined], value.ecdhCurve);
1489
+ assertAny('https.certificateRevocationLists', [is.string, is.buffer, is.array, is.undefined], value.certificateRevocationLists);
1490
+ assertAny('https.secureOptions', [is.number, is.undefined], value.secureOptions);
1454
1491
  for (const key in value) {
1455
1492
  if (!(key in this._internals.https)) {
1456
1493
  throw new Error(`HTTPS option \`${key}\` does not exist`);
@@ -1480,7 +1517,7 @@ export default class Options {
1480
1517
  if (value === null) {
1481
1518
  throw new TypeError('To get a Buffer, set `options.responseType` to `buffer` instead');
1482
1519
  }
1483
- assert.any([is.string, is.undefined], value);
1520
+ assertAny('encoding', [is.string, is.undefined], value);
1484
1521
  this._internals.encoding = value;
1485
1522
  }
1486
1523
  /**
@@ -1580,7 +1617,7 @@ export default class Options {
1580
1617
  return this._internals.maxHeaderSize;
1581
1618
  }
1582
1619
  set maxHeaderSize(value) {
1583
- assert.any([is.number, is.undefined], value);
1620
+ assertAny('maxHeaderSize', [is.number, is.undefined], value);
1584
1621
  this._internals.maxHeaderSize = value;
1585
1622
  }
1586
1623
  get enableUnixSockets() {
@@ -1590,6 +1627,23 @@ export default class Options {
1590
1627
  assert.boolean(value);
1591
1628
  this._internals.enableUnixSockets = value;
1592
1629
  }
1630
+ /**
1631
+ Throw an error if the server response's `content-length` header value doesn't match the number of bytes received.
1632
+
1633
+ This is useful for detecting truncated responses and follows RFC 9112 requirements for message completeness.
1634
+
1635
+ __Note__: Responses without a `content-length` header are not validated.
1636
+ __Note__: When enabled and validation fails, a `ReadError` with code `ERR_HTTP_CONTENT_LENGTH_MISMATCH` will be thrown.
1637
+
1638
+ @default false
1639
+ */
1640
+ get strictContentLength() {
1641
+ return this._internals.strictContentLength;
1642
+ }
1643
+ set strictContentLength(value) {
1644
+ assert.boolean(value);
1645
+ this._internals.strictContentLength = value;
1646
+ }
1593
1647
  // eslint-disable-next-line @typescript-eslint/naming-convention
1594
1648
  toJSON() {
1595
1649
  return { ...this._internals };
@@ -1648,6 +1702,7 @@ export default class Options {
1648
1702
  dhparam: https.dhparam,
1649
1703
  ecdhCurve: https.ecdhCurve,
1650
1704
  crl: https.certificateRevocationLists,
1705
+ secureOptions: https.secureOptions,
1651
1706
  // HTTP options
1652
1707
  lookup: internals.dnsLookup ?? internals.dnsCache?.lookup,
1653
1708
  family: internals.dnsLookupIpVersion,
@@ -15,6 +15,9 @@ export default async function getBodySize(body, headers) {
15
15
  if (is.buffer(body)) {
16
16
  return body.length;
17
17
  }
18
+ if (is.typedArray(body)) {
19
+ return body.byteLength;
20
+ }
18
21
  if (isFormData(body)) {
19
22
  try {
20
23
  return await promisify(body.getLength.bind(body))();
@@ -1 +1,17 @@
1
1
  export default function isUnixSocketURL(url: URL): boolean;
2
+ /**
3
+ Extract the socket path from a UNIX socket URL.
4
+
5
+ @example
6
+ ```
7
+ getUnixSocketPath(new URL('http://unix/foo:/path'));
8
+ //=> '/foo'
9
+
10
+ getUnixSocketPath(new URL('unix:/foo:/path'));
11
+ //=> '/foo'
12
+
13
+ getUnixSocketPath(new URL('http://example.com'));
14
+ //=> undefined
15
+ ```
16
+ */
17
+ export declare function getUnixSocketPath(url: URL): string | undefined;
@@ -2,3 +2,24 @@
2
2
  export default function isUnixSocketURL(url) {
3
3
  return url.protocol === 'unix:' || url.hostname === 'unix';
4
4
  }
5
+ /**
6
+ Extract the socket path from a UNIX socket URL.
7
+
8
+ @example
9
+ ```
10
+ getUnixSocketPath(new URL('http://unix/foo:/path'));
11
+ //=> '/foo'
12
+
13
+ getUnixSocketPath(new URL('unix:/foo:/path'));
14
+ //=> '/foo'
15
+
16
+ getUnixSocketPath(new URL('http://example.com'));
17
+ //=> undefined
18
+ ```
19
+ */
20
+ export function getUnixSocketPath(url) {
21
+ if (!isUnixSocketURL(url)) {
22
+ return undefined;
23
+ }
24
+ return /(?<socketPath>.+?):(?<path>.+)/.exec(`${url.pathname}${url.search}`)?.groups?.socketPath;
25
+ }
@@ -139,7 +139,15 @@ const create = (defaults) => {
139
139
  }
140
140
  else {
141
141
  normalizedOptions.merge(optionsToMerge);
142
- assert.any([is.urlInstance, is.undefined], optionsToMerge.url);
142
+ try {
143
+ assert.any([is.urlInstance, is.undefined], optionsToMerge.url);
144
+ }
145
+ catch (error) {
146
+ if (error instanceof Error) {
147
+ error.message = `Option 'pagination.paginate.url': ${error.message}`;
148
+ }
149
+ throw error;
150
+ }
143
151
  if (optionsToMerge.url !== undefined) {
144
152
  normalizedOptions.prefixUrl = '';
145
153
  normalizedOptions.url = optionsToMerge.url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "got",
3
- "version": "14.5.0",
3
+ "version": "14.6.0",
4
4
  "description": "Human-friendly and powerful HTTP request library for Node.js",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/got",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "scripts": {
18
18
  "test": "xo && tsc --noEmit && NODE_OPTIONS='--import=tsx/esm' ava",
19
+ "test:coverage": "xo && tsc --noEmit && NODE_OPTIONS='--import=tsx/esm' c8 ava",
19
20
  "release": "np",
20
21
  "build": "del-cli dist && tsc",
21
22
  "prepare": "npm run build"
@@ -40,6 +41,8 @@
40
41
  "network",
41
42
  "gzip",
42
43
  "brotli",
44
+ "zstd",
45
+ "zstandard",
43
46
  "requests",
44
47
  "human-friendly",
45
48
  "axios",
@@ -52,7 +55,7 @@
52
55
  "@szmarczak/http-timer": "^5.0.1",
53
56
  "cacheable-lookup": "^7.0.0",
54
57
  "cacheable-request": "^13.0.12",
55
- "decompress-response": "^6.0.0",
58
+ "decompress-response": "^10.0.0",
56
59
  "form-data-encoder": "^4.0.2",
57
60
  "http2-wrapper": "^2.2.1",
58
61
  "keyv": "^5.5.3",
@@ -78,6 +81,7 @@
78
81
  "benchmark": "^2.1.4",
79
82
  "bluebird": "^3.7.2",
80
83
  "body-parser": "^1.20.3",
84
+ "c8": "^10.1.3",
81
85
  "create-cert": "^1.0.6",
82
86
  "create-test-server": "^3.0.1",
83
87
  "del-cli": "^6.0.0",
@@ -90,7 +94,6 @@
90
94
  "nock": "^13.5.5",
91
95
  "node-fetch": "^3.3.2",
92
96
  "np": "^10.0.5",
93
- "nyc": "^17.1.0",
94
97
  "p-event": "^6.0.1",
95
98
  "pem": "^1.14.8",
96
99
  "pify": "^6.1.0",
@@ -117,17 +120,15 @@
117
120
  },
118
121
  "workerThreads": false
119
122
  },
120
- "nyc": {
123
+ "c8": {
121
124
  "reporter": [
122
125
  "text",
123
126
  "html",
124
127
  "lcov"
125
128
  ],
126
- "extension": [
127
- ".ts"
128
- ],
129
129
  "exclude": [
130
- "**/test/**"
130
+ "test/**",
131
+ "dist/**"
131
132
  ]
132
133
  },
133
134
  "xo": {
package/readme.md CHANGED
@@ -127,6 +127,7 @@ By default, Got will retry on failure. To disable this option, set [`options.ret
127
127
 
128
128
  #### Integration
129
129
 
130
+ - [x] [Diagnostics Channel](documentation/diagnostics-channel.md)
130
131
  - [x] [TypeScript support](documentation/typescript.md)
131
132
  - [x] [AWS](documentation/tips.md#aws)
132
133
  - [x] [Testing](documentation/tips.md#testing)