@nxtedition/nxt-undici 6.2.11 → 6.2.13

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,6 +1,6 @@
1
1
  import undici from '@nxtedition/undici'
2
2
  import { DecoratorHandler, parseCacheControl, parseContentRange } from '../utils.js'
3
- import { SqliteCacheStore, assertCacheKey } from '../sqlite-cache-store.js'
3
+ import { SqliteCacheStore } from '../sqlite-cache-store.js'
4
4
 
5
5
  const DEFAULT_STORE = new SqliteCacheStore({ location: ':memory:' })
6
6
  const DEFAULT_MAX_ENTRY_SIZE = 128 * 1024
@@ -13,8 +13,6 @@ class CacheHandler extends DecoratorHandler {
13
13
  #maxEntrySize
14
14
 
15
15
  constructor(key, { store, handler, maxEntrySize }) {
16
- assertCacheKey(key)
17
-
18
16
  super(handler)
19
17
 
20
18
  this.#key = key
@@ -211,7 +209,7 @@ export default () => (dispatch) => (opts, handler) => {
211
209
  )
212
210
  }
213
211
 
214
- const { statusCode, headers, body } = entry ?? { statusCode: 504, headers: {} }
212
+ const { statusCode, headers, trailers, body } = entry ?? { statusCode: 504 }
215
213
 
216
214
  let aborted = false
217
215
  const abort = (reason) => {
@@ -230,7 +228,7 @@ export default () => (dispatch) => (opts, handler) => {
230
228
  return
231
229
  }
232
230
 
233
- handler.onHeaders(statusCode, headers, NOOP)
231
+ handler.onHeaders(statusCode, headers ?? {}, NOOP)
234
232
  if (aborted) {
235
233
  return
236
234
  }
@@ -242,7 +240,7 @@ export default () => (dispatch) => (opts, handler) => {
242
240
  }
243
241
  }
244
242
 
245
- handler.onComplete({})
243
+ handler.onComplete(trailers ?? {})
246
244
  } catch (err) {
247
245
  abort(err)
248
246
  }
