@nxtedition/nxt-undici 2.0.15 → 2.0.17

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.
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import assert from 'node:assert'
2
2
  import stream from 'node:stream'
3
3
  import createError from 'http-errors'
4
4
  import undici from 'undici'
5
- import { parseHeaders, AbortError } from './utils.js'
5
+ import { findHeader, parseHeaders, AbortError } from './utils.js'
6
6
  import CacheableLookup from 'cacheable-lookup'
7
7
 
8
8
  const dispatcherCache = new WeakMap()
@@ -25,13 +25,15 @@ const kStatusCode = Symbol('statusCode')
25
25
  const kStatusMessage = Symbol('statusMessage')
26
26
  const kHeaders = Symbol('headers')
27
27
  const kSize = Symbol('size')
28
+ const kHandler = Symbol('handler')
28
29
 
29
30
  let ABORT_ERROR
30
31
 
31
32
  class Readable extends stream.Readable {
32
- constructor({ statusCode, statusMessage, headers, size, abort, highWaterMark, resume }) {
33
- super(highWaterMark ? { highWaterMark } : undefined)
33
+ constructor(handler, { statusCode, statusMessage, headers, size, abort, highWaterMark, resume }) {
34
+ super({ highWaterMark })
34
35
 
36
+ this[kHandler] = handler
35
37
  this[kStatusCode] = statusCode
36
38
  this[kStatusMessage] = statusMessage
37
39
  this[kHeaders] = headers
@@ -71,12 +73,18 @@ class Readable extends stream.Readable {
71
73
  this[kAbort](err)
72
74
  }
73
75
 
76
+ if (this[kHandler].signal) {
77
+ this[kHandler].signal.removeEventListener('abort', this[kHandler].onAbort)
78
+ this[kHandler].signal = null
79
+ }
80
+
74
81
  callback(err)
75
82
  }
76
83
 
77
84
  async text() {
78
85
  const dec = new TextDecoder()
79
86
  let str = ''
87
+
80
88
  for await (const chunk of this) {
81
89
  if (typeof chunk === 'string') {
82
90
  str += chunk
@@ -84,6 +92,7 @@ class Readable extends stream.Readable {
84
92
  str += dec.decode(chunk, { stream: true })
85
93
  }
86
94
  }
95
+
87
96
  // Flush the streaming TextDecoder so that any pending
88
97
  // incomplete multibyte characters are handled.
89
98
  str += dec.decode(undefined, { stream: false })
@@ -127,16 +136,15 @@ class Readable extends stream.Readable {
127
136
  }
128
137
 
129
138
  const dispatchers = {
139
+ responseError: (await import('./interceptor/response-error.js')).default,
130
140
  requestBody: (await import('./interceptor/request-body.js')).default,
131
141
  requestBodyFactory: (await import('./interceptor/request-body-factory.js')).default,
132
142
  responseContent: (await import('./interceptor/response-content.js')).default,
133
143
  requestContent: (await import('./interceptor/request-content.js')).default,
134
144
  log: (await import('./interceptor/log.js')).default,
135
145
  redirect: (await import('./interceptor/redirect.js')).default,
136
- responseBodyRetry: (await import('./interceptor/response-body-retry.js')).default,
137
- responseStatusRetry: (await import('./interceptor/response-status-retry.js')).default,
138
146
  responseRetry: (await import('./interceptor/response-retry.js')).default,
139
- signal: (await import('./interceptor/signal.js')).default,
147
+ responseRetryBody: (await import('./interceptor/response-retry-body.js')).default,
140
148
  proxy: (await import('./interceptor/proxy.js')).default,
141
149
  cache: (await import('./interceptor/cache.js')).default,
142
150
  requestId: (await import('./interceptor/request-id.js')).default,
@@ -222,26 +230,25 @@ export async function request(url, opts) {
222
230
  let dispatch = dispatcherCache.get(dispatcher)
223
231
  if (dispatch == null) {
224
232
  dispatch = (opts, handler) => dispatcher.dispatch(opts, handler)
233
+ dispatch = dispatchers.responseError(dispatch)
225
234
  dispatch = dispatchers.requestBodyFactory(dispatch)
226
235
  dispatch = dispatchers.log(dispatch)
227
236
  dispatch = dispatchers.requestId(dispatch)
228
237
  dispatch = dispatchers.responseRetry(dispatch)
229
- dispatch = dispatchers.responseStatusRetry(dispatch)
230
- dispatch = dispatchers.responseBodyRetry(dispatch)
238
+ dispatch = dispatchers.responseRetryBody(dispatch)
231
239
  dispatch = dispatchers.responseContent(dispatch)
232
240
  dispatch = dispatchers.requestContent(dispatch)
233
- dispatch = dispatchers.redirect(dispatch)
234
- dispatch = dispatchers.signal(dispatch)
235
241
  dispatch = dispatchers.cache(dispatch)
242
+ dispatch = dispatchers.redirect(dispatch)
236
243
  dispatch = dispatchers.proxy(dispatch)
237
244
  dispatch = dispatchers.requestBody(dispatch)
238
245
  dispatcherCache.set(dispatcher, dispatch)
239
246
  }
240
247
 
241
- const res = await new Promise((resolve, reject) =>
248
+ return await new Promise((resolve, reject) =>
242
249
  dispatch(
243
250
  {
244
- id: opts.id ?? headers?.['request-id'] ?? headers?.['Request-Id'] ?? genReqId(),
251
+ id: opts.id ?? findHeader(headers, 'request-id') ?? genReqId(),
245
252
  url,
246
253
  method,
247
254
  body: opts.body,
@@ -253,7 +260,6 @@ export async function request(url, opts) {
253
260
  headersTimeout: opts.headersTimeout,
254
261
  bodyTimeout: opts.bodyTimeout,
255
262
  idempotent,
256
- signal: opts.signal,
257
263
  retry: opts.retry ?? 8,
258
264
  proxy: opts.proxy,
259
265
  cache: opts.cache,
@@ -266,14 +272,28 @@ export async function request(url, opts) {
266
272
  resolve,
267
273
  reject,
268
274
  logger: opts.logger,
275
+ signal: opts.signal,
269
276
  /** @type {Function | null} */ abort: null,
270
277
  /** @type {stream.Readable | null} */ body: null,
271
278
  onConnect(abort) {
272
- this.abort = abort
279
+ if (this.signal?.aborted) {
280
+ abort(this.signal.reason)
281
+ } else {
282
+ this.abort = abort
283
+
284
+ if (this.signal) {
285
+ this.onAbort = () => {
286
+ if (this.body) {
287
+ this.body.destroy(this.signal.reason ?? new AbortError())
288
+ } else {
289
+ this.abort(this.signal.reason)
290
+ }
291
+ }
292
+ this.signal.addEventListener('abort', this.onAbort)
293
+ }
294
+ }
273
295
  },
274
- onUpgrade(statusCode, rawHeaders, socket) {
275
- const headers = parseHeaders(rawHeaders)
276
-
296
+ onUpgrade(statusCode, rawHeaders, socket, headers = parseHeaders(rawHeaders)) {
277
297
  if (statusCode !== 101) {
278
298
  this.abort(createError(statusCode, { headers }))
279
299
  } else {
@@ -283,14 +303,18 @@ export async function request(url, opts) {
283
303
  },
284
304
  onBodySent(chunk) {},
285
305
  onRequestSent() {},
286
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
287
- const headers = parseHeaders(rawHeaders)
288
-
306
+ onHeaders(
307
+ statusCode,
308
+ rawHeaders,
309
+ resume,
310
+ statusMessage,
311
+ headers = parseHeaders(rawHeaders),
312
+ ) {
289
313
  assert(statusCode >= 200)
290
314
 
291
- const contentLength = Number(headers['content-length'] ?? headers['Content-Length'])
315
+ const contentLength = findHeader(rawHeaders, 'content-length')
292
316
 
293
- this.body = new Readable({
317
+ this.body = new Readable(this, {
294
318
  resume,
295
319
  abort: this.abort,
296
320
  highWaterMark: this.highWaterMark,
@@ -304,7 +328,7 @@ export async function request(url, opts) {
304
328
  this.resolve = null
305
329
  this.reject = null
306
330
 
307
- return false
331
+ return true
308
332
  },
309
333
  onData(chunk) {
310
334
  return this.body.push(chunk)
@@ -313,6 +337,9 @@ export async function request(url, opts) {
313
337
  this.body.push(null)
314
338
  },
315
339
  onError(err) {
340
+ this.signal?.removeEventListener('abort', this.onAbort)
341
+ this.signal = null
342
+
316
343
  if (this.body) {
317
344
  this.body.destroy(err)
318
345
  } else {
@@ -324,18 +351,4 @@ export async function request(url, opts) {
324
351
  },
325
352
  ),
326
353
  )
327
-
328
- if (method === 'HEAD') {
329
- await res.dump()
330
- } else if (res.statusCode >= 400) {
331
- // TODO (fix): Limit the size of the body?
332
- const data = /^application\/json$/i.test(res.headers['content-type'])
333
- ? await res.json()
334
- : /^text-.+/i.test(res.headers['content-type'])
335
- ? await res.text()
336
- : await res.dump()
337
- throw createError(res.statusCode, { headers, data })
338
- }
339
-
340
- return res
341
354
  }
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
  import { LRUCache } from 'lru-cache'
3
- import cacheControlParser from 'cache-control-parser'
3
+ import { findHeader, parseCacheControl } from '../utils.js'
4
4
 
5
5
  class CacheHandler {
6
6
  constructor({ key, handler, store }) {
@@ -14,8 +14,8 @@ class CacheHandler {
14
14
  return this.handler.onConnect(abort)
15
15
  }
16
16
 
17
- onUpgrade(statusCode, rawHeaders, socket) {
18
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
17
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
18
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
19
19
  }
20
20
 
21
21
  onBodySent(chunk) {
@@ -26,22 +26,13 @@ class CacheHandler {
26
26
  return this.handler.onRequestSent()
27
27
  }
28
28
 
29
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
29
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
30
30
  // NOTE: Only cache 307 respones for now...
31
31
  if (statusCode !== 307) {
32
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
32
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
33
33
  }
34
34
 
35
- let cacheControl
36
- for (let n = 0; n < rawHeaders.length; n += 2) {
37
- if (
38
- rawHeaders[n].length === 'cache-control'.length &&
39
- rawHeaders[n].toString().toLowerCase() === 'cache-control'
40
- ) {
41
- cacheControl = cacheControlParser.parse(rawHeaders[n + 1].toString())
42
- break
43
- }
44
- }
35
+ const cacheControl = parseCacheControl(findHeader(rawHeaders, 'cache-control'))
45
36
 
46
37
  if (
47
38
  cacheControl &&
@@ -73,7 +64,7 @@ class CacheHandler {
73
64
  }
74
65
  }
75
66
 
76
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
67
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
77
68
  }
78
69
 
79
70
  onData(chunk) {
@@ -31,13 +31,13 @@ class Handler {
31
31
  })
32
32
  }
33
33
 
34
- onUpgrade(statusCode, rawHeaders, socket) {
34
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
35
35
  this.logger.debug('upstream request upgraded')
36
36
  socket.on('close', () => {
37
37
  this.logger.debug('upstream request socket closed')
38
38
  })
39
39
 
40
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
40
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
41
41
  }
42
42
 
43
43
  onBodySent(chunk) {
@@ -54,18 +54,18 @@ class Handler {
54
54
  return this.handler.onRequestSent()
55
55
  }
56
56
 
57
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
57
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
58
58
  this.stats.headers = performance.now() - this.stats.start
59
59
 
60
60
  this.logger.debug(
61
61
  {
62
- ures: { statusCode, headers: parseHeaders(rawHeaders) },
62
+ ures: { statusCode, headers },
63
63
  elapsedTime: this.stats.headers,
64
64
  },
65
65
  'upstream request response',
66
66
  )
67
67
 
68
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
68
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
69
69
  }
70
70
 
71
71
  onData(chunk) {
@@ -34,8 +34,8 @@ class Handler {
34
34
  }
35
35
  }
36
36
 
37
- onUpgrade(statusCode, headers, socket) {
38
- return this.handler.onUpgrade(statusCode, headers, socket)
37
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
38
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
39
39
  }
40
40
 
41
41
  onBodySent(chunk) {
@@ -46,16 +46,16 @@ class Handler {
46
46
  return this.handler.onRequestSent()
47
47
  }
48
48
 
49
- onHeaders(statusCode, headers, resume, statusText) {
49
+ onHeaders(statusCode, rawHeaders, resume, statusText, headers) {
50
50
  if (redirectableStatusCodes.indexOf(statusCode) === -1) {
51
- return this.handler.onHeaders(statusCode, headers, resume, statusText)
51
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
52
52
  }
53
53
 
54
54
  if (isDisturbed(this.opts.body)) {
55
55
  throw new Error(`Disturbed request cannot be redirected.`)
56
56
  }
57
57
 
58
- this.location = findHeader(headers, 'location')
58
+ this.location = findHeader(rawHeaders, 'location')
59
59
 
60
60
  if (!this.location) {
61
61
  throw new Error(`Missing redirection location .`)
@@ -65,7 +65,7 @@ class Handler {
65
65
 
66
66
  if (typeof this.opts.follow === 'function') {
67
67
  if (!this.opts.follow(this.location, this.count)) {
68
- return this.handler.onHeaders(statusCode, headers, resume, statusText)
68
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
69
69
  }
70
70
  } else {
71
71
  if (this.count >= this.maxCount) {
@@ -8,7 +8,7 @@ export default (dispatch) => (opts, handler) => {
8
8
 
9
9
  const body = opts.body({ signal: opts.signal })
10
10
 
11
- if (typeof body.then === 'function') {
11
+ if (typeof body?.then === 'function') {
12
12
  body.then(
13
13
  (body) => dispatch({ ...opts, body }, handler),
14
14
  (err) => handler.onError(err),
@@ -27,8 +27,8 @@ class Handler {
27
27
  }
28
28
  }
29
29
 
30
- onUpgrade(statusCode, rawHeaders, socket) {
31
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
30
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
31
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
32
32
  }
33
33
 
34
34
  onBodySent(chunk) {
@@ -39,8 +39,8 @@ class Handler {
39
39
  return this.handler.onRequestSent()
40
40
  }
41
41
 
42
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
43
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
42
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
43
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
44
44
  }
45
45
 
46
46
  onData(chunk) {
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto'
2
+ import { findHeader } from '../utils.js'
2
3
 
3
4
  class Handler {
4
5
  constructor(opts, { handler, md5, length }) {
@@ -16,8 +17,8 @@ class Handler {
16
17
  return this.handler.onConnect(abort)
17
18
  }
18
19
 
19
- onUpgrade(statusCode, rawHeaders, socket) {
20
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
20
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
21
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
21
22
  }
22
23
 
23
24
  onBodySent(chunk) {
@@ -51,8 +52,8 @@ class Handler {
51
52
  return this.handler.onRequestSent()
52
53
  }
53
54
 
54
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
55
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
55
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
56
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
56
57
  }
57
58
 
58
59
  onData(chunk) {
@@ -74,8 +75,8 @@ export default (dispatch) => (opts, handler) => {
74
75
  }
75
76
 
76
77
  // TODO (fix): case-insensitive check?
77
- const md5 = opts.headers?.['content-md5'] ?? opts.headers?.['Content-MD5']
78
- const length = opts.headers?.['content-lenght'] ?? opts.headers?.['Content-Length']
78
+ const md5 = findHeader(opts.headers, 'content-md5')
79
+ const length = findHeader(opts.headers, 'content-length')
79
80
 
80
81
  return md5 != null || length != null
81
82
  ? dispatch(opts, new Handler(opts, { handler, md5, length }))
@@ -14,8 +14,8 @@ class Handler {
14
14
  return this.handler.onConnect(abort)
15
15
  }
16
16
 
17
- onUpgrade(statusCode, rawHeaders, socket) {
18
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
17
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
18
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
19
19
  }
20
20
 
21
21
  onBodySent(chunk) {
@@ -26,12 +26,12 @@ class Handler {
26
26
  return this.handler.onRequestSent()
27
27
  }
28
28
 
29
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
29
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
30
30
  this.md5 = findHeader(rawHeaders, 'content-md5')
31
31
  this.length = findHeader(rawHeaders, 'content-length')
32
32
  this.hasher = this.md5 != null ? crypto.createHash('md5') : null
33
33
 
34
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
34
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
35
35
  }
36
36
 
37
37
  onData(chunk) {
@@ -0,0 +1,96 @@
1
+ import { findHeader, parseHeaders } from '../utils.js'
2
+ import createHttpError from 'http-errors'
3
+
4
+ class Handler {
5
+ constructor(opts, { handler }) {
6
+ this.handler = handler
7
+ this.statusCode = 0
8
+ this.contentType = null
9
+ this.decoder = null
10
+ this.headers = null
11
+ this.body = null
12
+ }
13
+
14
+ onConnect(abort) {
15
+ return this.handler.onConnect(abort)
16
+ }
17
+
18
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
19
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
20
+ }
21
+
22
+ onBodySent(chunk) {
23
+ return this.handler.onBodySent(chunk)
24
+ }
25
+
26
+ onRequestSent() {
27
+ return this.handler.onRequestSent()
28
+ }
29
+
30
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
31
+ if (statusCode >= 400) {
32
+ this.statusCode = statusCode
33
+ this.headers = headers
34
+ this.contentType = findHeader(rawHeaders, 'content-type')
35
+ if (this.contentType === 'application/json' || this.contentType === 'text/plain') {
36
+ this.decoder = new TextDecoder('utf-8')
37
+ this.body = ''
38
+ }
39
+ } else {
40
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
41
+ }
42
+ }
43
+
44
+ onData(chunk) {
45
+ if (this.statusCode) {
46
+ if (this.decoder) {
47
+ // TODO (fix): Limit body size?
48
+ this.body += this.decoder.decode(chunk, { stream: true })
49
+ }
50
+ return true
51
+ } else {
52
+ return this.handler.onData(chunk)
53
+ }
54
+ }
55
+
56
+ onComplete(rawTrailers) {
57
+ this.onFinally(null, rawTrailers)
58
+ }
59
+
60
+ onError(err) {
61
+ this.onFinally(err, null)
62
+ }
63
+
64
+ onFinally(err, rawTrailers) {
65
+ if (this.statusCode) {
66
+ if (this.decoder != null) {
67
+ this.body += this.decoder.decode(undefined, { stream: false })
68
+ if (this.contentType === 'application/json') {
69
+ this.body = JSON.parse(this.body)
70
+ }
71
+ }
72
+
73
+ const stackTraceLimit = Error.stackTraceLimit
74
+ Error.stackTraceLimit = 0
75
+ try {
76
+ this.handler.onError(
77
+ createHttpError(this.statusCode, { headers: this.headers, body: this.body }),
78
+ )
79
+ } finally {
80
+ Error.stackTraceLimit = stackTraceLimit
81
+ }
82
+
83
+ this.decoder = null
84
+ this.contentType = null
85
+ this.body = null
86
+ } else if (err) {
87
+ this.handler.onError(err)
88
+ } else {
89
+ this.handler.onComplete(rawTrailers)
90
+ }
91
+
92
+ this.handler = null
93
+ }
94
+ }
95
+
96
+ export default (dispatch) => (opts, handler) => dispatch(opts, new Handler(opts, { handler }))
@@ -6,18 +6,16 @@ class Handler {
6
6
  this.dispatch = dispatch
7
7
  this.handler = handler
8
8
  this.opts = opts
9
- this.abort = null
10
- this.aborted = false
11
- this.reason = null
12
-
13
- this.retryCount = 0
14
- this.retryPromise = null
15
9
 
10
+ this.count = 0
16
11
  this.pos = 0
17
12
  this.end = null
18
13
  this.error = null
19
14
  this.etag = null
20
15
 
16
+ this.reason = null
17
+ this.aborted = false
18
+
21
19
  this.handler.onConnect((reason) => {
22
20
  this.aborted = true
23
21
  if (this.abort) {
@@ -36,8 +34,8 @@ class Handler {
36
34
  }
37
35
  }
38
36
 
39
- onUpgrade(statusCode, rawHeaders, socket) {
40
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
37
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
38
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
41
39
  }
42
40
 
43
41
  onBodySent(chunk) {
@@ -48,7 +46,7 @@ class Handler {
48
46
  return this.handler.onRequestSent()
49
47
  }
50
48
 
51
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
49
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
52
50
  const etag = findHeader(rawHeaders, 'etag')
53
51
 
54
52
  if (this.resume) {
@@ -82,7 +80,13 @@ class Handler {
82
80
  if (statusCode === 206) {
83
81
  const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
84
82
  if (!contentRange) {
85
- return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
83
+ return this.handler.onHeaders(
84
+ statusCode,
85
+ rawHeaders,
86
+ () => this.resume(),
87
+ statusMessage,
88
+ headers,
89
+ )
86
90
  }
87
91
 
88
92
  const { start, size, end = size } = contentRange
@@ -102,7 +106,14 @@ class Handler {
102
106
 
103
107
  this.etag = etag
104
108
  this.resume = resume
105
- return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
109
+
110
+ return this.handler.onHeaders(
111
+ statusCode,
112
+ rawHeaders,
113
+ () => this.resume(),
114
+ statusMessage,
115
+ headers,
116
+ )
106
117
  }
107
118
 
108
119
  onData(chunk) {
@@ -116,31 +127,15 @@ class Handler {
116
127
  }
117
128
 
118
129
  onError(err) {
119
- if (!this.resume || this.aborted || !this.etag || isDisturbed(this.opts.body)) {
130
+ if (this.aborted || !this.etag || isDisturbed(this.opts.body)) {
120
131
  return this.handler.onError(err)
121
132
  }
122
133
 
123
- const retryPromise = retryFn(err, this.retryCount++, this.opts)
134
+ const retryPromise = retryFn(err, this.count++, this.opts)
124
135
  if (retryPromise == null) {
125
136
  return this.handler.onError(err)
126
137
  }
127
138
 
128
- retryPromise
129
- .then(() => {
130
- if (!this.aborted) {
131
- try {
132
- this.dispatch(this.opts, this)
133
- } catch (err2) {
134
- this.handler.onError(new AggregateError([err, err2]))
135
- }
136
- }
137
- })
138
- .catch((err) => {
139
- if (!this.aborted) {
140
- this.handler.onError(err)
141
- }
142
- })
143
-
144
139
  this.error = err
145
140
  this.opts = {
146
141
  ...this.opts,
@@ -152,6 +147,18 @@ class Handler {
152
147
  }
153
148
 
154
149
  this.opts.logger?.debug('retrying response body')
150
+
151
+ retryPromise
152
+ .then(() => {
153
+ if (!this.aborted) {
154
+ this.dispatch(this.opts, this)
155
+ }
156
+ })
157
+ .catch((err) => {
158
+ if (!this.aborted) {
159
+ this.handler.onError(err)
160
+ }
161
+ })
155
162
  }
156
163
  }
157
164
 
@@ -5,13 +5,12 @@ class Handler {
5
5
  this.dispatch = dispatch
6
6
  this.handler = handler
7
7
  this.opts = opts
8
- this.abort = null
9
- this.aborted = false
10
- this.reason = null
11
- this.statusCode = 0
12
8
 
13
- this.retryCount = 0
14
- this.retryPromise = null
9
+ this.hasBody = false
10
+ this.count = 0
11
+
12
+ this.reason = null
13
+ this.aborted = false
15
14
 
16
15
  this.handler.onConnect((reason) => {
17
16
  this.aborted = true
@@ -31,8 +30,8 @@ class Handler {
31
30
  }
32
31
  }
33
32
 
34
- onUpgrade(statusCode, rawHeaders, socket) {
35
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
33
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
34
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
36
35
  }
37
36
 
38
37
  onBodySent(chunk) {
@@ -43,12 +42,12 @@ class Handler {
43
42
  return this.handler.onRequestSent()
44
43
  }
45
44
 
46
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
47
- this.statusCode = statusCode
48
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
45
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
46
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
49
47
  }
50
48
 
51
49
  onData(chunk) {
50
+ this.hasBody = true
52
51
  return this.handler.onData(chunk)
53
52
  }
54
53
 
@@ -57,23 +56,21 @@ class Handler {
57
56
  }
58
57
 
59
58
  onError(err) {
60
- if (this.aborted || this.statusCode || isDisturbed(this.opts.body)) {
59
+ if (this.aborted || this.hasBody || isDisturbed(this.opts.body)) {
61
60
  return this.handler.onError(err)
62
61
  }
63
62
 
64
- const retryPromise = retryFn(err, this.retryCount++, this.opts)
63
+ const retryPromise = retryFn(err, this.count++, this.opts)
65
64
  if (retryPromise == null) {
66
65
  return this.handler.onError(err)
67
66
  }
68
67
 
68
+ this.opts.logger?.debug('retrying response')
69
+
69
70
  retryPromise
70
71
  .then(() => {
71
72
  if (!this.aborted) {
72
- try {
73
- this.dispatch(this.opts, this)
74
- } catch (err2) {
75
- this.handler.onError(new AggregateError([err, err2]))
76
- }
73
+ this.dispatch(this.opts, this)
77
74
  }
78
75
  })
79
76
  .catch((err) => {
@@ -81,12 +78,11 @@ class Handler {
81
78
  this.handler.onError(err)
82
79
  }
83
80
  })
84
-
85
- this.opts.logger?.debug('retrying response')
86
81
  }
87
82
  }
88
83
 
89
- export default (dispatch) => (opts, handler) =>
90
- opts.idempotent && opts.retry && !opts.upgrade
84
+ export default (dispatch) => (opts, handler) => {
85
+ return opts.retry
91
86
  ? dispatch(opts, new Handler(opts, { handler, dispatch }))
92
87
  : dispatch(opts, handler)
88
+ }
package/lib/utils.js CHANGED
@@ -1,4 +1,19 @@
1
1
  import tp from 'node:timers/promises'
2
+ import cacheControlParser from 'cache-control-parser'
3
+
4
+ export function parseCacheControl(str) {
5
+ return str ? cacheControlParser.parse(str) : null
6
+ }
7
+
8
+ const lowerCaseCache = new Map()
9
+ export function toLowerCase(val) {
10
+ let ret = lowerCaseCache.get(val)
11
+ if (ret === undefined) {
12
+ ret = val.toLowerCase()
13
+ lowerCaseCache.set(val, ret)
14
+ }
15
+ return ret
16
+ }
2
17
 
3
18
  export function isDisturbed(body) {
4
19
  if (
@@ -45,15 +60,31 @@ export function parseContentRange(range) {
45
60
  return { start, end: end ? end + 1 : size, size }
46
61
  }
47
62
 
48
- export function findHeader(rawHeaders, name) {
63
+ export function findHeader(headers, name) {
49
64
  const len = name.length
50
65
 
51
- for (let i = 0; i < rawHeaders.length; i += 2) {
52
- const key = rawHeaders[i + 0]
53
- if (key.length === len && key.toString().toLowerCase() === name) {
54
- return rawHeaders[i + 1].toString()
66
+ if (Array.isArray(headers)) {
67
+ for (let i = 0; i < headers.length; i += 2) {
68
+ const key = headers[i + 0]
69
+ if (key.length === len && toLowerCase(key.toString()) === name) {
70
+ return headers[i + 1].toString()
71
+ }
72
+ }
73
+ } else if (headers != null) {
74
+ {
75
+ const val = headers[name]
76
+ if (val !== undefined) {
77
+ return val
78
+ }
79
+ }
80
+
81
+ for (const key of Object.keys(headers)) {
82
+ if (key.length === len && toLowerCase(key) === name) {
83
+ return headers[key].toString()
84
+ }
55
85
  }
56
86
  }
87
+
57
88
  return null
58
89
  }
59
90
 
@@ -178,18 +209,24 @@ export function parseOrigin(url) {
178
209
  return url
179
210
  }
180
211
 
181
- export function parseHeaders(rawHeaders, obj = {}) {
182
- for (let i = 0; i < rawHeaders.length; i += 2) {
183
- const key = rawHeaders[i].toString().toLowerCase()
212
+ export function parseHeaders(headers, obj = {}) {
213
+ for (let i = 0; i < headers.length; i += 2) {
214
+ const key = toLowerCase(headers[i].toString())
215
+
184
216
  let val = obj[key]
185
217
  if (!val) {
186
- obj[key] = rawHeaders[i + 1].toString()
218
+ val = headers[i + 1]
219
+ if (typeof val === 'string') {
220
+ obj[key] = val
221
+ } else {
222
+ obj[key] = Array.isArray(val) ? val.map((x) => x.toString()) : val.toString()
223
+ }
187
224
  } else {
188
225
  if (!Array.isArray(val)) {
189
226
  val = [val]
190
227
  obj[key] = val
191
228
  }
192
- val.push(rawHeaders[i + 1].toString())
229
+ val.push(headers[i + 1].toString())
193
230
  }
194
231
  }
195
232
  return obj
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",
@@ -1,104 +0,0 @@
1
- import { parseHeaders, isDisturbed, retry as retryFn } from '../utils.js'
2
- import createError from 'http-errors'
3
-
4
- class Handler {
5
- constructor(opts, { dispatch, handler }) {
6
- this.dispatch = dispatch
7
- this.handler = handler
8
- this.opts = opts
9
- this.abort = null
10
- this.aborted = false
11
- this.reason = null
12
-
13
- this.retryCount = 0
14
- this.retryPromise = null
15
-
16
- this.handler.onConnect((reason) => {
17
- this.aborted = true
18
- if (this.abort) {
19
- this.abort(reason)
20
- } else {
21
- this.reason = reason
22
- }
23
- })
24
- }
25
-
26
- onConnect(abort) {
27
- if (this.aborted) {
28
- abort(this.reason)
29
- } else {
30
- this.abort = abort
31
- }
32
- }
33
-
34
- onUpgrade(statusCode, rawHeaders, socket) {
35
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
36
- }
37
-
38
- onBodySent(chunk) {
39
- return this.handler.onBodySent(chunk)
40
- }
41
-
42
- onRequestSent() {
43
- return this.handler.onRequestSent()
44
- }
45
-
46
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
47
- if (statusCode < 400) {
48
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
49
- }
50
-
51
- const err = createError(statusCode, { headers: parseHeaders(rawHeaders) })
52
-
53
- const retryPromise = retryFn(err, this.retryCount++, this.opts)
54
- if (retryPromise == null) {
55
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
56
- }
57
-
58
- retryPromise.catch(() => {})
59
-
60
- this.retryPromise = retryPromise
61
-
62
- this.abort(err)
63
-
64
- return false
65
- }
66
-
67
- onData(chunk) {
68
- return this.handler.onData(chunk)
69
- }
70
-
71
- onComplete(rawTrailers) {
72
- return this.handler.onComplete(rawTrailers)
73
- }
74
-
75
- onError(err) {
76
- if (this.retryPromise == null || this.aborted || isDisturbed(this.opts.body)) {
77
- return this.handler.onError(err)
78
- }
79
-
80
- this.retryPromise
81
- .then(() => {
82
- if (!this.aborted) {
83
- try {
84
- this.dispatch(this.opts, this)
85
- } catch (err2) {
86
- this.handler.onError(new AggregateError([err, err2]))
87
- }
88
- }
89
- })
90
- .catch((err) => {
91
- if (!this.aborted) {
92
- this.handler.onError(err)
93
- }
94
- })
95
- this.retryPromise = null
96
-
97
- this.opts.logger?.debug('retrying response status')
98
- }
99
- }
100
-
101
- export default (dispatch) => (opts, handler) =>
102
- opts.idempotent && opts.retry
103
- ? dispatch(opts, new Handler(opts, { handler, dispatch }))
104
- : dispatch(opts, handler)
@@ -1,57 +0,0 @@
1
- class Handler {
2
- constructor(opts, { handler }) {
3
- this.handler = handler
4
- this.signal = opts.signal
5
- this.abort = null
6
- }
7
-
8
- onConnect(abort) {
9
- if (this.signal.aborted) {
10
- abort(this.signal.reason)
11
- } else {
12
- this.abort = () => abort(this.signal.reason)
13
- this.signal.addEventListener('abort', this.abort)
14
-
15
- this.handler.onConnect(abort)
16
- }
17
- }
18
-
19
- onUpgrade(statusCode, rawHeaders, socket) {
20
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
21
- }
22
-
23
- onBodySent(chunk) {
24
- return this.handler.onBodySent(chunk)
25
- }
26
-
27
- onRequestSent() {
28
- return this.handler.onRequestSent()
29
- }
30
-
31
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
32
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
33
- }
34
-
35
- onData(chunk) {
36
- return this.handler.onData(chunk)
37
- }
38
-
39
- onComplete(rawTrailers) {
40
- if (this.abort) {
41
- this.signal.removeEventListener('abort', this.abort)
42
- }
43
-
44
- return this.handler.onComplete(rawTrailers)
45
- }
46
-
47
- onError(err) {
48
- if (this.abort) {
49
- this.signal.removeEventListener('abort', this.abort)
50
- }
51
-
52
- return this.handler.onError(err)
53
- }
54
- }
55
-
56
- export default (dispatch) => (opts, handler) =>
57
- opts.signal ? dispatch(opts, new Handler(opts, { handler })) : dispatch(opts, handler)