11cb0ef41Sopenharmony_ciconst assert = require('assert') 21cb0ef41Sopenharmony_ci 31cb0ef41Sopenharmony_ciconst { kRetryHandlerDefaultRetry } = require('../core/symbols') 41cb0ef41Sopenharmony_ciconst { RequestRetryError } = require('../core/errors') 51cb0ef41Sopenharmony_ciconst { isDisturbed, parseHeaders, parseRangeHeader } = require('../core/util') 61cb0ef41Sopenharmony_ci 71cb0ef41Sopenharmony_cifunction calculateRetryAfterHeader (retryAfter) { 81cb0ef41Sopenharmony_ci const current = Date.now() 91cb0ef41Sopenharmony_ci const diff = new Date(retryAfter).getTime() - current 101cb0ef41Sopenharmony_ci 111cb0ef41Sopenharmony_ci return diff 121cb0ef41Sopenharmony_ci} 131cb0ef41Sopenharmony_ci 141cb0ef41Sopenharmony_ciclass RetryHandler { 151cb0ef41Sopenharmony_ci constructor (opts, handlers) { 161cb0ef41Sopenharmony_ci const { retryOptions, ...dispatchOpts } = opts 171cb0ef41Sopenharmony_ci const { 181cb0ef41Sopenharmony_ci // Retry scoped 191cb0ef41Sopenharmony_ci retry: retryFn, 201cb0ef41Sopenharmony_ci maxRetries, 211cb0ef41Sopenharmony_ci maxTimeout, 221cb0ef41Sopenharmony_ci minTimeout, 231cb0ef41Sopenharmony_ci timeoutFactor, 241cb0ef41Sopenharmony_ci // Response scoped 251cb0ef41Sopenharmony_ci methods, 261cb0ef41Sopenharmony_ci errorCodes, 271cb0ef41Sopenharmony_ci retryAfter, 281cb0ef41Sopenharmony_ci statusCodes 291cb0ef41Sopenharmony_ci } = retryOptions ?? {} 301cb0ef41Sopenharmony_ci 311cb0ef41Sopenharmony_ci this.dispatch = handlers.dispatch 321cb0ef41Sopenharmony_ci this.handler = handlers.handler 331cb0ef41Sopenharmony_ci this.opts = dispatchOpts 341cb0ef41Sopenharmony_ci this.abort = null 351cb0ef41Sopenharmony_ci this.aborted = false 361cb0ef41Sopenharmony_ci this.retryOpts = { 371cb0ef41Sopenharmony_ci retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], 381cb0ef41Sopenharmony_ci retryAfter: retryAfter ?? true, 391cb0ef41Sopenharmony_ci maxTimeout: maxTimeout ?? 30 * 1000, // 30s, 401cb0ef41Sopenharmony_ci timeout: minTimeout ?? 500, // .5s 411cb0ef41Sopenharmony_ci timeoutFactor: timeoutFactor ?? 2, 421cb0ef41Sopenharmony_ci maxRetries: maxRetries ?? 5, 431cb0ef41Sopenharmony_ci // What errors we should retry 441cb0ef41Sopenharmony_ci methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], 451cb0ef41Sopenharmony_ci // Indicates which errors to retry 461cb0ef41Sopenharmony_ci statusCodes: statusCodes ?? [500, 502, 503, 504, 429], 471cb0ef41Sopenharmony_ci // List of errors to retry 481cb0ef41Sopenharmony_ci errorCodes: errorCodes ?? [ 491cb0ef41Sopenharmony_ci 'ECONNRESET', 501cb0ef41Sopenharmony_ci 'ECONNREFUSED', 511cb0ef41Sopenharmony_ci 'ENOTFOUND', 521cb0ef41Sopenharmony_ci 'ENETDOWN', 531cb0ef41Sopenharmony_ci 'ENETUNREACH', 541cb0ef41Sopenharmony_ci 'EHOSTDOWN', 551cb0ef41Sopenharmony_ci 'EHOSTUNREACH', 561cb0ef41Sopenharmony_ci 'EPIPE' 571cb0ef41Sopenharmony_ci ] 581cb0ef41Sopenharmony_ci } 591cb0ef41Sopenharmony_ci 601cb0ef41Sopenharmony_ci this.retryCount = 0 611cb0ef41Sopenharmony_ci this.start = 0 621cb0ef41Sopenharmony_ci this.end = null 631cb0ef41Sopenharmony_ci this.etag = null 641cb0ef41Sopenharmony_ci this.resume = null 651cb0ef41Sopenharmony_ci 661cb0ef41Sopenharmony_ci // Handle possible onConnect duplication 671cb0ef41Sopenharmony_ci this.handler.onConnect(reason => { 681cb0ef41Sopenharmony_ci this.aborted = true 691cb0ef41Sopenharmony_ci if (this.abort) { 701cb0ef41Sopenharmony_ci this.abort(reason) 711cb0ef41Sopenharmony_ci } else { 721cb0ef41Sopenharmony_ci this.reason = reason 731cb0ef41Sopenharmony_ci } 741cb0ef41Sopenharmony_ci }) 751cb0ef41Sopenharmony_ci } 761cb0ef41Sopenharmony_ci 771cb0ef41Sopenharmony_ci onRequestSent () { 781cb0ef41Sopenharmony_ci if (this.handler.onRequestSent) { 791cb0ef41Sopenharmony_ci this.handler.onRequestSent() 801cb0ef41Sopenharmony_ci } 811cb0ef41Sopenharmony_ci } 821cb0ef41Sopenharmony_ci 831cb0ef41Sopenharmony_ci onUpgrade (statusCode, headers, socket) { 841cb0ef41Sopenharmony_ci if (this.handler.onUpgrade) { 851cb0ef41Sopenharmony_ci this.handler.onUpgrade(statusCode, headers, socket) 861cb0ef41Sopenharmony_ci } 871cb0ef41Sopenharmony_ci } 881cb0ef41Sopenharmony_ci 891cb0ef41Sopenharmony_ci onConnect (abort) { 901cb0ef41Sopenharmony_ci if (this.aborted) { 911cb0ef41Sopenharmony_ci abort(this.reason) 921cb0ef41Sopenharmony_ci } else { 931cb0ef41Sopenharmony_ci this.abort = abort 941cb0ef41Sopenharmony_ci } 951cb0ef41Sopenharmony_ci } 961cb0ef41Sopenharmony_ci 971cb0ef41Sopenharmony_ci onBodySent (chunk) { 981cb0ef41Sopenharmony_ci if (this.handler.onBodySent) return this.handler.onBodySent(chunk) 991cb0ef41Sopenharmony_ci } 1001cb0ef41Sopenharmony_ci 1011cb0ef41Sopenharmony_ci static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { 1021cb0ef41Sopenharmony_ci const { statusCode, code, headers } = err 1031cb0ef41Sopenharmony_ci const { method, retryOptions } = opts 1041cb0ef41Sopenharmony_ci const { 1051cb0ef41Sopenharmony_ci maxRetries, 1061cb0ef41Sopenharmony_ci timeout, 1071cb0ef41Sopenharmony_ci maxTimeout, 1081cb0ef41Sopenharmony_ci timeoutFactor, 1091cb0ef41Sopenharmony_ci statusCodes, 1101cb0ef41Sopenharmony_ci errorCodes, 1111cb0ef41Sopenharmony_ci methods 1121cb0ef41Sopenharmony_ci } = retryOptions 1131cb0ef41Sopenharmony_ci let { counter, currentTimeout } = state 1141cb0ef41Sopenharmony_ci 1151cb0ef41Sopenharmony_ci currentTimeout = 1161cb0ef41Sopenharmony_ci currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout 1171cb0ef41Sopenharmony_ci 1181cb0ef41Sopenharmony_ci // Any code that is not a Undici's originated and allowed to retry 1191cb0ef41Sopenharmony_ci if ( 1201cb0ef41Sopenharmony_ci code && 1211cb0ef41Sopenharmony_ci code !== 'UND_ERR_REQ_RETRY' && 1221cb0ef41Sopenharmony_ci code !== 'UND_ERR_SOCKET' && 1231cb0ef41Sopenharmony_ci !errorCodes.includes(code) 1241cb0ef41Sopenharmony_ci ) { 1251cb0ef41Sopenharmony_ci cb(err) 1261cb0ef41Sopenharmony_ci return 1271cb0ef41Sopenharmony_ci } 1281cb0ef41Sopenharmony_ci 1291cb0ef41Sopenharmony_ci // If a set of method are provided and the current method is not in the list 1301cb0ef41Sopenharmony_ci if (Array.isArray(methods) && !methods.includes(method)) { 1311cb0ef41Sopenharmony_ci cb(err) 1321cb0ef41Sopenharmony_ci return 1331cb0ef41Sopenharmony_ci } 1341cb0ef41Sopenharmony_ci 1351cb0ef41Sopenharmony_ci // If a set of status code are provided and the current status code is not in the list 1361cb0ef41Sopenharmony_ci if ( 1371cb0ef41Sopenharmony_ci statusCode != null && 1381cb0ef41Sopenharmony_ci Array.isArray(statusCodes) && 1391cb0ef41Sopenharmony_ci !statusCodes.includes(statusCode) 1401cb0ef41Sopenharmony_ci ) { 1411cb0ef41Sopenharmony_ci cb(err) 1421cb0ef41Sopenharmony_ci return 1431cb0ef41Sopenharmony_ci } 1441cb0ef41Sopenharmony_ci 1451cb0ef41Sopenharmony_ci // If we reached the max number of retries 1461cb0ef41Sopenharmony_ci if (counter > maxRetries) { 1471cb0ef41Sopenharmony_ci cb(err) 1481cb0ef41Sopenharmony_ci return 1491cb0ef41Sopenharmony_ci } 1501cb0ef41Sopenharmony_ci 1511cb0ef41Sopenharmony_ci let retryAfterHeader = headers != null && headers['retry-after'] 1521cb0ef41Sopenharmony_ci if (retryAfterHeader) { 1531cb0ef41Sopenharmony_ci retryAfterHeader = Number(retryAfterHeader) 1541cb0ef41Sopenharmony_ci retryAfterHeader = isNaN(retryAfterHeader) 1551cb0ef41Sopenharmony_ci ? calculateRetryAfterHeader(retryAfterHeader) 1561cb0ef41Sopenharmony_ci : retryAfterHeader * 1e3 // Retry-After is in seconds 1571cb0ef41Sopenharmony_ci } 1581cb0ef41Sopenharmony_ci 1591cb0ef41Sopenharmony_ci const retryTimeout = 1601cb0ef41Sopenharmony_ci retryAfterHeader > 0 1611cb0ef41Sopenharmony_ci ? Math.min(retryAfterHeader, maxTimeout) 1621cb0ef41Sopenharmony_ci : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout) 1631cb0ef41Sopenharmony_ci 1641cb0ef41Sopenharmony_ci state.currentTimeout = retryTimeout 1651cb0ef41Sopenharmony_ci 1661cb0ef41Sopenharmony_ci setTimeout(() => cb(null), retryTimeout) 1671cb0ef41Sopenharmony_ci } 1681cb0ef41Sopenharmony_ci 1691cb0ef41Sopenharmony_ci onHeaders (statusCode, rawHeaders, resume, statusMessage) { 1701cb0ef41Sopenharmony_ci const headers = parseHeaders(rawHeaders) 1711cb0ef41Sopenharmony_ci 1721cb0ef41Sopenharmony_ci this.retryCount += 1 1731cb0ef41Sopenharmony_ci 1741cb0ef41Sopenharmony_ci if (statusCode >= 300) { 1751cb0ef41Sopenharmony_ci this.abort( 1761cb0ef41Sopenharmony_ci new RequestRetryError('Request failed', statusCode, { 1771cb0ef41Sopenharmony_ci headers, 1781cb0ef41Sopenharmony_ci count: this.retryCount 1791cb0ef41Sopenharmony_ci }) 1801cb0ef41Sopenharmony_ci ) 1811cb0ef41Sopenharmony_ci return false 1821cb0ef41Sopenharmony_ci } 1831cb0ef41Sopenharmony_ci 1841cb0ef41Sopenharmony_ci // Checkpoint for resume from where we left it 1851cb0ef41Sopenharmony_ci if (this.resume != null) { 1861cb0ef41Sopenharmony_ci this.resume = null 1871cb0ef41Sopenharmony_ci 1881cb0ef41Sopenharmony_ci if (statusCode !== 206) { 1891cb0ef41Sopenharmony_ci return true 1901cb0ef41Sopenharmony_ci } 1911cb0ef41Sopenharmony_ci 1921cb0ef41Sopenharmony_ci const contentRange = parseRangeHeader(headers['content-range']) 1931cb0ef41Sopenharmony_ci // If no content range 1941cb0ef41Sopenharmony_ci if (!contentRange) { 1951cb0ef41Sopenharmony_ci this.abort( 1961cb0ef41Sopenharmony_ci new RequestRetryError('Content-Range mismatch', statusCode, { 1971cb0ef41Sopenharmony_ci headers, 1981cb0ef41Sopenharmony_ci count: this.retryCount 1991cb0ef41Sopenharmony_ci }) 2001cb0ef41Sopenharmony_ci ) 2011cb0ef41Sopenharmony_ci return false 2021cb0ef41Sopenharmony_ci } 2031cb0ef41Sopenharmony_ci 2041cb0ef41Sopenharmony_ci // Let's start with a weak etag check 2051cb0ef41Sopenharmony_ci if (this.etag != null && this.etag !== headers.etag) { 2061cb0ef41Sopenharmony_ci this.abort( 2071cb0ef41Sopenharmony_ci new RequestRetryError('ETag mismatch', statusCode, { 2081cb0ef41Sopenharmony_ci headers, 2091cb0ef41Sopenharmony_ci count: this.retryCount 2101cb0ef41Sopenharmony_ci }) 2111cb0ef41Sopenharmony_ci ) 2121cb0ef41Sopenharmony_ci return false 2131cb0ef41Sopenharmony_ci } 2141cb0ef41Sopenharmony_ci 2151cb0ef41Sopenharmony_ci const { start, size, end = size } = contentRange 2161cb0ef41Sopenharmony_ci 2171cb0ef41Sopenharmony_ci assert(this.start === start, 'content-range mismatch') 2181cb0ef41Sopenharmony_ci assert(this.end == null || this.end === end, 'content-range mismatch') 2191cb0ef41Sopenharmony_ci 2201cb0ef41Sopenharmony_ci this.resume = resume 2211cb0ef41Sopenharmony_ci return true 2221cb0ef41Sopenharmony_ci } 2231cb0ef41Sopenharmony_ci 2241cb0ef41Sopenharmony_ci if (this.end == null) { 2251cb0ef41Sopenharmony_ci if (statusCode === 206) { 2261cb0ef41Sopenharmony_ci // First time we receive 206 2271cb0ef41Sopenharmony_ci const range = parseRangeHeader(headers['content-range']) 2281cb0ef41Sopenharmony_ci 2291cb0ef41Sopenharmony_ci if (range == null) { 2301cb0ef41Sopenharmony_ci return this.handler.onHeaders( 2311cb0ef41Sopenharmony_ci statusCode, 2321cb0ef41Sopenharmony_ci rawHeaders, 2331cb0ef41Sopenharmony_ci resume, 2341cb0ef41Sopenharmony_ci statusMessage 2351cb0ef41Sopenharmony_ci ) 2361cb0ef41Sopenharmony_ci } 2371cb0ef41Sopenharmony_ci 2381cb0ef41Sopenharmony_ci const { start, size, end = size } = range 2391cb0ef41Sopenharmony_ci 2401cb0ef41Sopenharmony_ci assert( 2411cb0ef41Sopenharmony_ci start != null && Number.isFinite(start) && this.start !== start, 2421cb0ef41Sopenharmony_ci 'content-range mismatch' 2431cb0ef41Sopenharmony_ci ) 2441cb0ef41Sopenharmony_ci assert(Number.isFinite(start)) 2451cb0ef41Sopenharmony_ci assert( 2461cb0ef41Sopenharmony_ci end != null && Number.isFinite(end) && this.end !== end, 2471cb0ef41Sopenharmony_ci 'invalid content-length' 2481cb0ef41Sopenharmony_ci ) 2491cb0ef41Sopenharmony_ci 2501cb0ef41Sopenharmony_ci this.start = start 2511cb0ef41Sopenharmony_ci this.end = end 2521cb0ef41Sopenharmony_ci } 2531cb0ef41Sopenharmony_ci 2541cb0ef41Sopenharmony_ci // We make our best to checkpoint the body for further range headers 2551cb0ef41Sopenharmony_ci if (this.end == null) { 2561cb0ef41Sopenharmony_ci const contentLength = headers['content-length'] 2571cb0ef41Sopenharmony_ci this.end = contentLength != null ? Number(contentLength) : null 2581cb0ef41Sopenharmony_ci } 2591cb0ef41Sopenharmony_ci 2601cb0ef41Sopenharmony_ci assert(Number.isFinite(this.start)) 2611cb0ef41Sopenharmony_ci assert( 2621cb0ef41Sopenharmony_ci this.end == null || Number.isFinite(this.end), 2631cb0ef41Sopenharmony_ci 'invalid content-length' 2641cb0ef41Sopenharmony_ci ) 2651cb0ef41Sopenharmony_ci 2661cb0ef41Sopenharmony_ci this.resume = resume 2671cb0ef41Sopenharmony_ci this.etag = headers.etag != null ? headers.etag : null 2681cb0ef41Sopenharmony_ci 2691cb0ef41Sopenharmony_ci return this.handler.onHeaders( 2701cb0ef41Sopenharmony_ci statusCode, 2711cb0ef41Sopenharmony_ci rawHeaders, 2721cb0ef41Sopenharmony_ci resume, 2731cb0ef41Sopenharmony_ci statusMessage 2741cb0ef41Sopenharmony_ci ) 2751cb0ef41Sopenharmony_ci } 2761cb0ef41Sopenharmony_ci 2771cb0ef41Sopenharmony_ci const err = new RequestRetryError('Request failed', statusCode, { 2781cb0ef41Sopenharmony_ci headers, 2791cb0ef41Sopenharmony_ci count: this.retryCount 2801cb0ef41Sopenharmony_ci }) 2811cb0ef41Sopenharmony_ci 2821cb0ef41Sopenharmony_ci this.abort(err) 2831cb0ef41Sopenharmony_ci 2841cb0ef41Sopenharmony_ci return false 2851cb0ef41Sopenharmony_ci } 2861cb0ef41Sopenharmony_ci 2871cb0ef41Sopenharmony_ci onData (chunk) { 2881cb0ef41Sopenharmony_ci this.start += chunk.length 2891cb0ef41Sopenharmony_ci 2901cb0ef41Sopenharmony_ci return this.handler.onData(chunk) 2911cb0ef41Sopenharmony_ci } 2921cb0ef41Sopenharmony_ci 2931cb0ef41Sopenharmony_ci onComplete (rawTrailers) { 2941cb0ef41Sopenharmony_ci this.retryCount = 0 2951cb0ef41Sopenharmony_ci return this.handler.onComplete(rawTrailers) 2961cb0ef41Sopenharmony_ci } 2971cb0ef41Sopenharmony_ci 2981cb0ef41Sopenharmony_ci onError (err) { 2991cb0ef41Sopenharmony_ci if (this.aborted || isDisturbed(this.opts.body)) { 3001cb0ef41Sopenharmony_ci return this.handler.onError(err) 3011cb0ef41Sopenharmony_ci } 3021cb0ef41Sopenharmony_ci 3031cb0ef41Sopenharmony_ci this.retryOpts.retry( 3041cb0ef41Sopenharmony_ci err, 3051cb0ef41Sopenharmony_ci { 3061cb0ef41Sopenharmony_ci state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, 3071cb0ef41Sopenharmony_ci opts: { retryOptions: this.retryOpts, ...this.opts } 3081cb0ef41Sopenharmony_ci }, 3091cb0ef41Sopenharmony_ci onRetry.bind(this) 3101cb0ef41Sopenharmony_ci ) 3111cb0ef41Sopenharmony_ci 3121cb0ef41Sopenharmony_ci function onRetry (err) { 3131cb0ef41Sopenharmony_ci if (err != null || this.aborted || isDisturbed(this.opts.body)) { 3141cb0ef41Sopenharmony_ci return this.handler.onError(err) 3151cb0ef41Sopenharmony_ci } 3161cb0ef41Sopenharmony_ci 3171cb0ef41Sopenharmony_ci if (this.start !== 0) { 3181cb0ef41Sopenharmony_ci this.opts = { 3191cb0ef41Sopenharmony_ci ...this.opts, 3201cb0ef41Sopenharmony_ci headers: { 3211cb0ef41Sopenharmony_ci ...this.opts.headers, 3221cb0ef41Sopenharmony_ci range: `bytes=${this.start}-${this.end ?? ''}` 3231cb0ef41Sopenharmony_ci } 3241cb0ef41Sopenharmony_ci } 3251cb0ef41Sopenharmony_ci } 3261cb0ef41Sopenharmony_ci 3271cb0ef41Sopenharmony_ci try { 3281cb0ef41Sopenharmony_ci this.dispatch(this.opts, this) 3291cb0ef41Sopenharmony_ci } catch (err) { 3301cb0ef41Sopenharmony_ci this.handler.onError(err) 3311cb0ef41Sopenharmony_ci } 3321cb0ef41Sopenharmony_ci } 3331cb0ef41Sopenharmony_ci } 3341cb0ef41Sopenharmony_ci} 3351cb0ef41Sopenharmony_ci 3361cb0ef41Sopenharmony_cimodule.exports = RetryHandler 337