undici 6.19.8 → 7.0.0-alpha.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.
Files changed (115) hide show
  1. package/README.md +5 -9
  2. package/docs/docs/api/Agent.md +0 -3
  3. package/docs/docs/api/Client.md +0 -2
  4. package/docs/docs/api/Dispatcher.md +204 -6
  5. package/docs/docs/api/EnvHttpProxyAgent.md +0 -1
  6. package/docs/docs/api/Fetch.md +1 -0
  7. package/docs/docs/api/Pool.md +0 -1
  8. package/docs/docs/api/RetryHandler.md +1 -1
  9. package/index.js +0 -4
  10. package/lib/api/api-connect.js +3 -1
  11. package/lib/api/api-pipeline.js +3 -4
  12. package/lib/api/api-request.js +29 -46
  13. package/lib/api/api-stream.js +36 -49
  14. package/lib/api/api-upgrade.js +5 -3
  15. package/lib/api/readable.js +71 -27
  16. package/lib/core/connect.js +39 -24
  17. package/lib/core/errors.js +17 -4
  18. package/lib/core/request.js +7 -5
  19. package/lib/core/symbols.js +0 -1
  20. package/lib/core/tree.js +6 -0
  21. package/lib/core/util.js +1 -11
  22. package/lib/dispatcher/agent.js +3 -17
  23. package/lib/dispatcher/balanced-pool.js +5 -8
  24. package/lib/dispatcher/client-h1.js +44 -39
  25. package/lib/dispatcher/client.js +3 -27
  26. package/lib/dispatcher/dispatcher-base.js +2 -34
  27. package/lib/dispatcher/dispatcher.js +3 -24
  28. package/lib/dispatcher/pool.js +3 -6
  29. package/lib/dispatcher/proxy-agent.js +3 -6
  30. package/lib/handler/decorator-handler.js +24 -0
  31. package/lib/handler/redirect-handler.js +9 -0
  32. package/lib/handler/retry-handler.js +22 -3
  33. package/lib/interceptor/dump.js +2 -2
  34. package/lib/interceptor/redirect.js +11 -14
  35. package/lib/interceptor/response-error.js +89 -0
  36. package/lib/llhttp/constants.d.ts +97 -0
  37. package/lib/llhttp/constants.js +412 -192
  38. package/lib/llhttp/constants.js.map +1 -0
  39. package/lib/llhttp/llhttp-wasm.js +11 -1
  40. package/lib/llhttp/llhttp_simd-wasm.js +11 -1
  41. package/lib/llhttp/utils.d.ts +2 -0
  42. package/lib/llhttp/utils.js +9 -9
  43. package/lib/llhttp/utils.js.map +1 -0
  44. package/lib/mock/mock-client.js +2 -2
  45. package/lib/mock/mock-pool.js +2 -2
  46. package/lib/mock/mock-symbols.js +1 -0
  47. package/lib/util/timers.js +324 -44
  48. package/lib/web/cookies/index.js +15 -13
  49. package/lib/web/cookies/parse.js +2 -2
  50. package/lib/web/eventsource/eventsource-stream.js +9 -8
  51. package/lib/web/eventsource/eventsource.js +10 -6
  52. package/lib/web/fetch/body.js +4 -6
  53. package/lib/web/fetch/data-url.js +1 -1
  54. package/lib/web/fetch/formdata-parser.js +1 -2
  55. package/lib/web/fetch/formdata.js +28 -37
  56. package/lib/web/fetch/headers.js +1 -1
  57. package/lib/web/fetch/index.js +7 -8
  58. package/lib/web/fetch/request.js +7 -24
  59. package/lib/web/fetch/response.js +9 -22
  60. package/lib/web/fetch/symbols.js +0 -1
  61. package/lib/web/fetch/util.js +3 -12
  62. package/lib/web/fetch/webidl.js +73 -62
  63. package/lib/web/websocket/connection.js +26 -174
  64. package/lib/web/websocket/constants.js +1 -1
  65. package/lib/web/websocket/frame.js +45 -3
  66. package/lib/web/websocket/receiver.js +28 -26
  67. package/lib/web/websocket/sender.js +18 -13
  68. package/lib/web/websocket/util.js +20 -74
  69. package/lib/web/websocket/websocket.js +294 -70
  70. package/package.json +16 -29
  71. package/scripts/strip-comments.js +3 -1
  72. package/types/agent.d.ts +7 -7
  73. package/types/api.d.ts +24 -24
  74. package/types/balanced-pool.d.ts +11 -11
  75. package/types/client.d.ts +11 -12
  76. package/types/diagnostics-channel.d.ts +10 -10
  77. package/types/dispatcher.d.ts +96 -97
  78. package/types/env-http-proxy-agent.d.ts +2 -2
  79. package/types/errors.d.ts +53 -47
  80. package/types/eventsource.d.ts +0 -2
  81. package/types/fetch.d.ts +8 -8
  82. package/types/formdata.d.ts +7 -7
  83. package/types/global-dispatcher.d.ts +4 -4
  84. package/types/global-origin.d.ts +5 -5
  85. package/types/handlers.d.ts +4 -4
  86. package/types/header.d.ts +157 -1
  87. package/types/index.d.ts +42 -46
  88. package/types/interceptors.d.ts +10 -8
  89. package/types/mock-agent.d.ts +18 -18
  90. package/types/mock-client.d.ts +4 -4
  91. package/types/mock-errors.d.ts +3 -3
  92. package/types/mock-interceptor.d.ts +19 -19
  93. package/types/mock-pool.d.ts +4 -4
  94. package/types/patch.d.ts +0 -42
  95. package/types/pool-stats.d.ts +8 -8
  96. package/types/pool.d.ts +12 -12
  97. package/types/proxy-agent.d.ts +4 -4
  98. package/types/readable.d.ts +14 -9
  99. package/types/retry-agent.d.ts +1 -1
  100. package/types/retry-handler.d.ts +8 -8
  101. package/types/util.d.ts +3 -3
  102. package/types/utility.d.ts +7 -0
  103. package/types/webidl.d.ts +22 -4
  104. package/types/websocket.d.ts +1 -3
  105. package/docs/docs/api/DispatchInterceptor.md +0 -60
  106. package/lib/interceptor/redirect-interceptor.js +0 -21
  107. package/lib/web/fetch/file.js +0 -126
  108. package/lib/web/fileapi/encoding.js +0 -290
  109. package/lib/web/fileapi/filereader.js +0 -344
  110. package/lib/web/fileapi/progressevent.js +0 -78
  111. package/lib/web/fileapi/symbols.js +0 -10
  112. package/lib/web/fileapi/util.js +0 -391
  113. package/lib/web/websocket/symbols.js +0 -12
  114. package/types/file.d.ts +0 -39
  115. package/types/filereader.d.ts +0 -54
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # undici
2
2
 
