undici 7.0.0-alpha.7 → 7.0.0-alpha.9

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.
@@ -207,7 +207,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
207
207
 
208
208
  * **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
209
209
  * **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string[]>, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
210
- * **onResponseStart** `(controller: DispatchController, statusCode: number, statusMessage?: string, headers: Record<string, string | string []>) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
210
+ * **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string []>, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
211
211
  * **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
212
212
  * **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
213
213
  * **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
@@ -1054,203 +1054,27 @@ const response = await client.request({
1054
1054
  })
1055
1055
  ```
1056
1056
 
1057
- ##### `Response Error Interceptor`
1057
+ ##### `responseError`
1058
1058
 
1059
- **Introduction**
1059
+ The `responseError` interceptor throws an error for responses with status code errors (>= 400).
1060
1060
 
1061
- 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.
1062
-
1063
- **ResponseError Class**
1064
-
1065
- 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.
1066
-
1067
- **Definition**
1068
-
1069
- ```js
1070
- class ResponseError extends UndiciError {
1071
- constructor (message, code, { headers, data }) {
1072
- super(message);
1073
- this.name = 'ResponseError';
1074
- this.message = message || 'Response error';
1075
- this.code = 'UND_ERR_RESPONSE';
1076
- this.statusCode = code;
1077
- this.data = data;
1078
- this.headers = headers;
1079
- }
1080
- }
1081
- ```
1082
-
1083
- **Interceptor Handler**
1084
-
1085
- The interceptor's handler class extends `DecoratorHandler` and overrides methods to capture response details and handle errors based on the response status code.
1086
-
1087
- **Methods**
1088
-
1089
- - **onConnect**: Initializes response properties.
1090
- - **onHeaders**: Captures headers and status code. Decodes body if content type is `application/json` or `text/plain`.
1091
- - **onData**: Appends chunks to the body if status code indicates an error.
1092
- - **onComplete**: Finalizes error handling, constructs a `ResponseError`, and invokes the `onError` method.
1093
- - **onError**: Propagates errors to the handler.
1094
-
1095
- **Definition**
1096
-
1097
- ```js
1098
- class Handler extends DecoratorHandler {
1099
- // Private properties
1100
- #handler;
1101
- #statusCode;
1102
- #contentType;
1103
- #decoder;
1104
- #headers;
1105
- #body;
1106
-
1107
- constructor (opts, { handler }) {
1108
- super(handler);
1109
- this.#handler = handler;
1110
- }
1111
-
1112
- onConnect (abort) {
1113
- this.#statusCode = 0;
1114
- this.#contentType = null;
1115
- this.#decoder = null;
1116
- this.#headers = null;
1117
- this.#body = '';
1118
- return this.#handler.onConnect(abort);
1119
- }
1120
-
1121
- onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
1122
- this.#statusCode = statusCode;
1123
- this.#headers = headers;
1124
- this.#contentType = headers['content-type'];
1125
-
1126
- if (this.#statusCode < 400) {
1127
- return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers);
1128
- }
1129
-
1130
- if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
1131
- this.#decoder = new TextDecoder('utf-8');
1132
- }
1133
- }
1134
-
1135
- onData (chunk) {
1136
- if (this.#statusCode < 400) {
1137
- return this.#handler.onData(chunk);
1138
- }
1139
- this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '';
1140
- }
1141
-
1142
- onComplete (rawTrailers) {
1143
- if (this.#statusCode >= 400) {
1144
- this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '';
1145
- if (this.#contentType === 'application/json') {
1146
- try {
1147
- this.#body = JSON.parse(this.#body);
1148
- } catch {
1149
- // Do nothing...
1150
- }
1151
- }
1152
-
1153
- let err;
1154
- const stackTraceLimit = Error.stackTraceLimit;
1155
- Error.stackTraceLimit = 0;
1156
- try {
1157
- err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body);
1158
- } finally {
1159
- Error.stackTraceLimit = stackTraceLimit;
1160
- }
1161
-
1162
- this.#handler.onError(err);
1163
- } else {
1164
- this.#handler.onComplete(rawTrailers);
1165
- }
1166
- }
1167
-
1168
- onError (err) {
1169
- this.#handler.onError(err);
1170
- }
1171
- }
1172
-
1173
- module.exports = (dispatch) => (opts, handler) => opts.throwOnError
1174
- ? dispatch(opts, new Handler(opts, { handler }))
1175
- : dispatch(opts, handler);
1176
- ```
1177
-
1178
- **Tests**
1179
-
1180
- Unit tests ensure the interceptor functions correctly, handling both error and non-error responses appropriately.
1181
-
1182
- **Example Tests**
1183
-
1184
- - **No Error if `throwOnError` is False**:
1061
+ **Example**
1185
1062
 
