got 14.6.1 → 14.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { Timings } from '@szmarczak/http-timer';
1
+ import type { Timings } from './utils/timer.js';
2
2
  import type { RequestError } from './errors.js';
3
3
  export type RequestId = string;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import type { Timings } from '@szmarczak/http-timer';
1
+ import type { Timings } from './utils/timer.js';
2
2
  import type Options from './options.js';
3
3
  import type { TimeoutError as TimedOutTimeoutError } from './timed-out.js';
4
4
  import type { PlainResponse, Response } from './response.js';
@@ -1,7 +1,7 @@
1
1
  import { Duplex } from 'node:stream';
2
2
  import { type ClientRequest } from 'node:http';
3
3
  import type { Socket } from 'node:net';
4
- import { type Timings } from '@szmarczak/http-timer';
4
+ import { type Timings } from './utils/timer.js';
5
5
  import Options, { type OptionsInit } from './options.js';
6
6
  import { type PlainResponse, type Response } from './response.js';
7
7
  import { RequestError } from './errors.js';
@@ -107,7 +107,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
107
107
  private _flushed;
108
108
  private _aborted;
109
109
  private _expectedContentLength?;
110
- private _byteCounter?;
110
+ private _compressedBytesCount?;
111
111
  private readonly _requestId;
112
112
  private _requestInitialized;
113
113
  constructor(url: UrlType, options?: OptionsType, defaults?: DefaultsType);
@@ -1,12 +1,13 @@
1
1
  import process from 'node:process';
2
2
  import { Buffer } from 'node:buffer';
3
- import { Duplex, Transform } from 'node:stream';
3
+ import { Duplex } from 'node:stream';
4
4
  import http, { ServerResponse } from 'node:http';
5
- import timer from '@szmarczak/http-timer';
5
+ import { byteLength } from 'byte-counter';
6
6
  import CacheableRequest, { CacheError as CacheableCacheError, } from 'cacheable-request';
7
7
  import decompressResponse from 'decompress-response';
8
8
  import is, { isBuffer } from '@sindresorhus/is';
9
9
  import { FormDataEncoder, isFormData as isFormDataLike } from 'form-data-encoder';
10
+ import timer from './utils/timer.js';
10
11
  import getBodySize from './utils/get-body-size.js';
11
12
  import isFormData from './utils/is-form-data.js';
12
13
  import proxyEvents from './utils/proxy-events.js';
@@ -37,17 +38,6 @@ const proxiedRequestEvents = [
37
38
  'upgrade',
38
39
  ];
39
40
  const noop = () => { };