3
- [![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)
3
+ [![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![neostandard javascript style](https://img.shields.io/badge/neo-standard-7fffff?style=flat\&labelColor=ff80ff)](https://github.com/neostandard/neostandard) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)
4
4
 
5
5
  An HTTP/1.1 client, written from scratch for Node.js.
6
6
 
@@ -84,6 +84,7 @@ The `body` mixins are the most common way to format the request/response body. M
84
84
 
85
85
  - [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
86
86
  - [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
87
+ - [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
87
88
  - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
88
89
  - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
89
90
 
@@ -126,7 +127,6 @@ Arguments:
126
127
  * **options** [`RequestOptions`](./docs/docs/api/Dispatcher.md#parameter-requestoptions)
127
128
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
128
129
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
129
- * **maxRedirections** `Integer` - Default: `0`
130
130
 
131
131
  Returns a promise with the result of the `Dispatcher.request` method.
132
132
 
@@ -142,7 +142,6 @@ Arguments:
142
142
  * **options** [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions)
143
143
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
144
144
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
145
- * **maxRedirections** `Integer` - Default: `0`
146
145
  * **factory** `Dispatcher.stream.factory`
147
146
 
148
147
  Returns a promise with the result of the `Dispatcher.stream` method.
@@ -159,7 +158,6 @@ Arguments:
159
158
  * **options** [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions)
160
159
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
161
160
  * **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
162
- * **maxRedirections** `Integer` - Default: `0`
163
161
  * **handler** `Dispatcher.pipeline.handler`
164
162
 
165
163
  Returns: `stream.Duplex`
@@ -177,7 +175,6 @@ Arguments:
177
175
  * **url** `string | URL | UrlObject`
178
176
  * **options** [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions)
179
177
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
180
- * **maxRedirections** `Integer` - Default: `0`
181
178
  * **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
182
179
 
183
180
  Returns a promise with the result of the `Dispatcher.connect` method.
@@ -262,13 +259,13 @@ await fetch('http://example.com', { method: 'POST', body })
262
259
 
263
260
  #### `request.duplex`
264
261
 
265
- - half
262
+ - `'half'`
266
263
 
267
- In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`, however, fetch requests are currently always full duplex. For more detail refer to the [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex).
264
+ In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`, however, even though the value must be set to `'half'`, it is actually a _full_ duplex. For more detail refer to the [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex).
268
265
 
269
266
  #### `response.body`
270
267
 
271
- Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
268
+ Nodejs has two kinds of streams: [web streams](https://nodejs.org/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
272
269
 
273
270
  ```js
274
271
  import { fetch } from 'undici'
@@ -337,7 +334,6 @@ Arguments:
337
334
  * **url** `string | URL | UrlObject`
338
335
  * **options** [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions)
339
336
  * **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
340
- * **maxRedirections** `Integer` - Default: `0`
341
337
  * **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
342
338
 
343
339
  Returns a promise with the result of the `Dispatcher.upgrade` method.
@@ -19,8 +19,6 @@ Returns: `Agent`
19
19
  Extends: [`PoolOptions`](Pool.md#parameter-pooloptions)
20
20
 
21
21
  * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
22
- * **maxRedirections** `Integer` - Default: `0`. The number of HTTP redirection to follow unless otherwise specified in `DispatchOptions`.
23
- * **interceptors** `{ Agent: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
24
22
 
25
23
  ## Instance Properties
26
24
 
@@ -51,7 +49,6 @@ Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdis
51
49
  Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions)
52
50
 
53
51
  * **origin** `string | URL`
54
- * **maxRedirections** `Integer`.
55
52
 
56
53
  Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
57
54
 
@@ -29,8 +29,6 @@ Returns: `Client`
29
29
  * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
30
30
  * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
31
31
  * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
32
- <!-- TODO: Remove once we drop its support -->
33
- * **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time. **Note: this is deprecated in favor of [Dispatcher#compose](./Dispatcher.md#dispatcher). Support will be droped in next major.**
34
32
  * **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
35
33
  * **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
36
34
  * **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
@@ -201,7 +201,6 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
201
201
  * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
202
202
  * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
203
203
  * **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
204
- * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
205
204
  * **expectContinue** `boolean` (optional) - Default: `false` - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server
206
205
 
207
206
  #### Parameter: `DispatchHandler`
@@ -488,11 +487,13 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
488
487
 
489
488
  `body` contains the following additional [body mixin](https://fetch.spec.whatwg.org/#body-mixin) methods and properties:
490
489
 
491
- - `text()`
492
- - `json()`
493
- - `arrayBuffer()`
494
- - `body`
495
- - `bodyUsed`
490
+ * [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
491
+ * [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
492
+ * [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
493
+ * [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
494
+ * [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
495
+ * `body`
496
+ * `bodyUsed`
496
497
 
497
498
  `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`.
498
499
 
@@ -984,6 +985,203 @@ client.dispatch(
984
985
  );
985
986
  ```
986
987
 
988
+ ##### `Response Error Interceptor`
989
+
990
+ **Introduction**
991
+
992
+ The Response Error Interceptor is designed to handle HTTP response errors efficiently. It intercepts responses and throws detailed errors for responses with status codes indicating failure (4xx, 5xx). This interceptor enhances error handling by providing structured error information, including response headers, data, and status codes.
993
+
994
+ **ResponseError Class**
995
+
996
+ The `ResponseError` class extends the `UndiciError` class and encapsulates detailed error information. It captures the response status code, headers, and data, providing a structured way to handle errors.
997
+
998
+ **Definition**
999
+
1000
+ ```js
1001
+ class ResponseError extends UndiciError {
1002
+ constructor (message, code, { headers, data }) {
1003
+ super(message);
1004
+ this.name = 'ResponseError';
1005
+ this.message = message || 'Response error';
1006
+ this.code = 'UND_ERR_RESPONSE';
1007
+ this.statusCode = code;
1008
+ this.data = data;
1009
+ this.headers = headers;
1010
+ }
1011
+ }
1012
+ ```
1013
+
1014
+ **Interceptor Handler**
1015
+
1016
+ The interceptor's handler class extends `DecoratorHandler` and overrides methods to capture response details and handle errors based on the response status code.
1017
+
1018
+ **Methods**
1019
+
1020
+ - **onConnect**: Initializes response properties.
1021
+ - **onHeaders**: Captures headers and status code. Decodes body if content type is `application/json` or `text/plain`.
1022
+ - **onData**: Appends chunks to the body if status code indicates an error.
1023
+ - **onComplete**: Finalizes error handling, constructs a `ResponseError`, and invokes the `onError` method.
1024
+ - **onError**: Propagates errors to the handler.
1025
+
1026
+ **Definition**
1027
+
1028
+ ```js
1029
+ class Handler extends DecoratorHandler {
1030
+ // Private properties
1031
+ #handler;
1032
+ #statusCode;
1033
+ #contentType;
1034
+ #decoder;
1035
+ #headers;
1036
+ #body;
1037
+
1038
+ constructor (opts, { handler }) {
1039
+ super(handler);
1040
+ this.#handler = handler;
1041
+ }
1042
+
1043
+ onConnect (abort) {
1044
+ this.#statusCode = 0;
1045
+ this.#contentType = null;
1046
+ this.#decoder = null;
1047
+ this.#headers = null;
1048
+ this.#body = '';
1049
+ return this.#handler.onConnect(abort);
1050
+ }
1051
+
1052
+ onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
1053
+ this.#statusCode = statusCode;
1054
+ this.#headers = headers;
1055
+ this.#contentType = headers['content-type'];
1056
+
1057
+ if (this.#statusCode < 400) {
1058
+ return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers);
1059
+ }
1060
+
1061
+ if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
1062
+ this.#decoder = new TextDecoder('utf-8');
1063
+ }
1064
+ }
1065
+
1066
+ onData (chunk) {
1067
+ if (this.#statusCode < 400) {
1068
+ return this.#handler.onData(chunk);
1069
+ }
1070
+ this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '';
1071
+ }
1072
+
1073
+ onComplete (rawTrailers) {
1074
+ if (this.#statusCode >= 400) {
1075
+ this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '';
1076
+ if (this.#contentType === 'application/json') {
1077
+ try {
1078
+ this.#body = JSON.parse(this.#body);
1079
+ } catch {
1080
+ // Do nothing...
1081
+ }
1082
+ }
1083
+
1084
+ let err;
1085
+ const stackTraceLimit = Error.stackTraceLimit;
1086
+ Error.stackTraceLimit = 0;
1087
+ try {
1088
+ err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body);
1089
+ } finally {
1090
+ Error.stackTraceLimit = stackTraceLimit;
1091
+ }
1092
+
1093
+ this.#handler.onError(err);
1094
+ } else {
1095
+ this.#handler.onComplete(rawTrailers);
1096
+ }
1097
+ }
1098
+
1099
+ onError (err) {
1100
+ this.#handler.onError(err);
1101
+ }
1102
+ }
1103
+
1104
+ module.exports = (dispatch) => (opts, handler) => opts.throwOnError
1105
+ ? dispatch(opts, new Handler(opts, { handler }))
1106
+ : dispatch(opts, handler);
1107
+ ```
1108
+
1109
+ **Tests**
1110
+
1111
+ Unit tests ensure the interceptor functions correctly, handling both error and non-error responses appropriately.
1112
+
1113
+ **Example Tests**
1114
+
1115
+ - **No Error if `throwOnError` is False**:
1116
+
1117
+ ```js
1118
+ test('should not error if request is not meant to throw error', async (t) => {
1119
+ const opts = { throwOnError: false };
1120
+ const handler = { onError: () => {}, onData: () => {}, onComplete: () => {} };
1121
+ const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete());
1122
+ assert.doesNotThrow(() => interceptor(opts, handler));
1123
+ });
1124
+ ```
1125
+
1126
+ - **Error if Status Code is in Specified Error Codes**:
1127
+
1128
+ ```js
1129
+ test('should error if request status code is in the specified error codes', async (t) => {
1130
+ const opts = { throwOnError: true, statusCodes: [500] };
1131
+ const response = { statusCode: 500 };
1132
+ let capturedError;
1133
+ const handler = {
1134
+ onError: (err) => { capturedError = err; },
1135
+ onData: () => {},
1136
+ onComplete: () => {}
1137
+ };
1138
+
1139
+ const interceptor = createResponseErrorInterceptor((opts, handler) => {
1140
+ if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1141
+ handler.onError(new Error('Response Error'));
1142
+ } else {
1143
+ handler.onComplete();
1144
+ }
1145
+ });
1146
+
1147
+ interceptor({ ...opts, response }, handler);
1148
+
1149
+ await new Promise(resolve => setImmediate(resolve));
1150
+
1151
+ assert(capturedError, 'Expected error to be captured but it was not.');
1152
+ assert.strictEqual(capturedError.message, 'Response Error');
1153
+ assert.strictEqual(response.statusCode, 500);
1154
+ });
1155
+ ```
1156
+
1157
+ - **No Error if Status Code is Not in Specified Error Codes**:
1158
+
1159
+ ```js
1160
+ test('should not error if request status code is not in the specified error codes', async (t) => {
1161
+ const opts = { throwOnError: true, statusCodes: [500] };
1162
+ const response = { statusCode: 404 };
1163
+ const handler = {
1164
+ onError: () => {},
1165
+ onData: () => {},
1166
+ onComplete: () => {}
1167
+ };
1168
+
1169
+ const interceptor = createResponseErrorInterceptor((opts, handler) => {
1170
+ if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1171
+ handler.onError(new Error('Response Error'));
1172
+ } else {
1173
+ handler.onComplete();
1174
+ }
1175
+ });
1176
+
1177
+ assert.doesNotThrow(() => interceptor({ ...opts, response }, handler));
1178
+ });
1179
+ ```
1180
+
1181
+ **Conclusion**
1182
+
1183
+ The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
1184
+
987
1185
  ## Instance Events
988
1186
 
989
1187
  ### Event: `'connect'`
@@ -133,7 +133,6 @@ Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdis
133
133
  Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions)
134
134
 
135
135
  * **origin** `string | URL`
136
- * **maxRedirections** `Integer`.
137
136
 
138
137
  Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise).
139
138
 
@@ -28,6 +28,7 @@ This API is implemented as per the standard, you can find documentation on [MDN]
28
28
 
29
29
  - [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
30
30
  - [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
31
+ - [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
31
32
  - [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
32
33
  - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
33
34
  - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
@@ -19,7 +19,6 @@ Extends: [`ClientOptions`](Client.md#parameter-clientoptions)
19
19
 
20
20
  * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
21
21
  * **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
22
- * **interceptors** `{ Pool: DispatchInterceptor[] } }` - Default: `{ Pool: [] }` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching).
23
22
 
24
23
  ## Instance Properties
25
24
 
@@ -19,7 +19,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
19
19
 
20
20
  #### `RetryOptions`
21
21
 
22
- - **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
22
+ - **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => number | null` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
23
23
  - **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
24
24
  - **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
25
25
  - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
package/index.js CHANGED
@@ -21,7 +21,6 @@ const RetryHandler = require('./lib/handler/retry-handler')
21
21
  const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
22
22
  const DecoratorHandler = require('./lib/handler/decorator-handler')
23
23
  const RedirectHandler = require('./lib/handler/redirect-handler')
24
- const createRedirectInterceptor = require('./lib/interceptor/redirect-interceptor')
25
24
 
26
25
  Object.assign(Dispatcher.prototype, api)
27
26
 
@@ -37,7 +36,6 @@ module.exports.RetryHandler = RetryHandler
37
36
 
38
37
  module.exports.DecoratorHandler = DecoratorHandler
39
38
  module.exports.RedirectHandler = RedirectHandler
40
- module.exports.createRedirectInterceptor = createRedirectInterceptor
41
39
  module.exports.interceptors = {
42
40
  redirect: require('./lib/interceptor/redirect'),
43
41
  retry: require('./lib/interceptor/retry'),
@@ -119,8 +117,6 @@ module.exports.Headers = require('./lib/web/fetch/headers').Headers
119
117
  module.exports.Response = require('./lib/web/fetch/response').Response
120
118
  module.exports.Request = require('./lib/web/fetch/request').Request
121
119
  module.exports.FormData = require('./lib/web/fetch/formdata').FormData
122
- module.exports.File = globalThis.File ?? require('node:buffer').File
123
- module.exports.FileReader = require('./lib/web/fileapi/filereader').FileReader
124
120
 
125
121
  const { setGlobalOrigin, getGlobalOrigin } = require('./lib/web/fetch/global')
126
122
 
@@ -95,7 +95,9 @@ function connect (opts, callback) {
95
95
 
96
96
  try {
97
97
  const connectHandler = new ConnectHandler(opts, callback)
98
- this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler)
98
+ const connectOptions = { ...opts, method: 'CONNECT' }
99
+
100
+ this.dispatch(connectOptions, connectHandler)
99
101
  } catch (err) {
100
102
  if (typeof callback !== 'function') {
101
103
  throw err
@@ -5,15 +5,15 @@ const {
5
5
  Duplex,
6
6
  PassThrough
7
7
  } = require('node:stream')
8
+ const assert = require('node:assert')
9
+ const { AsyncResource } = require('node:async_hooks')
8
10
  const {
9
11
  InvalidArgumentError,
10
12
  InvalidReturnValueError,
11
13
  RequestAbortedError
12
14
  } = require('../core/errors')
13
15
  const util = require('../core/util')
14
- const { AsyncResource } = require('node:async_hooks')
15
16
  const { addSignal, removeSignal } = require('./abort-signal')
16
- const assert = require('node:assert')
17
17
 
18
18
  const kResume = Symbol('resume')
19
19
 
@@ -145,7 +145,7 @@ class PipelineHandler extends AsyncResource {
145
145
  }
146
146
 
147
147
  onConnect (abort, context) {
148
- const { ret, res } = this
148
+ const { res } = this
149
149
 
150
150
  if (this.reason) {
151
151
  abort(this.reason)
@@ -153,7 +153,6 @@ class PipelineHandler extends AsyncResource {
153
153
  }
154
154
 
155
155
  assert(!res, 'pipeline cannot be retried')
156
- assert(!ret.destroyed)
157
156
 
158
157
  this.abort = abort
159
158
  this.context = context
@@ -1,11 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
+ const { AsyncResource } = require('node:async_hooks')
4
5
  const { Readable } = require('./readable')
5
6
  const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
6
7
  const util = require('../core/util')
7
- const { getResolveErrorBodyCallback } = require('./util')
8
- const { AsyncResource } = require('node:async_hooks')
9
8
 
10
9
  class RequestHandler extends AsyncResource {
11
10
  constructor (opts, callback) {
@@ -13,7 +12,7 @@ class RequestHandler extends AsyncResource {
13
12
  throw new InvalidArgumentError('invalid opts')
14
13
  }
15
14
 
16
- const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts
15
+ const { signal, method, opaque, body, onInfo, responseHeaders, highWaterMark } = opts
17
16
 
18
17
  try {
19
18
  if (typeof callback !== 'function') {
@@ -54,38 +53,22 @@ class RequestHandler extends AsyncResource {
54
53
  this.trailers = {}
55
54
  this.context = null
56
55
  this.onInfo = onInfo || null
57
- this.throwOnError = throwOnError
58
56
  this.highWaterMark = highWaterMark
59
- this.signal = signal
60
57
  this.reason = null
61
58
  this.removeAbortListener = null
62
59
 
63
- if (util.isStream(body)) {
64
- body.on('error', (err) => {
65
- this.onError(err)
60
+ if (signal?.aborted) {
61
+ this.reason = signal.reason ?? new RequestAbortedError()
62
+ } else if (signal) {
63
+ this.removeAbortListener = util.addAbortListener(signal, () => {
64
+ this.reason = signal.reason ?? new RequestAbortedError()
65
+ if (this.res) {
66
+ util.destroy(this.res, this.reason)
67
+ } else if (this.abort) {
68
+ this.abort(this.reason)
69
+ }
66
70
  })
67
71
  }
68
-
69
- if (this.signal) {
70
- if (this.signal.aborted) {
71
- this.reason = this.signal.reason ?? new RequestAbortedError()
72
- } else {
73
- this.removeAbortListener = util.addAbortListener(this.signal, () => {
74
- this.reason = this.signal.reason ?? new RequestAbortedError()
75
- if (this.res) {
76
- util.destroy(this.res, this.reason)
77
- } else if (this.abort) {
78
- this.abort(this.reason)
79
- }
80
-
81
- if (this.removeAbortListener) {
82
- this.res?.off('close', this.removeAbortListener)
83
- this.removeAbortListener()
84
- this.removeAbortListener = null
85
- }
86
- })
87
- }
88
- }
89
72
  }
90
73
 
91
74
  onConnect (abort, context) {
@@ -127,25 +110,20 @@ class RequestHandler extends AsyncResource {
127
110
 
128
111
  if (this.removeAbortListener) {
129
112
  res.on('close', this.removeAbortListener)
113
+ this.removeAbortListener = null
130
114
  }
131
115
 
132
116
  this.callback = null
133
117
  this.res = res
134
118
  if (callback !== null) {
135
- if (this.throwOnError && statusCode >= 400) {
136
- this.runInAsyncScope(getResolveErrorBodyCallback, null,
137
- { callback, body: res, contentType, statusCode, statusMessage, headers }
138
- )
139
- } else {
140
- this.runInAsyncScope(callback, null, null, {
141
- statusCode,
142
- headers,
143
- trailers: this.trailers,
144
- opaque,
145
- body: res,
146
- context
147
- })
148
- }
119
+ this.runInAsyncScope(callback, null, null, {
120
+ statusCode,
121
+ headers,
122
+ trailers: this.trailers,
123
+ opaque,
124
+ body: res,
125
+ context
126
+ })
149
127
  }
150
128
  }
151
129
 
@@ -179,11 +157,14 @@ class RequestHandler extends AsyncResource {
179
157
 
180
158
  if (body) {
181
159
  this.body = null
182
- util.destroy(body, err)
160
+
161
+ if (util.isStream(body)) {
162
+ body.on('error', util.nop)
163
+ util.destroy(body, err)
164
+ }
183
165
  }
184
166
 
185
167
  if (this.removeAbortListener) {
186
- res?.off('close', this.removeAbortListener)
187
168
  this.removeAbortListener()
188
169
  this.removeAbortListener = null
189
170
  }
@@ -200,7 +181,9 @@ function request (opts, callback) {
200
181
  }
201
182
 
202
183
  try {
203
- this.dispatch(opts, new RequestHandler(opts, callback))
184
+ const handler = new RequestHandler(opts, callback)
185
+
186
+ this.dispatch(opts, handler)
204
187
  } catch (err) {
205
188
  if (typeof callback !== 'function') {
206
189
  throw err