1186
1063
  ```js
1187
- test('should not error if request is not meant to throw error', async (t) => {
1188
- const opts = { throwOnError: false };
1189
- const handler = { onError: () => {}, onData: () => {}, onComplete: () => {} };
1190
- const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete());
1191
- assert.doesNotThrow(() => interceptor(opts, handler));
1192
- });
1193
- ```
1194
-
1195
- - **Error if Status Code is in Specified Error Codes**:
1196
-
1197
- ```js
1198
- test('should error if request status code is in the specified error codes', async (t) => {
1199
- const opts = { throwOnError: true, statusCodes: [500] };
1200
- const response = { statusCode: 500 };
1201
- let capturedError;
1202
- const handler = {
1203
- onError: (err) => { capturedError = err; },
1204
- onData: () => {},
1205
- onComplete: () => {}
1206
- };
1207
-
1208
- const interceptor = createResponseErrorInterceptor((opts, handler) => {
1209
- if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1210
- handler.onError(new Error('Response Error'));
1211
- } else {
1212
- handler.onComplete();
1213
- }
1214
- });
1215
-
1216
- interceptor({ ...opts, response }, handler);
1217
-
1218
- await new Promise(resolve => setImmediate(resolve));
1219
-
1220
- assert(capturedError, 'Expected error to be captured but it was not.');
1221
- assert.strictEqual(capturedError.message, 'Response Error');
1222
- assert.strictEqual(response.statusCode, 500);
1223
- });
1224
- ```
1225
-
1226
- - **No Error if Status Code is Not in Specified Error Codes**:
1064
+ const { Client, interceptors } = require("undici");
1065
+ const { responseError } = interceptors;
1227
1066
 
1228
- ```js
1229
- test('should not error if request status code is not in the specified error codes', async (t) => {
1230
- const opts = { throwOnError: true, statusCodes: [500] };
1231
- const response = { statusCode: 404 };
1232
- const handler = {
1233
- onError: () => {},
1234
- onData: () => {},
1235
- onComplete: () => {}
1236
- };
1237
-
1238
- const interceptor = createResponseErrorInterceptor((opts, handler) => {
1239
- if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1240
- handler.onError(new Error('Response Error'));
1241
- } else {
1242
- handler.onComplete();
1243
- }
1244
- });
1067
+ const client = new Client("http://example.com").compose(
1068
+ responseError()
1069
+ );
1245
1070
 
1246
- assert.doesNotThrow(() => interceptor({ ...opts, response }, handler));
1071
+ // Will throw a ResponseError for status codes >= 400
1072
+ await client.request({
1073
+ method: "GET",
1074
+ path: "/"
1247
1075
  });
1248
1076
  ```
1249
1077
 
1250
- **Conclusion**
1251
-
1252
- 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.
1253
-
1254
1078
  ##### `Cache Interceptor`
1255
1079
 
1256
1080
  The `cache` interceptor implements client-side response caching as described in
@@ -1260,6 +1084,8 @@ The `cache` interceptor implements client-side response caching as described in
1260
1084
 
1261
1085
  - `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore).
1262
1086
  - `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