40
- /**
41
- Stream transform that counts bytes passing through.
42
- Used to track compressed bytes before decompression for content-length validation.
43
- */
44
- class ByteCounter extends Transform {
45
- count = 0;
46
- _transform(chunk, _encoding, callback) {
47
- this.count += chunk.length;
48
- callback(null, chunk);
49
- }
50
- }
51
41
  export default class Request extends Duplex {
52
42
  // @ts-expect-error - Ignoring for now.
53
43
  ['constructor'];
@@ -76,7 +66,7 @@ export default class Request extends Duplex {
76
66
  _flushed = false;
77
67
  _aborted = false;
78
68
  _expectedContentLength;
79
- _byteCounter;
69
+ _compressedBytesCount;
80
70
  _requestId = generateRequestId();
81
71
  // We need this because `this._request` if `undefined` when using cache
82
72
  _requestInitialized = false;
@@ -473,9 +463,9 @@ export default class Request extends Duplex {
473
463
  }
474
464
  _checkContentLengthMismatch() {
475
465
  if (this.options.strictContentLength && this._expectedContentLength !== undefined) {
476
- // Use ByteCounter's count when available (for compressed responses),
466
+ // Use compressed bytes count when available (for compressed responses),
477
467
  // otherwise use _downloadedSize (for uncompressed responses)
478
- const actualSize = this._byteCounter?.count ?? this._downloadedSize;
468
+ const actualSize = this._compressedBytesCount ?? this._downloadedSize;
479
469
  if (actualSize !== this._expectedContentLength) {
480
470
  this._beforeError(new ReadError({
481
471
  message: `Content-Length mismatch: expected ${this._expectedContentLength} bytes, received ${actualSize} bytes`,
@@ -578,9 +568,9 @@ export default class Request extends Duplex {
578
568
  // When strictContentLength is enabled, track compressed bytes by listening to
579
569
  // the native response's data events before decompression
580
570
  if (options.strictContentLength) {
581
- this._byteCounter = new ByteCounter();
571
+ this._compressedBytesCount = 0;
582
572
  this._nativeResponse.on('data', (chunk) => {
583
- this._byteCounter.count += chunk.length;
573
+ this._compressedBytesCount += byteLength(chunk);
584
574
  });
585
575
  }
586
576
  response = decompressResponse(response);
@@ -606,27 +596,6 @@ export default class Request extends Duplex {
606
596
  headers: response.headers,
607
597
  isFromCache: typedResponse.isFromCache,
608
598
  });
609
- // Workaround for http-timer bug: when connecting to an IP address (no DNS lookup),
610
- // http-timer sets lookup = connect instead of lookup = socket, resulting in
611
- // dns = lookup - socket being a small positive number instead of 0.
612
- // See https://github.com/sindresorhus/got/issues/2279
613
- const { timings } = response;
614
- if (timings?.lookup !== undefined && timings.socket !== undefined && timings.connect !== undefined && timings.lookup === timings.connect && timings.phases.dns !== 0) {
615
- // Fix the DNS phase to be 0 and set lookup to socket time
616
- timings.phases.dns = 0;
617
- timings.lookup = timings.socket;
618
- // Recalculate TCP time to be the full time from socket to connect
619
- timings.phases.tcp = timings.connect - timings.socket;
620
- }
621
- // Workaround for http-timer limitation with HTTP/2:
622
- // When using HTTP/2, the socket is a proxy that http-timer discards,
623
- // so lookup, connect, and secureConnect events are never captured.
624
- // This results in phases.request being NaN (undefined - undefined).
625
- // Set it to undefined to be consistent with other unavailable timings.
626
- // See https://github.com/sindresorhus/got/issues/1958
627
- if (timings && Number.isNaN(timings.phases.request)) {
628
- timings.phases.request = undefined;
629
- }
630
599
  response.once('error', (error) => {
631
600
  this._aborted = true;
632
601
  // Force clean-up, because some packages don't do this.
@@ -1259,7 +1228,9 @@ export default class Request extends Duplex {
1259
1228
  this._request.write(chunk, encoding, (error) => {
1260
1229
  // The `!destroyed` check is required to prevent `uploadProgress` being emitted after the stream was destroyed
1261
1230
  if (!error && !this._request.destroyed) {
1262
- this._uploadedSize += Buffer.byteLength(chunk, encoding);
1231
+ // For strings, encode them first to measure the actual bytes that will be sent
1232
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk;
1233
+ this._uploadedSize += byteLength(bytes);
1263
1234
  const progress = this.uploadProgress;
1264
1235
  if (progress.percent < 1) {
1265
1236
  this.emit('uploadProgress', progress);
@@ -10,8 +10,8 @@ import { type FormDataLike } from 'form-data-encoder';
10
10
  import type { KeyvStoreAdapter } from 'keyv';
11
11
  import type KeyvType from 'keyv';
12
12
  import type ResponseLike from 'responselike';
13
- import type { IncomingMessageWithTimings } from '@szmarczak/http-timer';
14
13
  import type { CancelableRequest } from '../as-promise/types.js';
14
+ import type { IncomingMessageWithTimings } from './utils/timer.js';
15
15
  import type { PlainResponse, Response } from './response.js';
16
16
  import type { RequestError } from './errors.js';
17
17
  import type { Delays } from './timed-out.js';
@@ -1435,8 +1435,8 @@ export default class Options {
1435
1435
  get strictContentLength(): boolean;
1436
1436
  set strictContentLength(value: boolean);
1437
1437
  toJSON(): {
1438
- headers: Headers;
1439
1438
  timeout: Delays;
1439
+ headers: Headers;
1440
1440
  request: RequestFunction | undefined;
1441
1441
  username: string;
1442
1442
  password: string;
@@ -1528,7 +1528,6 @@ export default class Options {
1528
1528
  _defaultAgent?: http.Agent | undefined;
1529
1529
  auth?: string | null | undefined;
1530
1530
  defaultPort?: number | string | undefined;
1531
- hints?: import("dns").LookupOptions["hints"];
1532
1531
  host?: string | null | undefined;
1533
1532
  hostname?: string | null | undefined;
1534
1533
  insecureHTTPParser?: boolean | undefined;
@@ -1540,7 +1539,8 @@ export default class Options {
1540
1539
  signal?: AbortSignal | undefined;
1541
1540
  socketPath?: string | undefined;
1542
1541
  uniqueHeaders?: Array<string | string[]> | undefined;
1543
- joinDuplicateHeaders?: boolean;
1542
+ joinDuplicateHeaders?: boolean | undefined;
1543
+ hints?: number | undefined;
1544
1544
  ALPNCallback?: ((arg: {
1545
1545
  servername: string;
1546
1546
  protocols: string[];
@@ -781,7 +781,7 @@ export default class Options {
781
781
  }
782
782
  // Detect if URL is already absolute (has a protocol/scheme)
783
783
  const valueString = value.toString();
784
- const isAbsolute = is.urlInstance(value) || /^[a-z][a-z\d+.-]*:/i.test(valueString);
784
+ const isAbsolute = is.urlInstance(value) || /^[a-z][a-z\d+.-]*:\/\//i.test(valueString);
785
785
  // Only concatenate prefixUrl if the URL is relative
786
786
  const urlString = isAbsolute ? valueString : `${this.prefixUrl}${valueString}`;
787
787
  const url = new URL(urlString);
@@ -1,5 +1,5 @@
1
1
  import type { Buffer } from 'node:buffer';
2
- import type { IncomingMessageWithTimings, Timings } from '@szmarczak/http-timer';
2
+ import type { IncomingMessageWithTimings, Timings } from './utils/timer.js';
3
3
  import { RequestError } from './errors.js';
4
4
  import type { ParseJsonFunction, ResponseType } from './options.js';
5
5
  import type Request from './index.js';
@@ -0,0 +1,9 @@
1
+ import type { Socket } from 'node:net';
2
+ import type { TLSSocket } from 'node:tls';
3
+ type Listeners = {
4
+ connect?: () => void;
5
+ secureConnect?: () => void;
6
+ close?: (hadError: boolean) => void;
7
+ };
8
+ declare const deferToConnect: (socket: TLSSocket | Socket, fn: Listeners | (() => void)) => void;
9
+ export default deferToConnect;
@@ -0,0 +1,44 @@
1
+ function isTlsSocket(socket) {
2
+ return 'encrypted' in socket;
3
+ }
4
+ const deferToConnect = (socket, fn) => {
5
+ let listeners;
6
+ if (typeof fn === 'function') {
7
+ const connect = fn;
8
+ listeners = { connect };
9
+ }
10
+ else {
11
+ listeners = fn;
12
+ }
13
+ const hasConnectListener = typeof listeners.connect === 'function';
14
+ const hasSecureConnectListener = typeof listeners.secureConnect === 'function';
15
+ const hasCloseListener = typeof listeners.close === 'function';
16
+ const onConnect = () => {
17
+ if (hasConnectListener) {
18
+ listeners.connect();
19
+ }
20
+ if (isTlsSocket(socket) && hasSecureConnectListener) {
21
+ if (socket.authorized) {
22
+ listeners.secureConnect();
23
+ }
24
+ else {
25
+ // Wait for secureConnect event (even if authorization fails, we need the timing)
26
+ socket.once('secureConnect', listeners.secureConnect);
27
+ }
28
+ }
29
+ if (hasCloseListener) {
30
+ socket.once('close', listeners.close);
31
+ }
32
+ };
33
+ if (socket.writable && !socket.connecting) {
34
+ onConnect();
35
+ }
36
+ else if (socket.connecting) {
37
+ socket.once('connect', onConnect);
38
+ }
39
+ else if (socket.destroyed && hasCloseListener) {
40
+ const hadError = '_hadError' in socket ? Boolean(socket._hadError) : false;
41
+ listeners.close(hadError);
42
+ }
43
+ };
44
+ export default deferToConnect;
@@ -0,0 +1,31 @@
1
+ import type { ClientRequest, IncomingMessage } from 'node:http';
2
+ export type Timings = {
3
+ start: number;
4
+ socket?: number;
5
+ lookup?: number;
6
+ connect?: number;
7
+ secureConnect?: number;
8
+ upload?: number;
9
+ response?: number;
10
+ end?: number;
11
+ error?: number;
12
+ abort?: number;
13
+ phases: {
14
+ wait?: number;
15
+ dns?: number;
16
+ tcp?: number;
17
+ tls?: number;
18
+ request?: number;
19
+ firstByte?: number;
20
+ download?: number;
21
+ total?: number;
22
+ };
23
+ };
24
+ export type ClientRequestWithTimings = ClientRequest & {
25
+ timings?: Timings;
26
+ };
27
+ export type IncomingMessageWithTimings = IncomingMessage & {
28
+ timings?: Timings;
29
+ };
30
+ declare const timer: (request: ClientRequestWithTimings) => Timings;
31
+ export default timer;
@@ -0,0 +1,162 @@
1
+ import { errorMonitor } from 'node:events';
2
+ import { types } from 'node:util';
3
+ import deferToConnect from './defer-to-connect.js';
4
+ const timer = (request) => {
5
+ if (request.timings) {
6
+ return request.timings;
7
+ }
8
+ const timings = {
9
+ start: Date.now(),
10
+ socket: undefined,
11
+ lookup: undefined,
12
+ connect: undefined,
13
+ secureConnect: undefined,
14
+ upload: undefined,
15
+ response: undefined,
16
+ end: undefined,
17
+ error: undefined,
18
+ abort: undefined,
19
+ phases: {
20
+ wait: undefined,
21
+ dns: undefined,
22
+ tcp: undefined,
23
+ tls: undefined,
24
+ request: undefined,
25
+ firstByte: undefined,
26
+ download: undefined,
27
+ total: undefined,
28
+ },
29
+ };
30
+ request.timings = timings;
31
+ const handleError = (origin) => {
32
+ origin.once(errorMonitor, () => {
33
+ timings.error = Date.now();
34
+ timings.phases.total = timings.error - timings.start;
35
+ });
36
+ };
37
+ handleError(request);
38
+ const onAbort = () => {
39
+ timings.abort = Date.now();
40
+ timings.phases.total = timings.abort - timings.start;
41
+ };
42
+ request.prependOnceListener('abort', onAbort);
43
+ const onSocket = (socket) => {
44
+ timings.socket = Date.now();
45
+ timings.phases.wait = timings.socket - timings.start;
46
+ if (types.isProxy(socket)) {
47
+ // HTTP/2: The socket is a proxy, so connection events won't fire.
48
+ // We can't measure connection timings, so leave them undefined.
49
+ // This prevents NaN in phases.request calculation.
50
+ return;
51
+ }
52
+ // Check if socket is already connected (reused from connection pool)
53
+ const socketAlreadyConnected = socket.writable && !socket.connecting;
54
+ if (socketAlreadyConnected) {
55
+ // Socket reuse detected: the socket was already connected from a previous request.
56
+ // For reused sockets, set all connection timestamps to socket time since no new
57
+ // connection was made for THIS request. But preserve phase durations from the
58
+ // original connection so they're not lost.
59
+ timings.lookup = timings.socket;
60
+ timings.connect = timings.socket;
61
+ if (socket.__initial_connection_timings__) {
62
+ // Restore the phase timings from the initial connection
63
+ timings.phases.dns = socket.__initial_connection_timings__.dnsPhase;
64
+ timings.phases.tcp = socket.__initial_connection_timings__.tcpPhase;
65
+ timings.phases.tls = socket.__initial_connection_timings__.tlsPhase;
66
+ // Set secureConnect timestamp if there was TLS
67
+ if (timings.phases.tls !== undefined) {
68
+ timings.secureConnect = timings.socket;
69
+ }
70
+ }
71
+ else {
72
+ // Socket reused but no initial timings stored (e.g., from external code)
73
+ // Set phases to 0
74
+ timings.phases.dns = 0;
75
+ timings.phases.tcp = 0;
76
+ }
77
+ return;
78
+ }
79
+ const lookupListener = () => {
80
+ timings.lookup = Date.now();
81
+ timings.phases.dns = timings.lookup - timings.socket;
82
+ };
83
+ socket.prependOnceListener('lookup', lookupListener);
84
+ deferToConnect(socket, {
85
+ connect() {
86
+ timings.connect = Date.now();
87
+ if (timings.lookup === undefined) {
88
+ // No DNS lookup occurred (e.g., connecting to an IP address directly)
89
+ // Set lookup to socket time (no time elapsed for DNS)
90
+ socket.removeListener('lookup', lookupListener);
91
+ timings.lookup = timings.socket;
92
+ timings.phases.dns = 0;
93
+ }
94
+ timings.phases.tcp = timings.connect - timings.lookup;
95
+ // If lookup and connect happen at the EXACT same time (tcp = 0),
96
+ // DNS was served from cache and the dns value is just event loop overhead.
97
+ // Set dns to 0 to indicate no actual DNS resolution occurred.
98
+ // Fixes https://github.com/szmarczak/http-timer/issues/35
99
+ if (timings.phases.tcp === 0 && timings.phases.dns && timings.phases.dns > 0) {
100
+ timings.phases.dns = 0;
101
+ }
102
+ // Store connection phase timings on socket for potential reuse
103
+ if (!socket.__initial_connection_timings__) {
104
+ socket.__initial_connection_timings__ = {
105
+ dnsPhase: timings.phases.dns,
106
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript can't prove this is defined due to callback structure
107
+ tcpPhase: timings.phases.tcp,
108
+ };
109
+ }
110
+ },
111
+ secureConnect() {
112
+ timings.secureConnect = Date.now();
113
+ timings.phases.tls = timings.secureConnect - timings.connect;
114
+ // Update stored timings with TLS phase timing
115
+ if (socket.__initial_connection_timings__) {
116
+ socket.__initial_connection_timings__.tlsPhase = timings.phases.tls;
117
+ }
118
+ },
119
+ });
120
+ };
121
+ if (request.socket) {
122
+ onSocket(request.socket);
123
+ }
124
+ else {
125
+ request.prependOnceListener('socket', onSocket);
126
+ }
127
+ const onUpload = () => {
128
+ timings.upload = Date.now();
129
+ // Calculate request phase if we have connection timings
130
+ const secureOrConnect = timings.secureConnect ?? timings.connect;
131
+ if (secureOrConnect !== undefined) {
132
+ timings.phases.request = timings.upload - secureOrConnect;
133
+ }
134
+ // If both are undefined (HTTP/2), phases.request stays undefined (not NaN)
135
+ };
136
+ if (request.writableFinished) {
137
+ onUpload();
138
+ }
139
+ else {
140
+ request.prependOnceListener('finish', onUpload);
141
+ }
142
+ request.prependOnceListener('response', (response) => {
143
+ timings.response = Date.now();
144
+ timings.phases.firstByte = timings.response - timings.upload;
145
+ response.timings = timings;
146
+ handleError(response);
147
+ response.prependOnceListener('end', () => {
148
+ request.off('abort', onAbort);
149
+ response.off('aborted', onAbort);
150
+ if (timings.phases.total !== undefined) {
151
+ // Aborted or errored
152
+ return;
153
+ }
154
+ timings.end = Date.now();
155
+ timings.phases.download = timings.end - timings.response;
156
+ timings.phases.total = timings.end - timings.start;
157
+ });
158
+ response.prependOnceListener('aborted', onAbort);
159
+ });
160
+ return timings;
161
+ };
162
+ export default timer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "got",
3
- "version": "14.6.1",
3
+ "version": "14.6.3",
4
4
  "description": "Human-friendly and powerful HTTP request library for Node.js",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/got",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@sindresorhus/is": "^7.0.1",
55
- "@szmarczak/http-timer": "^5.0.1",
55
+ "byte-counter": "^0.1.0",
56
56
  "cacheable-lookup": "^7.0.0",
57
57
  "cacheable-request": "^13.0.12",
58
58
  "decompress-response": "^10.0.0",