@@ -35,15 +35,18 @@ export default () => (dispatch) => {
35
35
  function resolve(hostname, { logger }) {
36
36
  let promise = promises.get(hostname)
37
37
  if (!promise) {
38
- logger?.debug({ dns: { hostname } }, 'lookup started')
39
- promise = new Promise((resolve) =>
38
+ promise = new Promise((resolve) => {
39
+ logger?.debug({ dns: { hostname } }, 'lookup started')
40
40
  dns.resolve4(hostname, { ttl: true }, (err, records) => {
41
41
  promises.delete(hostname)
42
42
 
43
43
  if (err) {
44
44
  logger?.error({ err, dns: { hostname } }, 'lookup failed')
45
+
45
46
  resolve([err, null])
46
47
  } else {
48
+ logger?.debug({ dns: { hostname, records } }, 'lookup completed')
49
+
47
50
  const now = getFastNow()
48
51
  const val = records.map(({ address, ttl }) => ({
49
52
  address,
@@ -53,14 +56,12 @@ export default () => (dispatch) => {
53
56
  counter: 0,
54
57
  }))
55
58
 
56
- logger?.debug({ err, dns: { hostname, records } }, 'lookup completed')
57
-
58
59
  cache.set(hostname, val)
59
60
 
60
61
  resolve([null, val])
61
62
  }
62
- }),
63
- )
63
+ })
64
+ })
64
65
  promises.set(hostname, promise)
65
66
  }
66
67
  return promise
@@ -1,11 +1,10 @@
1
- import createHttpError from 'http-errors'
2
- import { DecoratorHandler } from '../utils.js'
1
+ import { DecoratorHandler, decorateError } from '../utils.js'
3
2
 
4
3
  class Handler extends DecoratorHandler {
5
4
  #statusCode = 0
6
- #contentType
7
5
  #decoder
8
6
  #headers
7
+ #trailers
9
8
  #body = ''
10
9
  #opts
11
10
 
@@ -15,13 +14,8 @@ class Handler extends DecoratorHandler {
15
14
  this.#opts = opts
16
15
  }
17
16
 
18
- #checkContentType(contentType) {
19
- return (this.#contentType ?? '').indexOf(contentType) === 0
20
- }
21
-
22
17
  onConnect(abort) {
23
18
  this.#statusCode = 0
24
- this.#contentType = null
25
19
  this.#decoder = null
26
20
  this.#headers = null
27
21
  this.#body = ''
@@ -32,14 +26,17 @@ class Handler extends DecoratorHandler {
32
26
  onHeaders(statusCode, headers, resume) {
33
27
  this.#statusCode = statusCode
34
28
  this.#headers = headers
35
- this.#contentType = headers['content-type']
36
29
 
37
30
  if (this.#statusCode < 400) {
38
31
  return super.onHeaders(statusCode, headers, resume)
39
32
  }
40
33
 
41
- if (this.#checkContentType('application/json') || this.#checkContentType('text/plain')) {
34
+ if (
35
+ this.#headers['content-type']?.startsWith('application/json') ||
36
+ this.#headers['content-type']?.startsWith('text/plain')
37
+ ) {
42
38
  this.#decoder = new TextDecoder('utf-8')
39
+ this.#body = ''
43
40
  }
44
41
  }
45
42
 
@@ -52,72 +49,33 @@ class Handler extends DecoratorHandler {
52
49
  }
53
50
 
54
51
  onComplete(trailers) {
55
- if (this.#statusCode >= 400) {
56
- this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
57
-
58
- if (this.#checkContentType('application/json')) {
59
- try {
60
- this.#body = JSON.parse(this.#body)
61
- } catch {
62
- // Do nothing...
63
- }
64
- }
65
-
66
- let err
67
- const stackTraceLimit = Error.stackTraceLimit
68
- Error.stackTraceLimit = 0
69
- try {
70
- err = createHttpError(this.#statusCode)
71
- } finally {
72
- Error.stackTraceLimit = stackTraceLimit
73
- }
74
-
75
- super.onError(this.#decorateError(err))
76
- } else {
77
- super.onComplete(trailers)
52
+ this.#trailers = trailers
53
+
54
+ if (this.#statusCode < 400) {
55
+ return super.onComplete(trailers)
78
56
  }
79
- }
80
57
 
81
- onError(err) {
82
- super.onError(this.#decorateError(err))
58
+ this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
59
+
60
+ super.onError(
61
+ decorateError(null, this.#opts, {
62
+ statusCode: this.#statusCode,
63
+ headers: this.#headers,
64
+ trailers: this.#trailers,
65
+ body: this.#body,
66
+ }),
67
+ )
83
68
  }
84
69
 
85
- #decorateError(err) {
86
- try {
87
- err.url ??= this.#opts.origin ? new URL(this.#opts.path, this.#opts.origin).href : null
88
-
89
- err.req = {
90
- method: this.#opts?.method,
91
- headers: this.#opts?.headers,
92
- body:
93
- // TODO (fix): JSON.stringify POJO
94
- typeof this.#opts?.body !== 'string' || this.#opts.body.length > 1024
95
- ? undefined
96
- : this.#opts.body,
97
- }
98
-
99
- err.res = {
70
+ onError(err) {
71
+ super.onError(
72
+ decorateError(err, this.#opts, {
73
+ statusCode: this.#statusCode,
100
74
  headers: this.#headers,
101
- // TODO (fix): JSON.stringify POJO
102
- body: typeof this.#body !== 'string' || this.#body.length < 1024 ? undefined : this.#body,
103
- }
104
-
105
- if (this.#body) {
106
- if (this.#body.reason != null) {
107
- err.reason ??= this.#body.reason
108
- }
109
- if (this.#body.code != null) {
110
- err.code ??= this.#body.code
111
- }
112
- if (this.#body.error != null) {
113
- err.error ??= this.#body.error
114
- }
115
- }
116
-
117
- return err
118
- } catch (er) {
119
- return new AggregateError([er, err])
120
- }
75
+ trailers: this.#trailers,
76
+ body: null,
77
+ }),
78
+ )
121
79
  }
122
80
  }
123
81
 
@@ -1,7 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
  import tp from 'node:timers/promises'
3
- import { DecoratorHandler, isDisturbed, parseRangeHeader } from '../utils.js'
4
- import createHttpError from 'http-errors'
3
+ import { DecoratorHandler, isDisturbed, decorateError, parseRangeHeader } from '../utils.js'
5
4
 
6
5
  function noop() {}
7
6
 
@@ -15,8 +14,10 @@ class Handler extends DecoratorHandler {
15
14
  #errorSent = false
16
15
 
17
16
  #statusCode = 0
18
- #headers = null
19
- #trailers = null
17
+ #headers
18
+ #trailers
19
+ #body = ''
20
+ #decoder
20
21
 
21
22
  #abort
22
23
  #aborted = false
@@ -48,6 +49,8 @@ class Handler extends DecoratorHandler {
48
49
  this.#statusCode = 0
49
50
  this.#headers = null
50
51
  this.#trailers = null
52
+ this.#body = ''
53
+ this.#decoder = null
51
54
 
52
55
  if (!this.#headersSent) {
53
56
  this.#pos = null
@@ -117,6 +120,13 @@ class Handler extends DecoratorHandler {
117
120
  this.#end = contentLength
118
121
  this.#etag = headers.etag
119
122
  } else if (statusCode >= 400) {
123
+ if (
124
+ this.#headers['content-type']?.startsWith('application/json') ||
125
+ this.#headers['content-type']?.startsWith('text/plain')
126
+ ) {
127
+ this.#decoder = new TextDecoder('utf-8')
128
+ this.#body = ''
129
+ }
120
130
  return true
121
131
  } else {
122
132
  return this.#onHeaders(statusCode, headers, resume)
@@ -161,29 +171,30 @@ class Handler extends DecoratorHandler {
161
171
  }
162
172
 
163
173
  onData(chunk) {
164
- if (this.#statusCode >= 400) {
165
- // TODO (fix): Limit the amount of data we read?
166
- return true
167
- }
168
-
169
174
  if (this.#pos != null) {
170
175
  this.#pos += chunk.byteLength
171
176
  }
172
- return super.onData(chunk)
173
- }
174
177
 
175
- onError(err) {
176
- this.#maybeRetry(err)
178
+ if (this.#statusCode < 400) {
179
+ return super.onData(chunk)
180
+ }
181
+
182
+ this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
177
183
  }
178
184
 
179
185
  onComplete(trailers) {
180
186
  this.#trailers = trailers
181
187
 
182
- if (this.#statusCode >= 400) {
183
- this.#maybeRetry(null, this.#statusCode)
184
- } else {
185
- super.onComplete(trailers)
188
+ if (this.#statusCode < 400) {
189
+ return super.onComplete(trailers)
186
190
  }
191
+
192
+ this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
193
+ this.#maybeRetry(null)
194
+ }
195
+
196
+ onError(err) {
197
+ this.#maybeRetry(err)
187
198
  }
188
199
 
189
200
  #maybeError(err) {
@@ -202,26 +213,29 @@ class Handler extends DecoratorHandler {
202
213
  }
203
214
  }
204
215
 
205
- #maybeRetry(err, statusCode) {
216
+ #maybeRetry(err) {
206
217
  if (this.#aborted || isDisturbed(this.#opts.body) || (this.#pos && !this.#etag)) {
207
218
  this.#maybeError(err)
208
219
  return
209
220
  }
210
221
 
211
- if (!err) {
212
- // TOOD (fix): Avoid creating an Error and do onHeaders + onComplete.
213
- const stackTraceLimit = Error.stackTraceLimit
214
- Error.stackTraceLimit = 0
215
- try {
216
- err = createHttpError(statusCode ?? 500)
217
- } finally {
218
- Error.stackTraceLimit = stackTraceLimit
219
- }
220
- }
222
+ this.#error =
223
+ err ??
224
+ decorateError(null, this.#opts, {
225
+ statusCode: this.#statusCode,
226
+ headers: this.#headers,
227
+ trailers: this.#trailers,
228
+ body: this.#body,
229
+ })
221
230
 
222
231
  let retryPromise
223
232
  try {
224
- retryPromise = retryFn(err, this.#retryCount, this.#opts)
233
+ retryPromise =
234
+ typeof this.#opts?.retry === 'function'
235
+ ? this.#opts?.retry(this.#error, this.#retryCount, this.#opts, () =>
236
+ retryFn(this.#error, this.#retryCount, this.#opts),
237
+ )
238
+ : retryFn(this.#error, this.#retryCount, this.#opts)
225
239
  } catch (err) {
226
240
  retryPromise = Promise.reject(err)
227
241
  }
@@ -231,8 +245,6 @@ class Handler extends DecoratorHandler {
231
245
  return
232
246
  }
233
247
 
234
- this.#error = err
235
-
236
248
  retryPromise
237
249
  .then((opts) => {
238
250
  if (this.#aborted) {
@@ -301,10 +313,6 @@ async function retryFn(err, retryCount, opts) {
301
313
  throw err
302
314
  }
303
315
 
304
- if (typeof retryOpts === 'function') {
305
- return retryOpts(err, retryCount, opts, () => retryFn(err, retryCount, opts))
306
- }
307
-
308
316
  if (typeof retryOpts === 'number') {
309
317
  retryOpts = { count: retryOpts }
310
318
  }
@@ -338,6 +346,7 @@ async function retryFn(err, retryCount, opts) {
338
346
  'EHOSTDOWN',
339
347
  'EHOSTUNREACH',
340
348
  'EPIPE',
349
+ 'ENODATA',
341
350
  'UND_ERR_CONNECT_TIMEOUT',
342
351
  ].includes(err.code)
343
352
  ) {
@@ -410,7 +410,7 @@ function makeResult(value) {
410
410
  /**
411
411
  * @param {any} key
412
412
  */
413
- export function assertCacheKey(key) {
413
+ function assertCacheKey(key) {
414
414
  if (typeof key !== 'object') {
415
415
  throw new TypeError(`expected key to be object, got ${typeof key}`)
416
416
  }
@@ -429,7 +429,7 @@ export function assertCacheKey(key) {
429
429
  /**
430
430
  * @param {any} value
431
431
  */
432
- export function assertCacheValue(value) {
432
+ function assertCacheValue(value) {
433
433
  if (typeof value !== 'object') {
434
434
  throw new TypeError(`expected value to be object, got ${typeof value}`)
435
435
  }
package/lib/utils.js CHANGED
@@ -2,6 +2,7 @@ import cacheControlParser from 'cache-control-parser'
2
2
  import stream from 'node:stream'
3
3
  import assert from 'node:assert'
4
4
  import { util } from '@nxtedition/undici'
5
+ import createHttpError from 'http-errors'
5
6
 
6
7
  let fastNow = Date.now()
7
8
 
@@ -351,9 +352,69 @@ export function parseHeaders(headers, obj) {
351
352
  }
352
353
 
353
354
  // See https://github.com/nodejs/node/pull/46528
354
- if ('content-length' in obj && 'content-disposition' in obj) {
355
+ if (obj != null && 'content-length' in obj && 'content-disposition' in obj) {
355
356
  obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
356
357
  }
357
358
 
358
359
  return obj
359
360
  }
361
+
362
+ export function decorateError(err, opts, { statusCode, headers, trailers, body }) {
363
+ try {
364
+ if (err == null) {
365
+ const stackTraceLimit = Error.stackTraceLimit
366
+ Error.stackTraceLimit = 0
367
+ try {
368
+ err = createHttpError(statusCode)
369
+ } finally {
370
+ Error.stackTraceLimit = stackTraceLimit
371
+ }
372
+ }
373
+
374
+ if (statusCode != null) {
375
+ err.statusCode = statusCode
376
+ }
377
+
378
+ err.url ??= opts.origin ? new URL(opts.path, opts.origin).href : null
379
+
380
+ err.req = {
381
+ method: opts?.method,
382
+ headers: opts?.headers,
383
+ body:
384
+ // TODO (fix): JSON.stringify POJO
385
+ typeof opts?.body !== 'string' || opts.body.length > 1024 ? undefined : opts.body,
386
+ }
387
+
388
+ if (headers?.['content-type']?.startsWith('application/json') && typeof body === 'string') {
389
+ try {
390
+ body = JSON.parse(body)
391
+ } catch {
392
+ // Do nothing...
393
+ }
394
+ }
395
+
396
+ err.res = {
397
+ headers,
398
+ trailers,
399
+ // TODO (fix): JSON.stringify POJO
400
+ body: typeof body !== 'string' || body.length < 1024 ? undefined : body,
401
+ statusCode,
402
+ }
403
+
404
+ if (body) {
405
+ if (body.reason != null) {
406
+ err.reason ??= body.reason
407
+ }
408
+ if (body.code != null) {
409
+ err.code ??= body.code
410
+ }
411
+ if (body.error != null) {
412
+ err.error ??= body.error
413
+ }
414
+ }
415
+
416
+ return err
417
+ } catch (er) {
418
+ return new AggregateError([er, err])
419
+ }
420
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "6.2.11",
3
+ "version": "6.2.13",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",