@nxtedition/nxt-undici 6.2.13 → 6.2.15

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