@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.
- package/lib/interceptor/response-retry.js +138 -133
- package/lib/utils.js +7 -1
- package/package.json +1 -1
|
@@ -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
|
-
#
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
this.#maybeError(null)
|
|
152
|
+
return null
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const contentRange = parseRangeHeader(headers['content-range'])
|
|
156
156
|
if (!contentRange) {
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
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.#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 (
|
|
211
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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((
|
|
274
|
+
.then((shouldRetry) => {
|
|
250
275
|
if (this.#aborted) {
|
|
251
|
-
this.#
|
|
252
|
-
} else if (isDisturbed(this.#opts.body)) {
|
|
253
|
-
this.#
|
|
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
|
-
|
|
264
|
-
|
|
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.#
|
|
295
|
+
this.#retryError = err
|
|
296
|
+
|
|
275
297
|
this.#dispatch(this.#opts, this)
|
|
276
298
|
}
|
|
277
299
|
})
|
|
278
300
|
.catch((err) => {
|
|
279
|
-
|
|
280
|
-
this.#onError(err)
|
|
281
|
-
}
|
|
301
|
+
this.#maybeError(err)
|
|
282
302
|
})
|
|
283
303
|
}
|
|
284
304
|
|
|
285
|
-
#
|
|
286
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
if (typeof retryOpts === 'number') {
|
|
313
|
+
retryOpts = { count: retryOpts }
|
|
314
|
+
}
|
|
319
315
|
|
|
320
|
-
|
|
316
|
+
const retryMax = retryOpts?.count ?? 8
|
|
321
317
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
318
|
+
if (retryCount > retryMax) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
325
321
|
|
|
326
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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 (
|
|
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 {
|