source: node_modules/undici/lib/handler/RetryHandler.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 8.1 KB
Line 
1const assert = require('assert')
2
3const { kRetryHandlerDefaultRetry } = require('../core/symbols')
4const { RequestRetryError } = require('../core/errors')
5const { isDisturbed, parseHeaders, parseRangeHeader } = require('../core/util')
6
7function calculateRetryAfterHeader (retryAfter) {
8 const current = Date.now()
9 const diff = new Date(retryAfter).getTime() - current
10
11 return diff
12}
13
14class RetryHandler {
15 constructor (opts, handlers) {
16 const { retryOptions, ...dispatchOpts } = opts
17 const {
18 // Retry scoped
19 retry: retryFn,
20 maxRetries,
21 maxTimeout,
22 minTimeout,
23 timeoutFactor,
24 // Response scoped
25 methods,
26 errorCodes,
27 retryAfter,
28 statusCodes
29 } = retryOptions ?? {}
30
31 this.dispatch = handlers.dispatch
32 this.handler = handlers.handler
33 this.opts = dispatchOpts
34 this.abort = null
35 this.aborted = false
36 this.retryOpts = {
37 retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
38 retryAfter: retryAfter ?? true,
39 maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
40 timeout: minTimeout ?? 500, // .5s
41 timeoutFactor: timeoutFactor ?? 2,
42 maxRetries: maxRetries ?? 5,
43 // What errors we should retry
44 methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
45 // Indicates which errors to retry
46 statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
47 // List of errors to retry
48 errorCodes: errorCodes ?? [
49 'ECONNRESET',
50 'ECONNREFUSED',
51 'ENOTFOUND',
52 'ENETDOWN',
53 'ENETUNREACH',
54 'EHOSTDOWN',
55 'EHOSTUNREACH',
56 'EPIPE'
57 ]
58 }
59
60 this.retryCount = 0
61 this.start = 0
62 this.end = null
63 this.etag = null
64 this.resume = null
65
66 // Handle possible onConnect duplication
67 this.handler.onConnect(reason => {
68 this.aborted = true
69 if (this.abort) {
70 this.abort(reason)
71 } else {
72 this.reason = reason
73 }
74 })
75 }
76
77 onRequestSent () {
78 if (this.handler.onRequestSent) {
79 this.handler.onRequestSent()
80 }
81 }
82
83 onUpgrade (statusCode, headers, socket) {
84 if (this.handler.onUpgrade) {
85 this.handler.onUpgrade(statusCode, headers, socket)
86 }
87 }
88
89 onConnect (abort) {
90 if (this.aborted) {
91 abort(this.reason)
92 } else {
93 this.abort = abort
94 }
95 }
96
97 onBodySent (chunk) {
98 if (this.handler.onBodySent) return this.handler.onBodySent(chunk)
99 }
100
101 static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
102 const { statusCode, code, headers } = err
103 const { method, retryOptions } = opts
104 const {
105 maxRetries,
106 timeout,
107 maxTimeout,
108 timeoutFactor,
109 statusCodes,
110 errorCodes,
111 methods
112 } = retryOptions
113 let { counter, currentTimeout } = state
114
115 currentTimeout =
116 currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout
117
118 // Any code that is not a Undici's originated and allowed to retry
119 if (
120 code &&
121 code !== 'UND_ERR_REQ_RETRY' &&
122 code !== 'UND_ERR_SOCKET' &&
123 !errorCodes.includes(code)
124 ) {
125 cb(err)
126 return
127 }
128
129 // If a set of method are provided and the current method is not in the list
130 if (Array.isArray(methods) && !methods.includes(method)) {
131 cb(err)
132 return
133 }
134
135 // If a set of status code are provided and the current status code is not in the list
136 if (
137 statusCode != null &&
138 Array.isArray(statusCodes) &&
139 !statusCodes.includes(statusCode)
140 ) {
141 cb(err)
142 return
143 }
144
145 // If we reached the max number of retries
146 if (counter > maxRetries) {
147 cb(err)
148 return
149 }
150
151 let retryAfterHeader = headers != null && headers['retry-after']
152 if (retryAfterHeader) {
153 retryAfterHeader = Number(retryAfterHeader)
154 retryAfterHeader = isNaN(retryAfterHeader)
155 ? calculateRetryAfterHeader(retryAfterHeader)
156 : retryAfterHeader * 1e3 // Retry-After is in seconds
157 }
158
159 const retryTimeout =
160 retryAfterHeader > 0
161 ? Math.min(retryAfterHeader, maxTimeout)
162 : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout)
163
164 state.currentTimeout = retryTimeout
165
166 setTimeout(() => cb(null), retryTimeout)
167 }
168
169 onHeaders (statusCode, rawHeaders, resume, statusMessage) {
170 const headers = parseHeaders(rawHeaders)
171
172 this.retryCount += 1
173
174 if (statusCode >= 300) {
175 this.abort(
176 new RequestRetryError('Request failed', statusCode, {
177 headers,
178 count: this.retryCount
179 })
180 )
181 return false
182 }
183
184 // Checkpoint for resume from where we left it
185 if (this.resume != null) {
186 this.resume = null
187
188 if (statusCode !== 206) {
189 return true
190 }
191
192 const contentRange = parseRangeHeader(headers['content-range'])
193 // If no content range
194 if (!contentRange) {
195 this.abort(
196 new RequestRetryError('Content-Range mismatch', statusCode, {
197 headers,
198 count: this.retryCount
199 })
200 )
201 return false
202 }
203
204 // Let's start with a weak etag check
205 if (this.etag != null && this.etag !== headers.etag) {
206 this.abort(
207 new RequestRetryError('ETag mismatch', statusCode, {
208 headers,
209 count: this.retryCount
210 })
211 )
212 return false
213 }
214
215 const { start, size, end = size } = contentRange
216
217 assert(this.start === start, 'content-range mismatch')
218 assert(this.end == null || this.end === end, 'content-range mismatch')
219
220 this.resume = resume
221 return true
222 }
223
224 if (this.end == null) {
225 if (statusCode === 206) {
226 // First time we receive 206
227 const range = parseRangeHeader(headers['content-range'])
228
229 if (range == null) {
230 return this.handler.onHeaders(
231 statusCode,
232 rawHeaders,
233 resume,
234 statusMessage
235 )
236 }
237
238 const { start, size, end = size } = range
239
240 assert(
241 start != null && Number.isFinite(start) && this.start !== start,
242 'content-range mismatch'
243 )
244 assert(Number.isFinite(start))
245 assert(
246 end != null && Number.isFinite(end) && this.end !== end,
247 'invalid content-length'
248 )
249
250 this.start = start
251 this.end = end
252 }
253
254 // We make our best to checkpoint the body for further range headers
255 if (this.end == null) {
256 const contentLength = headers['content-length']
257 this.end = contentLength != null ? Number(contentLength) : null
258 }
259
260 assert(Number.isFinite(this.start))
261 assert(
262 this.end == null || Number.isFinite(this.end),
263 'invalid content-length'
264 )
265
266 this.resume = resume
267 this.etag = headers.etag != null ? headers.etag : null
268
269 return this.handler.onHeaders(
270 statusCode,
271 rawHeaders,
272 resume,
273 statusMessage
274 )
275 }
276
277 const err = new RequestRetryError('Request failed', statusCode, {
278 headers,
279 count: this.retryCount
280 })
281
282 this.abort(err)
283
284 return false
285 }
286
287 onData (chunk) {
288 this.start += chunk.length
289
290 return this.handler.onData(chunk)
291 }
292
293 onComplete (rawTrailers) {
294 this.retryCount = 0
295 return this.handler.onComplete(rawTrailers)
296 }
297
298 onError (err) {
299 if (this.aborted || isDisturbed(this.opts.body)) {
300 return this.handler.onError(err)
301 }
302
303 this.retryOpts.retry(
304 err,
305 {
306 state: { counter: this.retryCount++, currentTimeout: this.retryAfter },
307 opts: { retryOptions: this.retryOpts, ...this.opts }
308 },
309 onRetry.bind(this)
310 )
311
312 function onRetry (err) {
313 if (err != null || this.aborted || isDisturbed(this.opts.body)) {
314 return this.handler.onError(err)
315 }
316
317 if (this.start !== 0) {
318 this.opts = {
319 ...this.opts,
320 headers: {
321 ...this.opts.headers,
322 range: `bytes=${this.start}-${this.end ?? ''}`
323 }
324 }
325 }
326
327 try {
328 this.dispatch(this.opts, this)
329 } catch (err) {
330 this.handler.onError(err)
331 }
332 }
333 }
334}
335
336module.exports = RetryHandler
Note: See TracBrowser for help on using the repository browser.