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