@nxtedition/nxt-undici 2.0.14 → 2.0.16

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,6 +73,11 @@ 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
 
@@ -127,16 +134,14 @@ class Readable extends stream.Readable {
127
134
  }
128
135
 
129
136
  const dispatchers = {
137
+ responseError: (await import('./interceptor/response-error.js')).default,
130
138
  requestBody: (await import('./interceptor/request-body.js')).default,
131
139
  requestBodyFactory: (await import('./interceptor/request-body-factory.js')).default,
132
140
  responseContent: (await import('./interceptor/response-content.js')).default,
133
141
  requestContent: (await import('./interceptor/request-content.js')).default,
134
142
  log: (await import('./interceptor/log.js')).default,
135
143
  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
144
  responseRetry: (await import('./interceptor/response-retry.js')).default,
139
- signal: (await import('./interceptor/signal.js')).default,
140
145
  proxy: (await import('./interceptor/proxy.js')).default,
141
146
  cache: (await import('./interceptor/cache.js')).default,
142
147
  requestId: (await import('./interceptor/request-id.js')).default,
@@ -222,23 +227,21 @@ export async function request(url, opts) {
222
227
  let dispatch = dispatcherCache.get(dispatcher)
223
228
  if (dispatch == null) {
224
229
  dispatch = (opts, handler) => dispatcher.dispatch(opts, handler)
230
+ dispatch = dispatchers.responseError(dispatch)
225
231
  dispatch = dispatchers.requestBodyFactory(dispatch)
226
232
  dispatch = dispatchers.log(dispatch)
227
233
  dispatch = dispatchers.requestId(dispatch)
228
234
  dispatch = dispatchers.responseRetry(dispatch)
229
- dispatch = dispatchers.responseStatusRetry(dispatch)
230
- dispatch = dispatchers.responseBodyRetry(dispatch)
231
235
  dispatch = dispatchers.responseContent(dispatch)
232
236
  dispatch = dispatchers.requestContent(dispatch)
233
237
  dispatch = dispatchers.redirect(dispatch)
234
- dispatch = dispatchers.signal(dispatch)
235
238
  dispatch = dispatchers.cache(dispatch)
236
239
  dispatch = dispatchers.proxy(dispatch)
237
240
  dispatch = dispatchers.requestBody(dispatch)
238
241
  dispatcherCache.set(dispatcher, dispatch)
239
242
  }
240
243
 
241
- const res = await new Promise((resolve, reject) =>
244
+ return await new Promise((resolve, reject) =>
242
245
  dispatch(
243
246
  {
244
247
  id: opts.id ?? headers?.['request-id'] ?? headers?.['Request-Id'] ?? genReqId(),
@@ -253,7 +256,6 @@ export async function request(url, opts) {
253
256
  headersTimeout: opts.headersTimeout,
254
257
  bodyTimeout: opts.bodyTimeout,
255
258
  idempotent,
256
- signal: opts.signal,
257
259
  retry: opts.retry ?? 8,
258
260
  proxy: opts.proxy,
259
261
  cache: opts.cache,
@@ -266,10 +268,26 @@ export async function request(url, opts) {
266
268
  resolve,
267
269
  reject,
268
270
  logger: opts.logger,
271
+ signal: opts.signal,
269
272
  /** @type {Function | null} */ abort: null,
270
273
  /** @type {stream.Readable | null} */ body: null,
271
274
  onConnect(abort) {
272
- this.abort = abort
275
+ if (this.signal?.aborted) {
276
+ abort(this.signal.reason)
277
+ } else {
278
+ this.abort = abort
279
+
280
+ if (this.signal) {
281
+ this.onAbort = () => {
282
+ if (this.body) {
283
+ this.body.destroy(this.signal.reason ?? new AbortError())
284
+ } else {
285
+ this.abort(this.signal.reason)
286
+ }
287
+ }
288
+ this.signal.addEventListener('abort', this.onAbort)
289
+ }
290
+ }
273
291
  },
274
292
  onUpgrade(statusCode, rawHeaders, socket) {
275
293
  const headers = parseHeaders(rawHeaders)
@@ -283,14 +301,18 @@ export async function request(url, opts) {
283
301
  },
284
302
  onBodySent(chunk) {},
285
303
  onRequestSent() {},
286
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
287
- const headers = parseHeaders(rawHeaders)
288
-
304
+ onHeaders(
305
+ statusCode,
306
+ rawHeaders,
307
+ resume,
308
+ statusMessage,
309
+ headers = parseHeaders(rawHeaders),
310
+ ) {
289
311
  assert(statusCode >= 200)
290
312
 
291
- const contentLength = Number(headers['content-length'] ?? headers['Content-Length'])
313
+ const contentLength = findHeader(rawHeaders, 'content-length')
292
314
 
293
- this.body = new Readable({
315
+ this.body = new Readable(this, {
294
316
  resume,
295
317
  abort: this.abort,
296
318
  highWaterMark: this.highWaterMark,
@@ -304,15 +326,21 @@ export async function request(url, opts) {
304
326
  this.resolve = null
305
327
  this.reject = null
306
328
 
307
- return false
329
+ return true
308
330
  },
309
331
  onData(chunk) {
310
332
  return this.body.push(chunk)
311
333
  },
312
334
  onComplete() {
335
+ this.signal?.removeEventListener('abort', this.onAbort)
336
+ this.signal = null
337
+
313
338
  this.body.push(null)
314
339
  },
315
340
  onError(err) {
341
+ this.signal?.removeEventListener('abort', this.onAbort)
342
+ this.signal = null
343
+
316
344
  if (this.body) {
317
345
  this.body.destroy(err)
318
346
  } else {
@@ -324,18 +352,4 @@ export async function request(url, opts) {
324
352
  },
325
353
  ),
326
354
  )
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
355
  }
@@ -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 }) {
@@ -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) {
@@ -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) {
@@ -4,16 +4,34 @@ import { findHeader, isDisturbed, parseURL } from '../utils.js'
4
4
  const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
5
5
 
6
6
  class Handler {
7
- constructor(opts, { dispatch, handler, count = 0 }) {
7
+ constructor(opts, { dispatch, handler }) {
8
8
  this.dispatch = dispatch
9
9
  this.handler = handler
10
10
  this.opts = opts
11
- this.redirectOpts = null
12
- this.count = count
11
+ this.abort = null
12
+ this.aborted = false
13
+ this.reason = null
14
+ this.maxCount = Number.isFinite(opts.follow) ? opts.follow : opts.follow?.count ?? 0
15
+
16
+ this.count = 0
17
+ this.location = null
18
+
19
+ this.handler.onConnect((reason) => {
20
+ this.aborted = true
21
+ if (this.abort) {
22
+ this.abort(reason)
23
+ } else {
24
+ this.reason = reason
25
+ }
26
+ })
13
27
  }
14
28
 
15
29
  onConnect(abort) {
16
- return this.handler.onConnect(abort)
30
+ if (this.aborted) {
31
+ abort(this.reason)
32
+ } else {
33
+ this.abort = abort
34
+ }
17
35
  }
18
36
 
19
37
  onUpgrade(statusCode, headers, socket) {
@@ -28,47 +46,42 @@ class Handler {
28
46
  return this.handler.onRequestSent()
29
47
  }
30
48
 
31
- onHeaders(statusCode, headers, resume, statusText) {
49
+ onHeaders(statusCode, rawHeaders, resume, statusText, headers) {
32
50
  if (redirectableStatusCodes.indexOf(statusCode) === -1) {
33
- return this.handler.onHeaders(statusCode, headers, resume, statusText)
51
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
34
52
  }
35
53
 
36
- const location = findHeader(headers, 'location')
54
+ if (isDisturbed(this.opts.body)) {
55
+ throw new Error(`Disturbed request cannot be redirected.`)
56
+ }
57
+
58
+ this.location = findHeader(rawHeaders, 'location')
37
59
 
38
- if (!location) {
39
- // TODO (perf): Consume body?
60
+ if (!this.location) {
40
61
  throw new Error(`Missing redirection location .`)
41
62
  }
42
63
 
43
64
  this.count += 1
44
65
 
45
- // TODO (feat): follow as function...
46
-
47
- const maxCount = Number.isFinite(this.opts.follow)
48
- ? this.opts.follow
49
- : Number.isFinite(this.opts.follow?.count)
50
- ? this.opts.follow?.count
51
- : 0
52
-
53
- if (this.count >= maxCount) {
54
- // TODO (perf): Consume body?
55
- throw new Error(`Max redirections reached: ${maxCount}.`)
56
- }
57
-
58
- if (isDisturbed(this.opts.body)) {
59
- // TODO (perf): Consume body?
60
- throw new Error(`Disturbed request cannot be redirected.`)
66
+ if (typeof this.opts.follow === 'function') {
67
+ if (!this.opts.follow(this.location, this.count)) {
68
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
69
+ }
70
+ } else {
71
+ if (this.count >= this.maxCount) {
72
+ throw new Error(`Max redirections reached: ${this.maxCount}.`)
73
+ }
61
74
  }
62
75
 
63
76
  const { origin, pathname, search } = parseURL(
64
- new URL(location, this.opts.origin && new URL(this.opts.path, this.opts.origin)),
77
+ new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)),
65
78
  )
66
79
  const path = search ? `${pathname}${search}` : pathname
67
80
 
68
81
  // Remove headers referring to the original URL.
69
82
  // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
70
83
  // https://tools.ietf.org/html/rfc7231#section-6.4
71
- this.redirectOpts = {
84
+ this.opts = {
72
85
  ...this.opts,
73
86
  headers: cleanRequestHeaders(
74
87
  this.opts.headers,
@@ -77,17 +90,18 @@ class Handler {
77
90
  ),
78
91
  path,
79
92
  origin,
93
+ query: null,
80
94
  }
81
95
 
82
96
  // https://tools.ietf.org/html/rfc7231#section-6.4.4
83
97
  // In case of HTTP 303, always replace method to be either HEAD or GET
84
- if (statusCode === 303 && this.redirectOpts.method !== 'HEAD') {
85
- this.redirectOpts = { ...this.redirectOpts, method: 'GET', body: null }
98
+ if (statusCode === 303 && this.opts.method !== 'HEAD') {
99
+ this.opts = { ...this.opts, method: 'GET', body: null }
86
100
  }
87
101
  }
88
102
 
89
103
  onData(chunk) {
90
- if (this.redirectOpts) {
104
+ if (this.location) {
91
105
  /*
92
106
  https://tools.ietf.org/html/rfc7231#section-6.4
93
107
 
@@ -111,7 +125,7 @@ class Handler {
111
125
  }
112
126
 
113
127
  onComplete(trailers) {
114
- if (this.redirectOpts) {
128
+ if (this.location) {
115
129
  /*
116
130
  https://tools.ietf.org/html/rfc7231#section-6.4
117
131
 
@@ -120,15 +134,10 @@ class Handler {
120
134
 
121
135
  See comment on onData method above for more detailed informations.
122
136
  */
123
- this.dispatch(
124
- this.redirectOpts,
125
- new Handler(this.redirectOpts, {
126
- handler: this.handler,
127
- dispatch: this.dispatch,
128
- count: this.count,
129
- }),
130
- )
131
- this.handler = null
137
+
138
+ this.location = null
139
+
140
+ this.dispatch(this.opts, this)
132
141
  } else {
133
142
  return this.handler.onComplete(trailers)
134
143
  }
@@ -169,5 +178,5 @@ function cleanRequestHeaders(headers, removeContent, unknownOrigin) {
169
178
 
170
179
  export default (dispatch) => (opts, handler) =>
171
180
  opts.follow != null
172
- ? dispatch(opts, new Handler(opts, { handler, dispatch, count: 0 }))
181
+ ? dispatch(opts, new Handler(opts, { handler, dispatch }))
173
182
  : dispatch(opts, handler)
@@ -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),
@@ -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) {
@@ -51,8 +51,8 @@ class Handler {
51
51
  return this.handler.onRequestSent()
52
52
  }
53
53
 
54
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
55
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
54
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
55
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
56
56
  }
57
57
 
58
58
  onData(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,89 @@
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) {
19
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket)
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
+ const stackTraceLimit = Error.stackTraceLimit
67
+ Error.stackTraceLimit = 0
68
+ try {
69
+ if (this.decoder != null) {
70
+ this.body += this.decoder.decode(undefined, { stream: false })
71
+ if (this.contentType === 'application/json') {
72
+ this.body = JSON.parse(this.body)
73
+ }
74
+ }
75
+ this.handler.onError(
76
+ createHttpError(this.statusCode, { headers: this.headers, body: this.body }),
77
+ )
78
+ } finally {
79
+ Error.stackTraceLimit = stackTraceLimit
80
+ }
81
+ } else if (err) {
82
+ this.handler.onError(err)
83
+ } else {
84
+ this.handler.onComplete(rawTrailers)
85
+ }
86
+ }
87
+ }
88
+
89
+ export default (dispatch) => (opts, handler) => dispatch(opts, new Handler(opts, { handler }))
@@ -1,34 +1,27 @@
1
- import { isDisturbed, retry as retryFn } from '../utils.js'
1
+ import assert from 'node:assert'
2
+ import { parseContentRange, isDisturbed, findHeader, retry as retryFn } from '../utils.js'
2
3
 
3
4
  class Handler {
4
5
  constructor(opts, { dispatch, handler }) {
5
6
  this.dispatch = dispatch
6
7
  this.handler = handler
7
8
  this.opts = opts
8
- this.abort = null
9
- this.aborted = false
10
- this.reason = null
11
- this.statusCode = 0
12
-
13
- this.retryCount = 0
14
- this.retryPromise = null
15
9
 
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
- })
10
+ this.hasBody = false
11
+ this.aborted = false
12
+ this.count = 0
13
+ this.pos = 0
14
+ this.end = null
15
+ this.error = null
16
+ this.etag = null
24
17
  }
25
18
 
26
19
  onConnect(abort) {
27
- if (this.aborted) {
28
- abort(this.reason)
29
- } else {
30
- this.abort = abort
31
- }
20
+ this.aborted = false
21
+ return this.handler.onConnect((reason) => {
22
+ this.aborted = true
23
+ abort(reason)
24
+ })
32
25
  }
33
26
 
34
27
  onUpgrade(statusCode, rawHeaders, socket) {
@@ -43,12 +36,81 @@ class Handler {
43
36
  return this.handler.onRequestSent()
44
37
  }
45
38
 
46
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
47
- this.statusCode = statusCode
48
- return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
39
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
40
+ const etag = findHeader(rawHeaders, 'etag')
41
+
42
+ if (this.resume) {
43
+ this.resume = null
44
+
45
+ // TODO (fix): Support other statusCode with skip?
46
+ if (statusCode !== 206) {
47
+ throw this.error
48
+ }
49
+
50
+ // TODO (fix): strict vs weak etag?
51
+ if (this.etag == null || this.etag !== etag) {
52
+ throw this.error
53
+ }
54
+
55
+ const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
56
+ if (!contentRange) {
57
+ throw this.error
58
+ }
59
+
60
+ const { start, size, end = size } = contentRange
61
+
62
+ assert(this.pos === start, 'content-range mismatch')
63
+ assert(this.end == null || this.end === end, 'content-range mismatch')
64
+
65
+ this.resume = resume
66
+ return true
67
+ }
68
+
69
+ if (this.end == null) {
70
+ if (statusCode === 206) {
71
+ const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
72
+ if (!contentRange) {
73
+ return this.handler.onHeaders(
74
+ statusCode,
75
+ rawHeaders,
76
+ () => this.resume(),
77
+ statusMessage,
78
+ headers,
79
+ )
80
+ }
81
+
82
+ const { start, size, end = size } = contentRange
83
+
84
+ this.end = end
85
+ this.pos = Number(start)
86
+ } else {
87
+ const contentLength = findHeader(rawHeaders, 'content-length')
88
+ if (contentLength) {
89
+ this.end = Number(contentLength)
90
+ }
91
+ }
92
+
93
+ assert(Number.isFinite(this.pos))
94
+ assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
95
+ }
96
+
97
+ this.etag = etag
98
+ this.resume = resume
99
+
100
+ return this.handler.onHeaders(
101
+ statusCode,
102
+ rawHeaders,
103
+ () => this.resume(),
104
+ statusMessage,
105
+ headers,
106
+ )
49
107
  }
50
108
 
51
109
  onData(chunk) {
110
+ this.pos += chunk.length
111
+ this.count = 0
112
+ this.hasBody = true
113
+
52
114
  return this.handler.onData(chunk)
53
115
  }
54
116
 
@@ -57,23 +119,34 @@ class Handler {
57
119
  }
58
120
 
59
121
  onError(err) {
60
- if (this.aborted || this.statusCode || isDisturbed(this.opts.body)) {
122
+ if (this.aborted || (this.hasBody && !this.etag) || isDisturbed(this.opts.body)) {
61
123
  return this.handler.onError(err)
62
124
  }
63
125
 
64
- const retryPromise = retryFn(err, this.retryCount++, this.opts)
126
+ const retryPromise = retryFn(err, this.count++, this.opts)
65
127
  if (retryPromise == null) {
66
128
  return this.handler.onError(err)
67
129
  }
68
130
 
131
+ this.error = err
132
+
133
+ if (this.hasBody) {
134
+ this.opts = {
135
+ ...this.opts,
136
+ headers: {
137
+ ...this.opts.headers,
138
+ 'if-match': this.etag,
139
+ range: `bytes=${this.pos}-${this.end ?? ''}`,
140
+ },
141
+ }
142
+ }
143
+
144
+ this.opts.logger?.debug('retrying response body')
145
+
69
146
  retryPromise
70
147
  .then(() => {
71
148
  if (!this.aborted) {
72
- try {
73
- this.dispatch(this.opts, this)
74
- } catch (err2) {
75
- this.handler.onError(new AggregateError([err, err2]))
76
- }
149
+ this.dispatch(this.opts, this)
77
150
  }
78
151
  })
79
152
  .catch((err) => {
@@ -81,12 +154,11 @@ class Handler {
81
154
  this.handler.onError(err)
82
155
  }
83
156
  })
84
-
85
- this.opts.logger?.debug('retrying response')
86
157
  }
87
158
  }
88
159
 
89
- export default (dispatch) => (opts, handler) =>
90
- opts.idempotent && opts.retry && !opts.upgrade
160
+ export default (dispatch) => (opts, handler) => {
161
+ return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
91
162
  ? dispatch(opts, new Handler(opts, { handler, dispatch }))
92
163
  : dispatch(opts, handler)
164
+ }
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 (
@@ -50,10 +65,11 @@ export function findHeader(rawHeaders, name) {
50
65
 
51
66
  for (let i = 0; i < rawHeaders.length; i += 2) {
52
67
  const key = rawHeaders[i + 0]
53
- if (key.length === len && key.toString().toLowerCase() === name) {
68
+ if (key.length === len && toLowerCase(key.toString()) === name) {
54
69
  return rawHeaders[i + 1].toString()
55
70
  }
56
71
  }
72
+
57
73
  return null
58
74
  }
59
75
 
@@ -178,18 +194,24 @@ export function parseOrigin(url) {
178
194
  return url
179
195
  }
180
196
 
181
- export function parseHeaders(rawHeaders, obj = {}) {
182
- for (let i = 0; i < rawHeaders.length; i += 2) {
183
- const key = rawHeaders[i].toString().toLowerCase()
197
+ export function parseHeaders(headers, obj = {}) {
198
+ for (let i = 0; i < headers.length; i += 2) {
199
+ const key = toLowerCase(headers[i].toString())
200
+
184
201
  let val = obj[key]
185
202
  if (!val) {
186
- obj[key] = rawHeaders[i + 1].toString()
203
+ val = headers[i + 1]
204
+ if (typeof val === 'string') {
205
+ obj[key] = val
206
+ } else {
207
+ obj[key] = Array.isArray(val) ? val.map((x) => x.toString()) : val.toString()
208
+ }
187
209
  } else {
188
210
  if (!Array.isArray(val)) {
189
211
  val = [val]
190
212
  obj[key] = val
191
213
  }
192
- val.push(rawHeaders[i + 1].toString())
214
+ val.push(headers[i + 1].toString())
193
215
  }
194
216
  }
195
217
  return obj
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",
@@ -1,162 +0,0 @@
1
- import assert from 'node:assert'
2
- import { parseContentRange, isDisturbed, findHeader, retry as retryFn } from '../utils.js'
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.pos = 0
17
- this.end = null
18
- this.error = null
19
- this.etag = null
20
-
21
- this.handler.onConnect((reason) => {
22
- this.aborted = true
23
- if (this.abort) {
24
- this.abort(reason)
25
- } else {
26
- this.reason = reason
27
- }
28
- })
29
- }
30
-
31
- onConnect(abort) {
32
- if (this.aborted) {
33
- abort(this.reason)
34
- } else {
35
- this.abort = abort
36
- }
37
- }
38
-
39
- onUpgrade(statusCode, rawHeaders, socket) {
40
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
41
- }
42
-
43
- onBodySent(chunk) {
44
- return this.handler.onBodySent(chunk)
45
- }
46
-
47
- onRequestSent() {
48
- return this.handler.onRequestSent()
49
- }
50
-
51
- onHeaders(statusCode, rawHeaders, resume, statusMessage) {
52
- const etag = findHeader(rawHeaders, 'etag')
53
-
54
- if (this.resume) {
55
- this.resume = null
56
-
57
- // TODO (fix): Support other statusCode with skip?
58
- if (statusCode !== 206) {
59
- throw this.error
60
- }
61
-
62
- // TODO (fix): strict vs weak etag?
63
- if (this.etag == null || this.etag !== etag) {
64
- throw this.error
65
- }
66
-
67
- const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
68
- if (!contentRange) {
69
- throw this.error
70
- }
71
-
72
- const { start, size, end = size } = contentRange
73
-
74
- assert(this.pos === start, 'content-range mismatch')
75
- assert(this.end == null || this.end === end, 'content-range mismatch')
76
-
77
- this.resume = resume
78
- return true
79
- }
80
-
81
- if (this.end == null) {
82
- if (statusCode === 206) {
83
- const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
84
- if (!contentRange) {
85
- return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
86
- }
87
-
88
- const { start, size, end = size } = contentRange
89
-
90
- this.end = end
91
- this.pos = Number(start)
92
- } else {
93
- const contentLength = findHeader(rawHeaders, 'content-length')
94
- if (contentLength) {
95
- this.end = Number(contentLength)
96
- }
97
- }
98
-
99
- assert(Number.isFinite(this.pos))
100
- assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
101
- }
102
-
103
- this.etag = etag
104
- this.resume = resume
105
- return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
106
- }
107
-
108
- onData(chunk) {
109
- this.pos += chunk.length
110
- this.count = 0
111
- return this.handler.onData(chunk)
112
- }
113
-
114
- onComplete(rawTrailers) {
115
- return this.handler.onComplete(rawTrailers)
116
- }
117
-
118
- onError(err) {
119
- if (!this.resume || this.aborted || !this.etag || isDisturbed(this.opts.body)) {
120
- return this.handler.onError(err)
121
- }
122
-
123
- const retryPromise = retryFn(err, this.retryCount++, this.opts)
124
- if (retryPromise == null) {
125
- return this.handler.onError(err)
126
- }
127
-
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
- this.error = err
145
- this.opts = {
146
- ...this.opts,
147
- headers: {
148
- ...this.opts.headers,
149
- 'if-match': this.etag,
150
- range: `bytes=${this.pos}-${this.end ?? ''}`,
151
- },
152
- }
153
-
154
- this.opts.logger?.debug('retrying response body')
155
- }
156
- }
157
-
158
- export default (dispatch) => (opts, handler) => {
159
- return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
160
- ? dispatch(opts, new Handler(opts, { handler, dispatch }))
161
- : dispatch(opts, handler)
162
- }
@@ -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)