1087
+ - `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration. If this isn't present, responses without explicit expiration will not be cached. Default `undefined`.
1088
+ - `type` - The type of cache for Undici to act as. Can be `shared` or `private`. Default `shared`.
1263
1089
 
1264
1090
  ## Instance Events
1265
1091
 
package/index.js CHANGED
@@ -38,6 +38,7 @@ module.exports.DecoratorHandler = DecoratorHandler
38
38
  module.exports.RedirectHandler = RedirectHandler
39
39
  module.exports.interceptors = {
40
40
  redirect: require('./lib/interceptor/redirect'),
41
+ responseError: require('./lib/interceptor/response-error'),
41
42
  retry: require('./lib/interceptor/retry'),
42
43
  dump: require('./lib/interceptor/dump'),
43
44
  dns: require('./lib/interceptor/dns'),
@@ -89,7 +89,9 @@ class MemoryCacheStore {
89
89
  statusCode: entry.statusCode,
90
90
  headers: entry.headers,
91
91
  body: entry.body,
92
+ vary: entry.vary ? entry.vary : undefined,
92
93
  etag: entry.etag,
94
+ cacheControlDirectives: entry.cacheControlDirectives,
93
95
  cachedAt: entry.cachedAt,
94
96
  staleAt: entry.staleAt,
95
97
  deleteAt: entry.deleteAt
@@ -4,7 +4,10 @@ const { DatabaseSync } = require('node:sqlite')
4
4
  const { Writable } = require('stream')
5
5
  const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
6
6
 
7
- const VERSION = 2
7
+ const VERSION = 3
8
+
9
+ // 2gb
10
+ const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
8
11
 
9
12
  /**
10
13
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
@@ -17,8 +20,8 @@ const VERSION = 2
17
20
  * body: string
18
21
  * } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
19
22
  */
20
- class SqliteCacheStore {
21
- #maxEntrySize = Infinity
23
+ module.exports = class SqliteCacheStore {
24
+ #maxEntrySize = MAX_ENTRY_SIZE
22
25
  #maxCount = Infinity
23
26
 
24
27
  /**
@@ -78,6 +81,11 @@ class SqliteCacheStore {
78
81
  ) {
79
82
  throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
80
83
  }
84
+
85
+ if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
86
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
87
+ }
88
+
81
89
  this.#maxEntrySize = opts.maxEntrySize
82
90
  }
83
91
 
@@ -103,11 +111,12 @@ class SqliteCacheStore {
103
111
  method TEXT NOT NULL,
104
112
 
105
113
  -- Data returned to the interceptor
106
- body TEXT NULL,
114
+ body BUF NULL,
107
115
  deleteAt INTEGER NOT NULL,
108
116
  statusCode INTEGER NOT NULL,
109
117
  statusMessage TEXT NOT NULL,
110
118
  headers TEXT NULL,
119
+ cacheControlDirectives TEXT NULL,
111
120
  etag TEXT NULL,
112
121
  vary TEXT NULL,
113
122
  cachedAt INTEGER NOT NULL,
@@ -128,6 +137,7 @@ class SqliteCacheStore {
128
137
  statusMessage,
129
138
  headers,
130
139
  etag,
140
+ cacheControlDirectives,
131
141
  vary,
132
142
  cachedAt,
133
143
  staleAt
@@ -147,6 +157,7 @@ class SqliteCacheStore {
147
157
  statusMessage = ?,
148
158
  headers = ?,
149
159
  etag = ?,
160
+ cacheControlDirectives = ?,
150
161
  cachedAt = ?,
151
162
  staleAt = ?,
152
163
  deleteAt = ?
@@ -164,11 +175,12 @@ class SqliteCacheStore {
164
175
  statusMessage,
165
176
  headers,
166
177
  etag,
178
+ cacheControlDirectives,
167
179
  vary,
168
180
  cachedAt,
169
181
  staleAt,
170
182
  deleteAt
171
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
183
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
172
184
  `)
173
185
 
174
186
  this.#deleteByUrlQuery = this.#db.prepare(
