@nxtedition/nxt-undici 6.2.13 → 6.2.14

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.
@@ -10,14 +10,14 @@ class Handler extends DecoratorHandler {
10
10
  #opts
11
11
 
12
12
  #retryCount = 0
13
+ #retryError = null
13
14
  #headersSent = false
14
15
  #errorSent = false
15
16
 
16
17
  #statusCode = 0
17
18
  #headers
18
19
  #trailers
19
- #body = ''
20
- #decoder
20
+ #body
21
21
 
22
22
  #abort
23
23
  #aborted = false
@@ -27,7 +27,6 @@ class Handler extends DecoratorHandler {
27
27
  #pos
28
28
  #end
29
29
  #etag
30
- #error
31
30
 
32
31
  constructor(opts, { handler, dispatch }) {
33
32
  super(handler)
@@ -48,15 +47,13 @@ class Handler extends DecoratorHandler {
48
47
  onConnect(abort) {
49
48
  this.#statusCode = 0
50
49
  this.#headers = null
50
+ this.#body = null
51
51
  this.#trailers = null
52
- this.#body = ''
53
- this.#decoder = null
54
52
 
55
53
  if (!this.#headersSent) {
56
54
  this.#pos = null
57
55
  this.#end = null
58
56
  this.#etag = null
59
- this.#error = null
60
57
  this.#resume = null
61
58
 
62
59
  super.onConnect((reason) => {
@@ -82,25 +79,28 @@ class Handler extends DecoratorHandler {
82
79
  this.#statusCode = statusCode
83
80
  this.#headers = headers
84
81
 
85
- if (this.#error == null) {
82
+ if (!this.#headersSent) {
86
83
  assert(this.#etag == null)
87
84
  assert(this.#pos == null)
88
85
  assert(this.#end == null)
89
86
  assert(this.#headersSent === false)
90
87
 
91
88
  if (headers.trailer) {
92
- return this.#onHeaders(statusCode, headers, resume)
89
+ this.#headersSent = true
90
+ return super.onHeaders(statusCode, headers, resume)
93
91
  }
94
92
 
95
93
  const contentLength = headers['content-length'] ? Number(headers['content-length']) : null
96
94
  if (contentLength != null && !Number.isFinite(contentLength)) {
97
- return this.#onHeaders(statusCode, headers, resume)
95
+ this.#headersSent = true
96
+ return super.onHeaders(statusCode, headers, resume)
98
97
  }
99
98
 
100
99
  if (statusCode === 206) {
101
100
  const range = parseRangeHeader(headers['content-range'])
102
101
  if (!range) {
103
- return this.#onHeaders(statusCode, headers, resume)
102
+ this.#headersSent = true
103
+ return super.onHeaders(statusCode, headers, resume)
104
104
  }
105
105
 
106
106
  const { start, size, end = size } = range
@@ -120,16 +120,11 @@ class Handler extends DecoratorHandler {
120
120
  this.#end = contentLength
121
121
  this.#etag = headers.etag
122
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
- }
123
+ this.#body = []
130
124
  return true
131
125
  } else {
132
- return this.#onHeaders(statusCode, headers, resume)
126
+ this.#headersSent = true
127
+ return super.onHeaders(statusCode, headers, resume)
133
128
  }
134
129
 
135
130
  // Weak etags are not useful for comparison nor cache
@@ -144,17 +139,20 @@ class Handler extends DecoratorHandler {
144
139
 
145
140
  this.#resume = resume
146
141
 
147
- return this.#onHeaders(statusCode, headers, () => this.#resume?.())
142
+ this.#headersSent = true
143
+ return super.onHeaders(statusCode, headers, () => this.#resume?.())
148
144
  } else if (statusCode === 206 || (this.#pos === 0 && statusCode === 200)) {
149
145
  assert(this.#etag != null || !this.#pos)
150
146
 
151
147
  if (this.#pos > 0 && this.#etag !== headers.etag) {
152
- throw this.#error
148
+ this.#maybeError(null)
149
+ return null
153
150
  }
154
151
 
155
152
  const contentRange = parseRangeHeader(headers['content-range'])
156
153
  if (!contentRange) {
157
- throw this.#error
154
+ this.#maybeError(null)
155
+ return null
158
156
  }
159
157
 
160
158
  const { start, size, end = size } = contentRange
@@ -166,7 +164,7 @@ class Handler extends DecoratorHandler {
166
164
  // TODO (fix): What if we were paused before the error?
167
165
  return true
168
166
  } else {
169
- throw this.#error
167
+ this.#maybeError(this.#retryError)
170
168
  }
171
169
  }
172
170
 
@@ -179,7 +177,7 @@ class Handler extends DecoratorHandler {
179
177
  return super.onData(chunk)
180
178
  }
181
179
 
182
- this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
180
+ this.#body?.push(chunk)
183
181
  }
184
182
 
185
183
  onComplete(trailers) {
@@ -189,7 +187,6 @@ class Handler extends DecoratorHandler {
189
187
  return super.onComplete(trailers)
190
188
  }
191
189
 
192
- this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
193
190
  this.#maybeRetry(null)
194
191
  }
195
192
 
@@ -197,20 +194,38 @@ class Handler extends DecoratorHandler {
197
194
  this.#maybeRetry(err)
198
195
  }
199
196
 
197
+ #maybeAbort(err) {
198
+ if (this.#abort && !this.#aborted) {
199
+ this.#aborted = true
200
+ this.#abort(err)
201
+ }
202
+ }
203
+
200
204
  #maybeError(err) {
201
205
  if (err) {
202
- this.#onError(err)
203
- } else {
204
- assert(this.#aborted === false, 'unexpected aborted')
205
- assert(this.#statusCode, 'unexpected statusCode')
206
- assert(this.#headers, 'unexpected headers')
207
-
208
- this.#onHeaders(this.#statusCode, this.#headers, noop)
206
+ if (!this.#errorSent) {
207
+ this.#errorSent = true
208
+ super.onError(err)
209
+ }
210
+ } else if (!this.#headersSent) {
211
+ super.onHeaders(this.#statusCode, this.#headers, noop)
212
+ if (this.#aborted) {
213
+ return
214
+ }
209
215
 
210
- if (!this.#aborted) {
211
- super.onComplete(this.#trailers)
216
+ if (this.#body) {
217
+ for (const chunk of this.#body) {
218
+ super.onData(chunk)
219
+ if (this.#aborted) {
220
+ return
221
+ }
222
+ }
212
223
  }
224
+
225
+ super.onComplete(this.#trailers)
213
226
  }
227
+
228
+ this.#maybeAbort(err)
214
229
  }
215
230
 
216
231
  #maybeRetry(err) {
@@ -219,23 +234,23 @@ class Handler extends DecoratorHandler {
219
234
  return
220
235
  }
221
236
 
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
- })
230
-
231
237
  let retryPromise
232
238
  try {
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)
239
+ if (typeof this.#opts.retry === 'function') {
240
+ retryPromise = this.#opts.retry(
241
+ decorateError(err, this.#opts, {
242
+ statusCode: this.#statusCode,
243
+ headers: this.#headers,
244
+ trailers: this.#trailers,
245
+ body: this.#body,
246
+ }),
247
+ this.#retryCount,
248
+ this.#opts,
249
+ () => this.#retryFn(err, this.#retryCount, this.#opts),
250
+ )
251
+ } else {
252
+ retryPromise = this.#retryFn(err, this.#retryCount, this.#opts)
253
+ }
239
254
  } catch (err) {
240
255
  retryPromise = Promise.reject(err)
241
256
  }
@@ -246,116 +261,96 @@ class Handler extends DecoratorHandler {
246
261
  }
247
262
 
248
263
  retryPromise
249
- .then((opts) => {
264
+ .then((shouldRetry) => {
250
265
  if (this.#aborted) {
251
- this.#onError(this.#reason)
252
- } else if (isDisturbed(this.#opts.body)) {
253
- this.#onError(this.#error)
266
+ this.#maybeError(this.#reason)
267
+ } else if (shouldRetry === false || isDisturbed(this.#opts.body)) {
268
+ this.#maybeError(err)
254
269
  } else if (!this.#headersSent) {
255
- this.#retryCount++
256
270
  this.#opts.logger?.debug({ err, retryCount: this.#retryCount }, 'retry response headers')
271
+
272
+ this.#retryCount++
273
+ this.#retryError = err
274
+
257
275
  this.#dispatch(this.#opts, this)
258
276
  } else {
259
277
  assert(Number.isFinite(this.#pos))
260
278
  assert(this.#end == null || (Number.isFinite(this.#end) && this.#end > 0))
261
279
 
262
- this.#opts = {
263
- ...this.#opts,
264
- ...opts,
265
- headers: {
266
- ...this.#opts.headers,
267
- ...opts?.headers,
268
- 'if-match': this.#etag,
269
- range: `bytes=${this.#pos}-${this.#end ? this.#end - 1 : ''}`,
270
- },
271
- }
280
+ this.#opts.headers['if-match'] = this.#etag
281
+ this.#opts.headers.range = `bytes=${this.#pos}-${this.#end ? this.#end - 1 : ''}`
282
+ this.#opts.logger?.debug({ err, retryCount: this.#retryCount }, 'retry response body')
272
283
 
273
284
  this.#retryCount++
274
- this.#opts.logger?.debug({ err, retryCount: this.#retryCount }, 'retry response body')
285
+ this.#retryError = err
286
+
275
287
  this.#dispatch(this.#opts, this)
276
288
  }
277
289
  })
278
290
  .catch((err) => {
279
- if (!this.#errorSent) {
280
- this.#onError(err)
281
- }
291
+ this.#maybeError(err)
282
292
  })
283
293
  }
284
294
 
285
- #onError(err) {
286
- assert(!this.#errorSent)
287
- this.#errorSent = true
288
- super.onError(err)
289
- }
290
-
291
- #onHeaders(statusCode, headers, resume) {
292
- assert(!this.#headersSent)
293
- this.#headersSent = true
294
- return super.onHeaders(statusCode, headers, resume)
295
- }
296
- }
297
-
298
- export default () => (dispatch) => (opts, handler) =>
299
- opts.retry !== false &&
300
- !opts.upgrade &&
301
- (opts.method === 'HEAD' ||
302
- opts.method === 'GET' ||
303
- opts.method === 'PUT' ||
304
- opts.method === 'PATCH' ||
305
- opts.idempotent)
306
- ? dispatch(opts, new Handler(opts, { handler, dispatch }))
307
- : dispatch(opts, handler)
308
-
309
- async function retryFn(err, retryCount, opts) {
310
- let retryOpts = opts?.retry
295
+ async #retryFn(err, retryCount, opts) {
296
+ let retryOpts = opts?.retry
311
297
 
312
- if (!retryOpts) {
313
- throw err
314
- }
298
+ if (!retryOpts) {
299
+ return false
300
+ }
315
301
 
316
- if (typeof retryOpts === 'number') {
317
- retryOpts = { count: retryOpts }
318
- }
302
+ if (typeof retryOpts === 'number') {
303
+ retryOpts = { count: retryOpts }
304
+ }
319
305
 
320
- const retryMax = retryOpts?.count ?? 8
306
+ const retryMax = retryOpts?.count ?? 8
321
307
 
322
- if (retryCount > retryMax) {
323
- throw err
324
- }
308
+ if (retryCount > retryMax) {
309
+ return false
310
+ }
325
311
 
326
- const statusCode = err.statusCode ?? err.status ?? err.$metadata?.httpStatusCode ?? null
312
+ const statusCode =
313
+ err?.statusCode ?? err?.status ?? err?.$metadata?.httpStatusCode ?? this.#statusCode
314
+ const headers = err?.headers ?? this.#headers
315
+
316
+ if (statusCode && [420, 429, 502, 503, 504].includes(statusCode)) {
317
+ const retryAfter = headers?.['retry-after'] ? Number(headers['retry-after']) * 1e3 : null
318
+ const delay =
319
+ retryAfter != null && Number.isFinite(retryAfter)
320
+ ? retryAfter
321
+ : Math.min(10e3, retryCount * 1e3)
322
+ return tp.setTimeout(delay, true, { signal: opts.signal })
323
+ }
327
324
 
328
- if (statusCode && [420, 429, 502, 503, 504].includes(statusCode)) {
329
- let retryAfter = err.headers?.['retry-after'] ? err.headers['retry-after'] * 1e3 : null
330
- retryAfter = Number.isFinite(retryAfter) ? retryAfter : Math.min(10e3, retryCount * 1e3)
331
- if (retryAfter != null && Number.isFinite(retryAfter)) {
332
- return tp.setTimeout(retryAfter, undefined, { signal: opts.signal })
333
- } else {
334
- return null
325
+ if (
326
+ err?.code &&
327
+ [
328
+ 'ECONNRESET',
329
+ 'ECONNREFUSED',
330
+ 'ENOTFOUND',
331
+ 'ENETDOWN',
332
+ 'ENETUNREACH',
333
+ 'EHOSTDOWN',
334
+ 'EHOSTUNREACH',
335
+ 'EPIPE',
336
+ 'ENODATA',
337
+ 'UND_ERR_CONNECT_TIMEOUT',
338
+ ].includes(err.code)
339
+ ) {
340
+ return tp.setTimeout(Math.min(10e3, retryCount * 1e3), true, { signal: opts.signal })
335
341
  }
336
- }
337
342
 
338
- if (
339
- err.code &&
340
- [
341
- 'ECONNRESET',
342
- 'ECONNREFUSED',
343
- 'ENOTFOUND',
344
- 'ENETDOWN',
345
- 'ENETUNREACH',
346
- 'EHOSTDOWN',
347
- 'EHOSTUNREACH',
348
- 'EPIPE',
349
- 'ENODATA',
350
- 'UND_ERR_CONNECT_TIMEOUT',
351
- ].includes(err.code)
352
- ) {
353
- return tp.setTimeout(Math.min(10e3, retryCount * 1e3), undefined, { signal: opts.signal })
354
- }
343
+ if (err?.message && ['other side closed'].includes(err.message)) {
344
+ return tp.setTimeout(Math.min(10e3, retryCount * 1e3), true, { signal: opts.signal })
345
+ }
355
346
 
356
- if (err.message && ['other side closed'].includes(err.message)) {
357
- return tp.setTimeout(Math.min(10e3, retryCount * 1e3), undefined, { signal: opts.signal })
347
+ return false
358
348
  }
359
-
360
- throw err
361
349
  }
350
+
351
+ export default () => (dispatch) => (opts, handler) =>
352
+ opts.retry !== false &&
353
+ !opts.upgrade &&
354
+ (/^(HEAD|GET|PUT|PATCH)$/.test(opts.method) || opts.idempotent)
355
+ ? dispatch(opts, new Handler(opts, { handler, dispatch }))
356
+ : dispatch(opts, handler)
package/lib/utils.js CHANGED
@@ -385,7 +385,13 @@ export function decorateError(err, opts, { statusCode, headers, trailers, body }
385
385
  typeof opts?.body !== 'string' || opts.body.length > 1024 ? undefined : opts.body,
386
386
  }
387
387
 
388
- if (headers?.['content-type']?.startsWith('application/json') && typeof body === 'string') {
388
+ if (Array.isArray(body) && body.every((x) => Buffer.isBuffer(x))) {
389
+ body = Buffer.concat(body).toString()
390
+ } else if (typeof body !== 'string') {
391
+ body = null
392
+ }
393
+
394
+ if (typeof body === 'string' && headers?.['content-type']?.startsWith('application/json')) {
389
395
  try {
390
396
  body = JSON.parse(body)
391
397
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "6.2.13",
3
+ "version": "6.2.14",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",