got 14.5.0 → 14.6.1

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) => {
@@ -38,7 +40,7 @@ export default function asPromise(firstRequest) {
38
40
  request.once('response', async (response) => {
39
41
  // Parse body
40
42
  const contentEncoding = (response.headers['content-encoding'] ?? '').toLowerCase();
41
- const isCompressed = contentEncoding === 'gzip' || contentEncoding === 'deflate' || contentEncoding === 'br';
43
+ const isCompressed = contentEncoding === 'gzip' || contentEncoding === 'deflate' || contentEncoding === 'br' || contentEncoding === 'zstd';
42
44
  const { options } = request;
43
45
  if (isCompressed && !options.decompress) {
44
46
  response.body = response.rawBody;
@@ -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';
@@ -142,27 +170,35 @@ export default function asPromise(firstRequest) {
142
170
  emitter.off(event, function_);
143
171
  return promise;
144
172
  };
145
- const shortcut = (responseType) => {
173
+ const shortcut = (promiseToAwait, responseType) => {
146
174
  const newPromise = (async () => {
147
175
  // Wait until downloading has ended
148
- await promise;
176
+ await promiseToAwait;
149
177
  const { options } = globalResponse.request;
150
178
  return parseBody(globalResponse, responseType, options.parseJson, options.encoding);
151
179
  })();
152
180
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
153
- Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise));
181
+ Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promiseToAwait));
154
182
  return newPromise;
155
183
  };
156
- promise.json = () => {
184
+ // Note: These use `function` syntax (not arrows) to access `this` context.
185
+ // When custom handlers wrap the promise to transform errors, these methods
186
+ // are copied to the handler's promise. Using `this` ensures we await the
187
+ // handler's wrapped promise, not the original, so errors propagate correctly.
188
+ promise.json = function () {
157
189
  if (globalRequest.options) {
158
190
  const { headers } = globalRequest.options;
159
191
  if (!globalRequest.writableFinished && !('accept' in headers)) {
160
192
  headers.accept = 'application/json';
161
193
  }
162
194
  }
163
- return shortcut('json');
195
+ return shortcut(this, 'json');
196
+ };
197
+ promise.buffer = function () {
198
+ return shortcut(this, 'buffer');
199
+ };
200
+ promise.text = function () {
201
+ return shortcut(this, 'text');
164
202
  };
165
- promise.buffer = () => shortcut('buffer');
166
- promise.text = () => shortcut('text');
167
203
  return promise;
168
204
  }
@@ -0,0 +1,89 @@
1
+ import type { Timings } from '@szmarczak/http-timer';
2
+ import type { RequestError } from './errors.js';
3
+ export type RequestId = string;
4
+ /**
5
+ Message for the `got:request:create` diagnostic channel.
6
+
7
+ Emitted when a request is created.
8
+ */
9
+ export type DiagnosticRequestCreate = {
10
+ requestId: RequestId;
11
+ url: string;
12
+ method: string;
13
+ };
14
+ /**
15
+ Message for the `got:request:start` diagnostic channel.
16
+
17
+ Emitted before the native HTTP request is sent.
18
+ */
19
+ export type DiagnosticRequestStart = {
20
+ requestId: RequestId;
21
+ url: string;
22
+ method: string;
23
+ headers: Record<string, string | string[] | undefined>;
24
+ };
25
+ /**
26
+ Message for the `got:response:start` diagnostic channel.
27
+
28
+ Emitted when response headers are received.
29
+ */
30
+ export type DiagnosticResponseStart = {
31
+ requestId: RequestId;
32
+ url: string;
33
+ statusCode: number;
34
+ headers: Record<string, string | string[] | undefined>;
35
+ isFromCache: boolean;
36
+ };
37
+ /**
38
+ Message for the `got:response:end` diagnostic channel.
39
+
40
+ Emitted when the response completes.
41
+ */
42
+ export type DiagnosticResponseEnd = {
43
+ requestId: RequestId;
44
+ url: string;
45
+ statusCode: number;
46
+ bodySize?: number;
47
+ timings?: Timings;
48
+ };
49
+ /**
50
+ Message for the `got:request:retry` diagnostic channel.
51
+
52
+ Emitted when retrying a request.
53
+ */
54
+ export type DiagnosticRequestRetry = {
55
+ requestId: RequestId;
56
+ retryCount: number;
57
+ error: RequestError;
58
+ delay: number;
59
+ };
60
+ /**
61
+ Message for the `got:request:error` diagnostic channel.
62
+
63
+ Emitted when a request fails.
64
+ */
65
+ export type DiagnosticRequestError = {
66
+ requestId: RequestId;
67
+ url: string;
68
+ error: RequestError;
69
+ timings?: Timings;
70
+ };
71
+ /**
72
+ Message for the `got:response:redirect` diagnostic channel.
73
+
74
+ Emitted when following a redirect.
75
+ */
76
+ export type DiagnosticResponseRedirect = {
77
+ requestId: RequestId;
78
+ fromUrl: string;
79
+ toUrl: string;
80
+ statusCode: number;
81
+ };
82
+ export declare function generateRequestId(): RequestId;
83
+ export declare function publishRequestCreate(message: DiagnosticRequestCreate): void;
84
+ export declare function publishRequestStart(message: DiagnosticRequestStart): void;
85
+ export declare function publishResponseStart(message: DiagnosticResponseStart): void;
86
+ export declare function publishResponseEnd(message: DiagnosticResponseEnd): void;
87
+ export declare function publishRetry(message: DiagnosticRequestRetry): void;
88
+ export declare function publishError(message: DiagnosticRequestError): void;
89
+ export declare function publishRedirect(message: DiagnosticResponseRedirect): void;
@@ -0,0 +1,49 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import diagnosticsChannel from 'node:diagnostics_channel';
3
+ const channels = {
4
+ requestCreate: diagnosticsChannel.channel('got:request:create'),
5
+ requestStart: diagnosticsChannel.channel('got:request:start'),
6
+ responseStart: diagnosticsChannel.channel('got:response:start'),
7
+ responseEnd: diagnosticsChannel.channel('got:response:end'),
8
+ retry: diagnosticsChannel.channel('got:request:retry'),
9
+ error: diagnosticsChannel.channel('got:request:error'),
10
+ redirect: diagnosticsChannel.channel('got:response:redirect'),
11
+ };
12
+ export function generateRequestId() {
13
+ return randomUUID();
14
+ }
15
+ export function publishRequestCreate(message) {
16
+ if (channels.requestCreate.hasSubscribers) {
17
+ channels.requestCreate.publish(message);
18
+ }
19
+ }
20
+ export function publishRequestStart(message) {
21
+ if (channels.requestStart.hasSubscribers) {
22
+ channels.requestStart.publish(message);
23
+ }
24
+ }
25
+ export function publishResponseStart(message) {
26
+ if (channels.responseStart.hasSubscribers) {
27
+ channels.responseStart.publish(message);
28
+ }
29
+ }
30
+ export function publishResponseEnd(message) {
31
+ if (channels.responseEnd.hasSubscribers) {
32
+ channels.responseEnd.publish(message);
33
+ }
34
+ }
35
+ export function publishRetry(message) {
36
+ if (channels.retry.hasSubscribers) {
37
+ channels.retry.publish(message);
38
+ }
39
+ }
40
+ export function publishError(message) {
41
+ if (channels.error.hasSubscribers) {
42
+ channels.error.publish(message);
43
+ }
44
+ }
45
+ export function publishRedirect(message) {
46
+ if (channels.redirect.hasSubscribers) {
47
+ channels.redirect.publish(message);
48
+ }
49
+ }
@@ -9,8 +9,9 @@ An error to be thrown when a request fails.
9
9
  Contains a `code` property with error class code, like `ECONNREFUSED`.
10
10
  */
11
11
  export declare class RequestError<T = unknown> extends Error {
12
- input?: string;
12
+ name: string;
13
13
  code: string;
14
+ input?: string;
14
15
  stack: string;
15
16
  readonly options: Options;
16
17
  readonly response?: Response<T>;
@@ -25,6 +26,8 @@ An error to be thrown when the server redirects you more than ten times.
25
26
  Includes a `response` property.
26
27
  */
27
28
  export declare class MaxRedirectsError extends RequestError {
29
+ name: string;
30
+ code: string;
28
31
  readonly response: Response;
29
32
  readonly request: Request;
30
33
  readonly timings: Timings;
@@ -35,6 +38,8 @@ An error to be thrown when the server response code is not 2xx nor 3xx if `optio
35
38
  Includes a `response` property.
36
39
  */
37
40
  export declare class HTTPError<T = any> extends RequestError<T> {
41
+ name: string;
42
+ code: string;
38
43
  readonly response: Response<T>;
39
44
  readonly request: Request;
40
45
  readonly timings: Timings;
@@ -45,6 +50,7 @@ An error to be thrown when a cache method fails.
45
50
  For example, if the database goes down or there's a filesystem error.
46
51
  */
47
52
  export declare class CacheError extends RequestError {
53
+ name: string;
48
54
  readonly request: Request;
49
55
  constructor(error: Error, request: Request);
50
56
  }
@@ -52,6 +58,7 @@ export declare class CacheError extends RequestError {
52
58
  An error to be thrown when the request body is a stream and an error occurs while reading from that stream.
53
59
  */
54
60
  export declare class UploadError extends RequestError {
61
+ name: string;
55
62
  readonly request: Request;
56
63
  constructor(error: Error, request: Request);
57
64
  }
@@ -60,6 +67,7 @@ An error to be thrown when the request is aborted due to a timeout.
60
67
  Includes an `event` and `timings` property.
61
68
  */
62
69
  export declare class TimeoutError extends RequestError {
70
+ name: string;
63
71
  readonly request: Request;
64
72
  readonly timings: Timings;
65
73
  readonly event: string;
@@ -69,6 +77,7 @@ export declare class TimeoutError extends RequestError {
69
77
  An error to be thrown when reading from response stream fails.
70
78
  */
71
79
  export declare class ReadError extends RequestError {
80
+ name: string;
72
81
  readonly request: Request;
73
82
  readonly response: Response;
74
83
  readonly timings: Timings;
@@ -78,12 +87,16 @@ export declare class ReadError extends RequestError {
78
87
  An error which always triggers a new retry when thrown.
79
88
  */
80
89
  export declare class RetryError extends RequestError {
90
+ name: string;
91
+ code: string;
81
92
  constructor(request: Request);
82
93
  }
83
94
  /**
84
95
  An error to be thrown when the request is aborted by AbortController.
85
96
  */
86
97
  export declare class AbortError extends RequestError {
98
+ name: string;
99
+ code: string;
87
100
  constructor(request: Request);
88
101
  }
89
102
  export {};
@@ -8,8 +8,9 @@ An error to be thrown when a request fails.
8
8
  Contains a `code` property with error class code, like `ECONNREFUSED`.
9
9
  */
10
10
  export class RequestError extends Error {
11
+ name = 'RequestError';
12
+ code = 'ERR_GOT_REQUEST_ERROR';
11
13
  input;
12
- code;
13
14
  stack;
14
15
  response;
15
16
  request;
@@ -17,8 +18,9 @@ export class RequestError extends Error {
17
18
  constructor(message, error, self) {
18
19
  super(message, { cause: error });
19
20
  Error.captureStackTrace(this, this.constructor);
20
- this.name = 'RequestError';
21
- this.code = error.code ?? 'ERR_GOT_REQUEST_ERROR';
21
+ if (error.code) {
22
+ this.code = error.code;
23
+ }
22
24
  this.input = error.input;
23
25
  if (isRequest(self)) {
24
26
  Object.defineProperty(this, 'request', {
@@ -53,10 +55,10 @@ An error to be thrown when the server redirects you more than ten times.
53
55
  Includes a `response` property.
54
56
  */
55
57
  export class MaxRedirectsError extends RequestError {
58
+ name = 'MaxRedirectsError';
59
+ code = 'ERR_TOO_MANY_REDIRECTS';
56
60
  constructor(request) {
57
61
  super(`Redirected ${request.options.maxRedirects} times. Aborting.`, {}, request);
58
- this.name = 'MaxRedirectsError';
59
- this.code = 'ERR_TOO_MANY_REDIRECTS';
60
62
  }
61
63
  }
62
64
  /**
@@ -66,10 +68,10 @@ Includes a `response` property.
66
68
  // TODO: Change `HTTPError<T = any>` to `HTTPError<T = unknown>` in the next major version to enforce type usage.
67
69
  // eslint-disable-next-line @typescript-eslint/naming-convention
68
70
  export class HTTPError extends RequestError {
71
+ name = 'HTTPError';
72
+ code = 'ERR_NON_2XX_3XX_RESPONSE';
69
73
  constructor(response) {
70
- super(`Response code ${response.statusCode} (${response.statusMessage})`, {}, response.request);
71
- this.name = 'HTTPError';
72
- this.code = 'ERR_NON_2XX_3XX_RESPONSE';
74
+ super(`Request failed with status code ${response.statusCode} (${response.statusMessage}): ${response.request.options.method} ${response.request.options.url.toString()}`, {}, response.request);
73
75
  }
74
76
  }
75
77
  /**
@@ -77,20 +79,24 @@ An error to be thrown when a cache method fails.
77
79
  For example, if the database goes down or there's a filesystem error.
78
80
  */
79
81
  export class CacheError extends RequestError {
82
+ name = 'CacheError';
80
83
  constructor(error, request) {
81
84
  super(error.message, error, request);
82
- this.name = 'CacheError';
83
- this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_CACHE_ACCESS' : this.code;
85
+ if (this.code === 'ERR_GOT_REQUEST_ERROR') {
86
+ this.code = 'ERR_CACHE_ACCESS';
87
+ }
84
88
  }
85
89
  }
86
90
  /**
87
91
  An error to be thrown when the request body is a stream and an error occurs while reading from that stream.
88
92
  */
89
93
  export class UploadError extends RequestError {
94
+ name = 'UploadError';
90
95
  constructor(error, request) {
91
96
  super(error.message, error, request);
92
- this.name = 'UploadError';
93
- this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_UPLOAD' : this.code;
97
+ if (this.code === 'ERR_GOT_REQUEST_ERROR') {
98
+ this.code = 'ERR_UPLOAD';
99
+ }
94
100
  }
95
101
  }
96
102
  /**
@@ -98,11 +104,11 @@ An error to be thrown when the request is aborted due to a timeout.
98
104
  Includes an `event` and `timings` property.
99
105
  */
100
106
  export class TimeoutError extends RequestError {
107
+ name = 'TimeoutError';
101
108
  timings;
102
109
  event;
103
110
  constructor(error, timings, request) {
104
111
  super(error.message, error, request);
105
- this.name = 'TimeoutError';
106
112
  this.event = error.event;
107
113
  this.timings = timings;
108
114
  }
@@ -111,29 +117,31 @@ export class TimeoutError extends RequestError {
111
117
  An error to be thrown when reading from response stream fails.
112
118
  */
113
119
  export class ReadError extends RequestError {
120
+ name = 'ReadError';
114
121
  constructor(error, request) {
115
122
  super(error.message, error, request);
116
- this.name = 'ReadError';
117
- this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_READING_RESPONSE_STREAM' : this.code;
123
+ if (this.code === 'ERR_GOT_REQUEST_ERROR') {
124
+ this.code = 'ERR_READING_RESPONSE_STREAM';
125
+ }
118
126
  }
119
127
  }
120
128
  /**
121
129
  An error which always triggers a new retry when thrown.
122
130
  */
123
131
  export class RetryError extends RequestError {
132
+ name = 'RetryError';
133
+ code = 'ERR_RETRYING';
124
134
  constructor(request) {
125
135
  super('Retrying', {}, request);
126
- this.name = 'RetryError';
127
- this.code = 'ERR_RETRYING';
128
136
  }
129
137
  }
130
138
  /**
131
139
  An error to be thrown when the request is aborted by AbortController.
132
140
  */
133
141
  export class AbortError extends RequestError {
142
+ name = 'AbortError';
143
+ code = 'ERR_ABORTED';
134
144
  constructor(request) {
135
145
  super('This operation was aborted.', {}, request);
136
- this.code = 'ERR_ABORTED';
137
- this.name = 'AbortError';
138
146
  }
139
147
  }
@@ -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,9 @@ 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?;
111
+ private readonly _requestId;
109
112
  private _requestInitialized;
110
113
  constructor(url: UrlType, options?: OptionsType, defaults?: DefaultsType);
111
114
  flush(): Promise<void>;
@@ -118,6 +121,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
118
121
  end?: boolean;
119
122
  }): T;
120
123
  unpipe<T extends NodeJS.WritableStream>(destination: T): this;
124
+ private _checkContentLengthMismatch;
121
125
  private _finalizeBody;
122
126
  private _onResponseBase;
123
127
  private _setRawBody;
@@ -180,5 +184,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
180
184
  */
181
185
  get isFromCache(): boolean | undefined;
182
186
  get reusedSocket(): boolean | undefined;
187
+ /**
188
+ Whether the stream is read-only. Returns `true` when `body`, `json`, or `form` options are provided.
189
+ */
190
+ get isReadonly(): boolean;
183
191
  }
184
192
  export {};