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