@@ -218,11 +230,15 @@ class SqliteCacheStore {
218
230
  * @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
219
231
  */
220
232
  const result = {
221
- body: value.body ? parseBufferArray(JSON.parse(value.body)) : null,
233
+ body: Buffer.from(value.body),
222
234
  statusCode: value.statusCode,
223
235
  statusMessage: value.statusMessage,
224
236
  headers: value.headers ? JSON.parse(value.headers) : undefined,
225
237
  etag: value.etag ? value.etag : undefined,
238
+ vary: value.vary ?? undefined,
239
+ cacheControlDirectives: value.cacheControlDirectives
240
+ ? JSON.parse(value.cacheControlDirectives)
241
+ : undefined,
226
242
  cachedAt: value.cachedAt,
227
243
  staleAt: value.staleAt,
228
244
  deleteAt: value.deleteAt
@@ -269,12 +285,13 @@ class SqliteCacheStore {
269
285
  if (existingValue) {
270
286
  // Updating an existing response, let's overwrite it
271
287
  store.#updateValueQuery.run(
272
- JSON.stringify(stringifyBufferArray(body)),
288
+ Buffer.concat(body),
273
289
  value.deleteAt,
274
290
  value.statusCode,
275
291
  value.statusMessage,
276
292
  value.headers ? JSON.stringify(value.headers) : null,
277
- value.etag,
293
+ value.etag ? value.etag : null,
294
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
278
295
  value.cachedAt,
279
296
  value.staleAt,
280
297
  value.deleteAt,
@@ -286,12 +303,13 @@ class SqliteCacheStore {
286
303
  store.#insertValueQuery.run(
287
304
  url,
288
305
  key.method,
289
- JSON.stringify(stringifyBufferArray(body)),
306
+ Buffer.concat(body),
290
307
  value.deleteAt,
291
308
  value.statusCode,
292
309
  value.statusMessage,
293
310
  value.headers ? JSON.stringify(value.headers) : null,
294
311
  value.etag ? value.etag : null,
312
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
295
313
  value.vary ? JSON.stringify(value.vary) : null,
296
314
  value.cachedAt,
297
315
  value.staleAt,
@@ -316,7 +334,7 @@ class SqliteCacheStore {
316
334
  }
317
335
 
318
336
  #prune () {
319
- if (this.#size <= this.#maxCount) {
337
+ if (this.size <= this.#maxCount) {
320
338
  return 0
321
339
  }
322
340
 
@@ -341,7 +359,7 @@ class SqliteCacheStore {
341
359
  * Counts the number of rows in the cache
342
360
  * @returns {Number}
343
361
  */
344
- get #size () {
362
+ get size () {
345
363
  const { total } = this.#countEntriesQuery.get()
346
364
  return total
347
365
  }
@@ -385,10 +403,10 @@ class SqliteCacheStore {
385
403
  return undefined
386
404
  }
387
405
 
388
- const vary = JSON.parse(value.vary)
406
+ value.vary = JSON.parse(value.vary)
389
407
 
390
- for (const header in vary) {
391
- if (headerValueEquals(headers[header], vary[header])) {
408
+ for (const header in value.vary) {
409
+ if (!headerValueEquals(headers[header], value.vary[header])) {
392
410
  matches = false
393
411
  break
394
412
  }
@@ -426,32 +444,3 @@ function headerValueEquals (lhs, rhs) {
426
444
 
427
445
  return lhs === rhs
428
446
  }
429
-
430
- /**
431
- * @param {Buffer[]} buffers
432
- * @returns {string[]}
433
- */
434
- function stringifyBufferArray (buffers) {
435
- const output = new Array(buffers.length)
436
- for (let i = 0; i < buffers.length; i++) {
437
- output[i] = buffers[i].toString()
438
- }
439
-
440
- return output
441
- }
442
-
443
- /**
444
- * @param {string[]} strings
445
- * @returns {Buffer[]}
446
- */
447
- function parseBufferArray (strings) {
448
- const output = new Array(strings.length)
449
-
450
- for (let i = 0; i < strings.length; i++) {
451
- output[i] = Buffer.from(strings[i])
452
- }
453
-
454
- return output
455
- }
456
-
457
- module.exports = SqliteCacheStore
@@ -196,13 +196,13 @@ class RequestRetryError extends UndiciError {
196
196
  }
197
197
 
198
198
  class ResponseError extends UndiciError {
199
- constructor (message, code, { headers, data }) {
199
+ constructor (message, code, { headers, body }) {
200
200
  super(message)
201
201
  this.name = 'ResponseError'
202
202
  this.message = message || 'Response error'
203
203
  this.code = 'UND_ERR_RESPONSE'
204
204
  this.statusCode = code
205
- this.data = data
205
+ this.body = body
206
206
  this.headers = headers
207
207
  }
208
208
  }
@@ -130,6 +130,8 @@ class DispatcherBase extends Dispatcher {
130
130
  throw new InvalidArgumentError('handler must be an object')
131
131
  }
132
132
 
133
+ handler = UnwrapHandler.unwrap(handler)
134
+
133
135
  try {
134
136
  if (!opts || typeof opts !== 'object') {
135
137
  throw new InvalidArgumentError('opts must be an object.')
@@ -143,10 +145,10 @@ class DispatcherBase extends Dispatcher {
143
145
  throw new ClientClosedError()
144
146
  }
145
147
 
146
- return this[kDispatch](opts, UnwrapHandler.unwrap(handler))
148
+ return this[kDispatch](opts, handler)
147
149
  } catch (err) {
148
150
  if (typeof handler.onError !== 'function') {
149
- throw new InvalidArgumentError('invalid onError method')
151
+ throw err
150
152
  }
151
153
 
152
154
  handler.onError(err)