11cb0ef41Sopenharmony_ciconst { Minipass } = require('minipass')
21cb0ef41Sopenharmony_ciconst fetch = require('minipass-fetch')
31cb0ef41Sopenharmony_ciconst promiseRetry = require('promise-retry')
41cb0ef41Sopenharmony_ciconst ssri = require('ssri')
51cb0ef41Sopenharmony_ci
61cb0ef41Sopenharmony_ciconst CachingMinipassPipeline = require('./pipeline.js')
71cb0ef41Sopenharmony_ciconst { getAgent } = require('@npmcli/agent')
81cb0ef41Sopenharmony_ciconst pkg = require('../package.json')
91cb0ef41Sopenharmony_ci
101cb0ef41Sopenharmony_ciconst USER_AGENT = `${pkg.name}/${pkg.version} (+https://npm.im/${pkg.name})`
111cb0ef41Sopenharmony_ci
121cb0ef41Sopenharmony_ciconst RETRY_ERRORS = [
131cb0ef41Sopenharmony_ci  'ECONNRESET', // remote socket closed on us
141cb0ef41Sopenharmony_ci  'ECONNREFUSED', // remote host refused to open connection
151cb0ef41Sopenharmony_ci  'EADDRINUSE', // failed to bind to a local port (proxy?)
161cb0ef41Sopenharmony_ci  'ETIMEDOUT', // someone in the transaction is WAY TOO SLOW
171cb0ef41Sopenharmony_ci  // from @npmcli/agent
181cb0ef41Sopenharmony_ci  'ECONNECTIONTIMEOUT',
191cb0ef41Sopenharmony_ci  'EIDLETIMEOUT',
201cb0ef41Sopenharmony_ci  'ERESPONSETIMEOUT',
211cb0ef41Sopenharmony_ci  'ETRANSFERTIMEOUT',
221cb0ef41Sopenharmony_ci  // Known codes we do NOT retry on:
231cb0ef41Sopenharmony_ci  // ENOTFOUND (getaddrinfo failure. Either bad hostname, or offline)
241cb0ef41Sopenharmony_ci  // EINVALIDPROXY // invalid protocol from @npmcli/agent
251cb0ef41Sopenharmony_ci  // EINVALIDRESPONSE // invalid status code from @npmcli/agent
261cb0ef41Sopenharmony_ci]
271cb0ef41Sopenharmony_ci
281cb0ef41Sopenharmony_ciconst RETRY_TYPES = [
291cb0ef41Sopenharmony_ci  'request-timeout',
301cb0ef41Sopenharmony_ci]
311cb0ef41Sopenharmony_ci
321cb0ef41Sopenharmony_ci// make a request directly to the remote source,
331cb0ef41Sopenharmony_ci// retrying certain classes of errors as well as
341cb0ef41Sopenharmony_ci// following redirects (through the cache if necessary)
351cb0ef41Sopenharmony_ci// and verifying response integrity
361cb0ef41Sopenharmony_ciconst remoteFetch = (request, options) => {
371cb0ef41Sopenharmony_ci  const agent = getAgent(request.url, options)
381cb0ef41Sopenharmony_ci  if (!request.headers.has('connection')) {
391cb0ef41Sopenharmony_ci    request.headers.set('connection', agent ? 'keep-alive' : 'close')
401cb0ef41Sopenharmony_ci  }
411cb0ef41Sopenharmony_ci
421cb0ef41Sopenharmony_ci  if (!request.headers.has('user-agent')) {
431cb0ef41Sopenharmony_ci    request.headers.set('user-agent', USER_AGENT)
441cb0ef41Sopenharmony_ci  }
451cb0ef41Sopenharmony_ci
461cb0ef41Sopenharmony_ci  // keep our own options since we're overriding the agent
471cb0ef41Sopenharmony_ci  // and the redirect mode
481cb0ef41Sopenharmony_ci  const _opts = {
491cb0ef41Sopenharmony_ci    ...options,
501cb0ef41Sopenharmony_ci    agent,
511cb0ef41Sopenharmony_ci    redirect: 'manual',
521cb0ef41Sopenharmony_ci  }
531cb0ef41Sopenharmony_ci
541cb0ef41Sopenharmony_ci  return promiseRetry(async (retryHandler, attemptNum) => {
551cb0ef41Sopenharmony_ci    const req = new fetch.Request(request, _opts)
561cb0ef41Sopenharmony_ci    try {
571cb0ef41Sopenharmony_ci      let res = await fetch(req, _opts)
581cb0ef41Sopenharmony_ci      if (_opts.integrity && res.status === 200) {
591cb0ef41Sopenharmony_ci        // we got a 200 response and the user has specified an expected
601cb0ef41Sopenharmony_ci        // integrity value, so wrap the response in an ssri stream to verify it
611cb0ef41Sopenharmony_ci        const integrityStream = ssri.integrityStream({
621cb0ef41Sopenharmony_ci          algorithms: _opts.algorithms,
631cb0ef41Sopenharmony_ci          integrity: _opts.integrity,
641cb0ef41Sopenharmony_ci          size: _opts.size,
651cb0ef41Sopenharmony_ci        })
661cb0ef41Sopenharmony_ci        const pipeline = new CachingMinipassPipeline({
671cb0ef41Sopenharmony_ci          events: ['integrity', 'size'],
681cb0ef41Sopenharmony_ci        }, res.body, integrityStream)
691cb0ef41Sopenharmony_ci        // we also propagate the integrity and size events out to the pipeline so we can use
701cb0ef41Sopenharmony_ci        // this new response body as an integrityEmitter for cacache
711cb0ef41Sopenharmony_ci        integrityStream.on('integrity', i => pipeline.emit('integrity', i))
721cb0ef41Sopenharmony_ci        integrityStream.on('size', s => pipeline.emit('size', s))
731cb0ef41Sopenharmony_ci        res = new fetch.Response(pipeline, res)
741cb0ef41Sopenharmony_ci        // set an explicit flag so we know if our response body will emit integrity and size
751cb0ef41Sopenharmony_ci        res.body.hasIntegrityEmitter = true
761cb0ef41Sopenharmony_ci      }
771cb0ef41Sopenharmony_ci
781cb0ef41Sopenharmony_ci      res.headers.set('x-fetch-attempts', attemptNum)
791cb0ef41Sopenharmony_ci
801cb0ef41Sopenharmony_ci      // do not retry POST requests, or requests with a streaming body
811cb0ef41Sopenharmony_ci      // do retry requests with a 408, 420, 429 or 500+ status in the response
821cb0ef41Sopenharmony_ci      const isStream = Minipass.isStream(req.body)
831cb0ef41Sopenharmony_ci      const isRetriable = req.method !== 'POST' &&
841cb0ef41Sopenharmony_ci          !isStream &&
851cb0ef41Sopenharmony_ci          ([408, 420, 429].includes(res.status) || res.status >= 500)
861cb0ef41Sopenharmony_ci
871cb0ef41Sopenharmony_ci      if (isRetriable) {
881cb0ef41Sopenharmony_ci        if (typeof options.onRetry === 'function') {
891cb0ef41Sopenharmony_ci          options.onRetry(res)
901cb0ef41Sopenharmony_ci        }
911cb0ef41Sopenharmony_ci
921cb0ef41Sopenharmony_ci        return retryHandler(res)
931cb0ef41Sopenharmony_ci      }
941cb0ef41Sopenharmony_ci
951cb0ef41Sopenharmony_ci      return res
961cb0ef41Sopenharmony_ci    } catch (err) {
971cb0ef41Sopenharmony_ci      const code = (err.code === 'EPROMISERETRY')
981cb0ef41Sopenharmony_ci        ? err.retried.code
991cb0ef41Sopenharmony_ci        : err.code
1001cb0ef41Sopenharmony_ci
1011cb0ef41Sopenharmony_ci      // err.retried will be the thing that was thrown from above
1021cb0ef41Sopenharmony_ci      // if it's a response, we just got a bad status code and we
1031cb0ef41Sopenharmony_ci      // can re-throw to allow the retry
1041cb0ef41Sopenharmony_ci      const isRetryError = err.retried instanceof fetch.Response ||
1051cb0ef41Sopenharmony_ci        (RETRY_ERRORS.includes(code) && RETRY_TYPES.includes(err.type))
1061cb0ef41Sopenharmony_ci
1071cb0ef41Sopenharmony_ci      if (req.method === 'POST' || isRetryError) {
1081cb0ef41Sopenharmony_ci        throw err
1091cb0ef41Sopenharmony_ci      }
1101cb0ef41Sopenharmony_ci
1111cb0ef41Sopenharmony_ci      if (typeof options.onRetry === 'function') {
1121cb0ef41Sopenharmony_ci        options.onRetry(err)
1131cb0ef41Sopenharmony_ci      }
1141cb0ef41Sopenharmony_ci
1151cb0ef41Sopenharmony_ci      return retryHandler(err)
1161cb0ef41Sopenharmony_ci    }
1171cb0ef41Sopenharmony_ci  }, options.retry).catch((err) => {
1181cb0ef41Sopenharmony_ci    // don't reject for http errors, just return them
1191cb0ef41Sopenharmony_ci    if (err.status >= 400 && err.type !== 'system') {
1201cb0ef41Sopenharmony_ci      return err
1211cb0ef41Sopenharmony_ci    }
1221cb0ef41Sopenharmony_ci
1231cb0ef41Sopenharmony_ci    throw err
1241cb0ef41Sopenharmony_ci  })
1251cb0ef41Sopenharmony_ci}
1261cb0ef41Sopenharmony_ci
1271cb0ef41Sopenharmony_cimodule.exports = remoteFetch
128