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