@nxtedition/nxt-undici 2.0.16 → 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
@@ -84,6 +84,7 @@ class Readable extends stream.Readable {
84
84
  async text() {
85
85
  const dec = new TextDecoder()
86
86
  let str = ''
87
+
87
88
  for await (const chunk of this) {
88
89
  if (typeof chunk === 'string') {
89
90
  str += chunk
@@ -91,6 +92,7 @@ class Readable extends stream.Readable {
91
92
  str += dec.decode(chunk, { stream: true })
92
93
  }
93
94
  }
95
+
94
96
  // Flush the streaming TextDecoder so that any pending
95
97
  // incomplete multibyte characters are handled.
96
98
  str += dec.decode(undefined, { stream: false })
@@ -142,6 +144,7 @@ const dispatchers = {
142
144
  log: (await import('./interceptor/log.js')).default,
143
145
  redirect: (await import('./interceptor/redirect.js')).default,
144
146
  responseRetry: (await import('./interceptor/response-retry.js')).default,
147
+ responseRetryBody: (await import('./interceptor/response-retry-body.js')).default,
145
148
  proxy: (await import('./interceptor/proxy.js')).default,
146
149
  cache: (await import('./interceptor/cache.js')).default,
147
150
  requestId: (await import('./interceptor/request-id.js')).default,
@@ -232,10 +235,11 @@ export async function request(url, opts) {
232
235
  dispatch = dispatchers.log(dispatch)
233
236
  dispatch = dispatchers.requestId(dispatch)
234
237
  dispatch = dispatchers.responseRetry(dispatch)
238
+ dispatch = dispatchers.responseRetryBody(dispatch)
235
239
  dispatch = dispatchers.responseContent(dispatch)
236
240
  dispatch = dispatchers.requestContent(dispatch)
237
- dispatch = dispatchers.redirect(dispatch)
238
241
  dispatch = dispatchers.cache(dispatch)
242
+ dispatch = dispatchers.redirect(dispatch)
239
243
  dispatch = dispatchers.proxy(dispatch)
240
244
  dispatch = dispatchers.requestBody(dispatch)
241
245
  dispatcherCache.set(dispatcher, dispatch)
@@ -244,7 +248,7 @@ export async function request(url, opts) {
244
248
  return await new Promise((resolve, reject) =>
245
249
  dispatch(
246
250
  {
247
- id: opts.id ?? headers?.['request-id'] ?? headers?.['Request-Id'] ?? genReqId(),
251
+ id: opts.id ?? findHeader(headers, 'request-id') ?? genReqId(),
248
252
  url,
249
253
  method,
250
254
  body: opts.body,
@@ -289,9 +293,7 @@ export async function request(url, opts) {
289
293
  }
290
294
  }
291
295
  },
292
- onUpgrade(statusCode, rawHeaders, socket) {
293
- const headers = parseHeaders(rawHeaders)
294
-
296
+ onUpgrade(statusCode, rawHeaders, socket, headers = parseHeaders(rawHeaders)) {
295
297
  if (statusCode !== 101) {
296
298
  this.abort(createError(statusCode, { headers }))
297
299
  } else {
@@ -332,9 +334,6 @@ export async function request(url, opts) {
332
334
  return this.body.push(chunk)
333
335
  },
334
336
  onComplete() {
335
- this.signal?.removeEventListener('abort', this.onAbort)
336
- this.signal = null
337
-
338
337
  this.body.push(null)
339
338
  },
340
339
  onError(err) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -15,8 +15,8 @@ class Handler {
15
15
  return this.handler.onConnect(abort)
16
16
  }
17
17
 
18
- onUpgrade(statusCode, rawHeaders, socket) {
19
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
18
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
19
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
20
20
  }
21
21
 
22
22
  onBodySent(chunk) {
@@ -63,26 +63,33 @@ class Handler {
63
63
 
64
64
  onFinally(err, rawTrailers) {
65
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
+
66
73
  const stackTraceLimit = Error.stackTraceLimit
67
74
  Error.stackTraceLimit = 0
68
75
  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
76
  this.handler.onError(
76
77
  createHttpError(this.statusCode, { headers: this.headers, body: this.body }),
77
78
  )
78
79
  } finally {
79
80
  Error.stackTraceLimit = stackTraceLimit
80
81
  }
82
+
83
+ this.decoder = null
84
+ this.contentType = null
85
+ this.body = null
81
86
  } else if (err) {
82
87
  this.handler.onError(err)
83
88
  } else {
84
89
  this.handler.onComplete(rawTrailers)
85
90
  }
91
+
92
+ this.handler = null
86
93
  }
87
94
  }
88
95
 
@@ -0,0 +1,169 @@
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
+
10
+ this.count = 0
11
+ this.pos = 0
12
+ this.end = null
13
+ this.error = null
14
+ this.etag = null
15
+
16
+ this.reason = null
17
+ this.aborted = false
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
+ })
27
+ }
28
+
29
+ onConnect(abort) {
30
+ if (this.aborted) {
31
+ abort(this.reason)
32
+ } else {
33
+ this.abort = abort
34
+ }
35
+ }
36
+
37
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
38
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
39
+ }
40
+
41
+ onBodySent(chunk) {
42
+ return this.handler.onBodySent(chunk)
43
+ }
44
+
45
+ onRequestSent() {
46
+ return this.handler.onRequestSent()
47
+ }
48
+
49
+ onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
50
+ const etag = findHeader(rawHeaders, 'etag')
51
+
52
+ if (this.resume) {
53
+ this.resume = null
54
+
55
+ // TODO (fix): Support other statusCode with skip?
56
+ if (statusCode !== 206) {
57
+ throw this.error
58
+ }
59
+
60
+ // TODO (fix): strict vs weak etag?
61
+ if (this.etag == null || this.etag !== etag) {
62
+ throw this.error
63
+ }
64
+
65
+ const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
66
+ if (!contentRange) {
67
+ throw this.error
68
+ }
69
+
70
+ const { start, size, end = size } = contentRange
71
+
72
+ assert(this.pos === start, 'content-range mismatch')
73
+ assert(this.end == null || this.end === end, 'content-range mismatch')
74
+
75
+ this.resume = resume
76
+ return true
77
+ }
78
+
79
+ if (this.end == null) {
80
+ if (statusCode === 206) {
81
+ const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
82
+ if (!contentRange) {
83
+ return this.handler.onHeaders(
84
+ statusCode,
85
+ rawHeaders,
86
+ () => this.resume(),
87
+ statusMessage,
88
+ headers,
89
+ )
90
+ }
91
+
92
+ const { start, size, end = size } = contentRange
93
+
94
+ this.end = end
95
+ this.pos = Number(start)
96
+ } else {
97
+ const contentLength = findHeader(rawHeaders, 'content-length')
98
+ if (contentLength) {
99
+ this.end = Number(contentLength)
100
+ }
101
+ }
102
+
103
+ assert(Number.isFinite(this.pos))
104
+ assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
105
+ }
106
+
107
+ this.etag = etag
108
+ this.resume = resume
109
+
110
+ return this.handler.onHeaders(
111
+ statusCode,
112
+ rawHeaders,
113
+ () => this.resume(),
114
+ statusMessage,
115
+ headers,
116
+ )
117
+ }
118
+
119
+ onData(chunk) {
120
+ this.pos += chunk.length
121
+ this.count = 0
122
+ return this.handler.onData(chunk)
123
+ }
124
+
125
+ onComplete(rawTrailers) {
126
+ return this.handler.onComplete(rawTrailers)
127
+ }
128
+
129
+ onError(err) {
130
+ if (this.aborted || !this.etag || isDisturbed(this.opts.body)) {
131
+ return this.handler.onError(err)
132
+ }
133
+
134
+ const retryPromise = retryFn(err, this.count++, this.opts)
135
+ if (retryPromise == null) {
136
+ return this.handler.onError(err)
137
+ }
138
+
139
+ this.error = err
140
+ this.opts = {
141
+ ...this.opts,
142
+ headers: {
143
+ ...this.opts.headers,
144
+ 'if-match': this.etag,
145
+ range: `bytes=${this.pos}-${this.end ?? ''}`,
146
+ },
147
+ }
148
+
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
+ })
162
+ }
163
+ }
164
+
165
+ export default (dispatch) => (opts, handler) => {
166
+ return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
167
+ ? dispatch(opts, new Handler(opts, { handler, dispatch }))
168
+ : dispatch(opts, handler)
169
+ }
@@ -1,5 +1,4 @@
1
- import assert from 'node:assert'
2
- import { parseContentRange, isDisturbed, findHeader, retry as retryFn } from '../utils.js'
1
+ import { isDisturbed, retry as retryFn } from '../utils.js'
3
2
 
4
3
  class Handler {
5
4
  constructor(opts, { dispatch, handler }) {
@@ -8,24 +7,31 @@ class Handler {
8
7
  this.opts = opts
9
8
 
10
9
  this.hasBody = false
11
- this.aborted = false
12
10
  this.count = 0
13
- this.pos = 0
14
- this.end = null
15
- this.error = null
16
- this.etag = null
17
- }
18
11
 
19
- onConnect(abort) {
12
+ this.reason = null
20
13
  this.aborted = false
21
- return this.handler.onConnect((reason) => {
14
+
15
+ this.handler.onConnect((reason) => {
22
16
  this.aborted = true
23
- abort(reason)
17
+ if (this.abort) {
18
+ this.abort(reason)
19
+ } else {
20
+ this.reason = reason
21
+ }
24
22
  })
25
23
  }
26
24
 
27
- onUpgrade(statusCode, rawHeaders, socket) {
28
- return this.handler.onUpgrade(statusCode, rawHeaders, socket)
25
+ onConnect(abort) {
26
+ if (this.aborted) {
27
+ abort(this.reason)
28
+ } else {
29
+ this.abort = abort
30
+ }
31
+ }
32
+
33
+ onUpgrade(statusCode, rawHeaders, socket, headers) {
34
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
29
35
  }
30
36
 
31
37
  onBodySent(chunk) {
@@ -37,80 +43,11 @@ class Handler {
37
43
  }
38
44
 
39
45
  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
- )
46
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
107
47
  }
108
48
 
109
49
  onData(chunk) {
110
- this.pos += chunk.length
111
- this.count = 0
112
50
  this.hasBody = true
113
-
114
51
  return this.handler.onData(chunk)
115
52
  }
116
53
 
@@ -119,7 +56,7 @@ class Handler {
119
56
  }
120
57
 
121
58
  onError(err) {
122
- if (this.aborted || (this.hasBody && !this.etag) || isDisturbed(this.opts.body)) {
59
+ if (this.aborted || this.hasBody || isDisturbed(this.opts.body)) {
123
60
  return this.handler.onError(err)
124
61
  }
125
62
 
@@ -128,20 +65,7 @@ class Handler {
128
65
  return this.handler.onError(err)
129
66
  }
130
67
 
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')
68
+ this.opts.logger?.debug('retrying response')
145
69
 
146
70
  retryPromise
147
71
  .then(() => {
@@ -158,7 +82,7 @@ class Handler {
158
82
  }
159
83
 
160
84
  export default (dispatch) => (opts, handler) => {
161
- return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
85
+ return opts.retry
162
86
  ? dispatch(opts, new Handler(opts, { handler, dispatch }))
163
87
  : dispatch(opts, handler)
164
88
  }
package/lib/utils.js CHANGED
@@ -60,13 +60,28 @@ export function parseContentRange(range) {
60
60
  return { start, end: end ? end + 1 : size, size }
61
61
  }
62
62
 
63
- export function findHeader(rawHeaders, name) {
63
+ export function findHeader(headers, name) {
64
64
  const len = name.length
65
65
 
66
- for (let i = 0; i < rawHeaders.length; i += 2) {
67
- const key = rawHeaders[i + 0]
68
- if (key.length === len && toLowerCase(key.toString()) === name) {
69
- 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
+ }
70
85
  }
71
86
  }
72
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",