[6a3a178] | 1 | const Minipass = require('minipass')
|
---|
| 2 | const MinipassPipeline = require('minipass-pipeline')
|
---|
| 3 | const fetch = require('minipass-fetch')
|
---|
| 4 | const promiseRetry = require('promise-retry')
|
---|
| 5 | const ssri = require('ssri')
|
---|
| 6 |
|
---|
| 7 | const getAgent = require('./agent.js')
|
---|
| 8 | const pkg = require('../package.json')
|
---|
| 9 |
|
---|
| 10 | const USER_AGENT = `${pkg.name}/${pkg.version} (+https://npm.im/${pkg.name})`
|
---|
| 11 |
|
---|
| 12 | const RETRY_ERRORS = [
|
---|
| 13 | 'ECONNRESET', // remote socket closed on us
|
---|
| 14 | 'ECONNREFUSED', // remote host refused to open connection
|
---|
| 15 | 'EADDRINUSE', // failed to bind to a local port (proxy?)
|
---|
| 16 | 'ETIMEDOUT', // someone in the transaction is WAY TOO SLOW
|
---|
| 17 | 'ERR_SOCKET_TIMEOUT', // same as above, but this one comes from agentkeepalive
|
---|
| 18 | // Known codes we do NOT retry on:
|
---|
| 19 | // ENOTFOUND (getaddrinfo failure. Either bad hostname, or offline)
|
---|
| 20 | ]
|
---|
| 21 |
|
---|
| 22 | const RETRY_TYPES = [
|
---|
| 23 | 'request-timeout',
|
---|
| 24 | ]
|
---|
| 25 |
|
---|
| 26 | // make a request directly to the remote source,
|
---|
| 27 | // retrying certain classes of errors as well as
|
---|
| 28 | // following redirects (through the cache if necessary)
|
---|
| 29 | // and verifying response integrity
|
---|
| 30 | const remoteFetch = (request, options) => {
|
---|
| 31 | const agent = getAgent(request.url, options)
|
---|
| 32 | if (!request.headers.has('connection'))
|
---|
| 33 | request.headers.set('connection', agent ? 'keep-alive' : 'close')
|
---|
| 34 |
|
---|
| 35 | if (!request.headers.has('user-agent'))
|
---|
| 36 | request.headers.set('user-agent', USER_AGENT)
|
---|
| 37 |
|
---|
| 38 | // keep our own options since we're overriding the agent
|
---|
| 39 | // and the redirect mode
|
---|
| 40 | const _opts = {
|
---|
| 41 | ...options,
|
---|
| 42 | agent,
|
---|
| 43 | redirect: 'manual',
|
---|
| 44 | }
|
---|
| 45 |
|
---|
| 46 | return promiseRetry(async (retryHandler, attemptNum) => {
|
---|
| 47 | const req = new fetch.Request(request, _opts)
|
---|
| 48 | try {
|
---|
| 49 | let res = await fetch(req, _opts)
|
---|
| 50 | if (_opts.integrity && res.status === 200) {
|
---|
| 51 | // we got a 200 response and the user has specified an expected
|
---|
| 52 | // integrity value, so wrap the response in an ssri stream to verify it
|
---|
| 53 | const integrityStream = ssri.integrityStream({ integrity: _opts.integrity })
|
---|
| 54 | res = new fetch.Response(new MinipassPipeline(res.body, integrityStream), res)
|
---|
| 55 | }
|
---|
| 56 |
|
---|
| 57 | res.headers.set('x-fetch-attempts', attemptNum)
|
---|
| 58 |
|
---|
| 59 | // do not retry POST requests, or requests with a streaming body
|
---|
| 60 | // do retry requests with a 408, 420, 429 or 500+ status in the response
|
---|
| 61 | const isStream = Minipass.isStream(req.body)
|
---|
| 62 | const isRetriable = req.method !== 'POST' &&
|
---|
| 63 | !isStream &&
|
---|
| 64 | ([408, 420, 429].includes(res.status) || res.status >= 500)
|
---|
| 65 |
|
---|
| 66 | if (isRetriable) {
|
---|
| 67 | if (typeof options.onRetry === 'function')
|
---|
| 68 | options.onRetry(res)
|
---|
| 69 |
|
---|
| 70 | return retryHandler(res)
|
---|
| 71 | }
|
---|
| 72 |
|
---|
| 73 | return res
|
---|
| 74 | } catch (err) {
|
---|
| 75 | const code = (err.code === 'EPROMISERETRY')
|
---|
| 76 | ? err.retried.code
|
---|
| 77 | : err.code
|
---|
| 78 |
|
---|
| 79 | // err.retried will be the thing that was thrown from above
|
---|
| 80 | // if it's a response, we just got a bad status code and we
|
---|
| 81 | // can re-throw to allow the retry
|
---|
| 82 | const isRetryError = err.retried instanceof fetch.Response ||
|
---|
| 83 | (RETRY_ERRORS.includes(code) && RETRY_TYPES.includes(err.type))
|
---|
| 84 |
|
---|
| 85 | if (req.method === 'POST' || isRetryError)
|
---|
| 86 | throw err
|
---|
| 87 |
|
---|
| 88 | if (typeof options.onRetry === 'function')
|
---|
| 89 | options.onRetry(err)
|
---|
| 90 |
|
---|
| 91 | return retryHandler(err)
|
---|
| 92 | }
|
---|
| 93 | }, options.retry).catch((err) => {
|
---|
| 94 | // don't reject for http errors, just return them
|
---|
| 95 | if (err.status >= 400 && err.type !== 'system')
|
---|
| 96 | return err
|
---|
| 97 |
|
---|
| 98 | throw err
|
---|
| 99 | })
|
---|
| 100 | }
|
---|
| 101 |
|
---|
| 102 | module.exports = remoteFetch
|
---|