index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. 'use strict'
  2. let Cache
  3. const url = require('url')
  4. const CachePolicy = require('http-cache-semantics')
  5. const fetch = require('node-fetch-npm')
  6. const pkg = require('./package.json')
  7. const retry = require('promise-retry')
  8. let ssri
  9. const Stream = require('stream')
  10. const getAgent = require('./agent')
  11. const setWarning = require('./warning')
  12. const isURL = /^https?:/
  13. const USER_AGENT = `${pkg.name}/${pkg.version} (+https://npm.im/${pkg.name})`
  14. const RETRY_ERRORS = [
  15. 'ECONNRESET', // remote socket closed on us
  16. 'ECONNREFUSED', // remote host refused to open connection
  17. 'EADDRINUSE', // failed to bind to a local port (proxy?)
  18. 'ETIMEDOUT' // someone in the transaction is WAY TOO SLOW
  19. // Known codes we do NOT retry on:
  20. // ENOTFOUND (getaddrinfo failure. Either bad hostname, or offline)
  21. ]
  22. const RETRY_TYPES = [
  23. 'request-timeout'
  24. ]
  25. // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
  26. module.exports = cachingFetch
  27. cachingFetch.defaults = function (_uri, _opts) {
  28. const fetch = this
  29. if (typeof _uri === 'object') {
  30. _opts = _uri
  31. _uri = null
  32. }
  33. function defaultedFetch (uri, opts) {
  34. const finalOpts = Object.assign({}, _opts || {}, opts || {})
  35. return fetch(uri || _uri, finalOpts)
  36. }
  37. defaultedFetch.defaults = fetch.defaults
  38. defaultedFetch.delete = fetch.delete
  39. return defaultedFetch
  40. }
  41. cachingFetch.delete = cacheDelete
  42. function cacheDelete (uri, opts) {
  43. opts = configureOptions(opts)
  44. if (opts.cacheManager) {
  45. const req = new fetch.Request(uri, {
  46. method: opts.method,
  47. headers: opts.headers
  48. })
  49. return opts.cacheManager.delete(req, opts)
  50. }
  51. }
  52. function initializeCache (opts) {
  53. if (typeof opts.cacheManager === 'string') {
  54. if (!Cache) {
  55. // Default cacache-based cache
  56. Cache = require('./cache')
  57. }
  58. opts.cacheManager = new Cache(opts.cacheManager, opts)
  59. }
  60. opts.cache = opts.cache || 'default'
  61. if (opts.cache === 'default' && isHeaderConditional(opts.headers)) {
  62. // If header list contains `If-Modified-Since`, `If-None-Match`,
  63. // `If-Unmodified-Since`, `If-Match`, or `If-Range`, fetch will set cache
  64. // mode to "no-store" if it is "default".
  65. opts.cache = 'no-store'
  66. }
  67. }
  68. function configureOptions (_opts) {
  69. const opts = Object.assign({}, _opts || {})
  70. opts.method = (opts.method || 'GET').toUpperCase()
  71. if (opts.retry && typeof opts.retry === 'number') {
  72. opts.retry = { retries: opts.retry }
  73. }
  74. if (opts.retry === false) {
  75. opts.retry = { retries: 0 }
  76. }
  77. if (opts.cacheManager) {
  78. initializeCache(opts)
  79. }
  80. return opts
  81. }
  82. function initializeSsri () {
  83. if (!ssri) {
  84. ssri = require('ssri')
  85. }
  86. }
  87. function cachingFetch (uri, _opts) {
  88. const opts = configureOptions(_opts)
  89. if (opts.integrity) {
  90. initializeSsri()
  91. // if verifying integrity, node-fetch must not decompress
  92. opts.compress = false
  93. }
  94. const isCachable = (opts.method === 'GET' || opts.method === 'HEAD') &&
  95. opts.cacheManager &&
  96. opts.cache !== 'no-store' &&
  97. opts.cache !== 'reload'
  98. if (isCachable) {
  99. const req = new fetch.Request(uri, {
  100. method: opts.method,
  101. headers: opts.headers
  102. })
  103. return opts.cacheManager.match(req, opts).then(res => {
  104. if (res) {
  105. const warningCode = (res.headers.get('Warning') || '').match(/^\d+/)
  106. if (warningCode && +warningCode >= 100 && +warningCode < 200) {
  107. // https://tools.ietf.org/html/rfc7234#section-4.3.4
  108. //
  109. // If a stored response is selected for update, the cache MUST:
  110. //
  111. // * delete any Warning header fields in the stored response with
  112. // warn-code 1xx (see Section 5.5);
  113. //
  114. // * retain any Warning header fields in the stored response with
  115. // warn-code 2xx;
  116. //
  117. res.headers.delete('Warning')
  118. }
  119. if (opts.cache === 'default' && !isStale(req, res)) {
  120. return res
  121. }
  122. if (opts.cache === 'default' || opts.cache === 'no-cache') {
  123. return conditionalFetch(req, res, opts)
  124. }
  125. if (opts.cache === 'force-cache' || opts.cache === 'only-if-cached') {
  126. // 112 Disconnected operation
  127. // SHOULD be included if the cache is intentionally disconnected from
  128. // the rest of the network for a period of time.
  129. // (https://tools.ietf.org/html/rfc2616#section-14.46)
  130. setWarning(res, 112, 'Disconnected operation')
  131. return res
  132. }
  133. }
  134. if (!res && opts.cache === 'only-if-cached') {
  135. const errorMsg = `request to ${
  136. uri
  137. } failed: cache mode is 'only-if-cached' but no cached response available.`
  138. const err = new Error(errorMsg)
  139. err.code = 'ENOTCACHED'
  140. throw err
  141. }
  142. // Missing cache entry, or mode is default (if stale), reload, no-store
  143. return remoteFetch(req.url, opts)
  144. })
  145. }
  146. return remoteFetch(uri, opts)
  147. }
  148. function iterableToObject (iter) {
  149. const obj = {}
  150. for (let k of iter.keys()) {
  151. obj[k] = iter.get(k)
  152. }
  153. return obj
  154. }
  155. function makePolicy (req, res) {
  156. const _req = {
  157. url: req.url,
  158. method: req.method,
  159. headers: iterableToObject(req.headers)
  160. }
  161. const _res = {
  162. status: res.status,
  163. headers: iterableToObject(res.headers)
  164. }
  165. return new CachePolicy(_req, _res, { shared: false })
  166. }
  167. // https://tools.ietf.org/html/rfc7234#section-4.2
  168. function isStale (req, res) {
  169. if (!res) {
  170. return null
  171. }
  172. const _req = {
  173. url: req.url,
  174. method: req.method,
  175. headers: iterableToObject(req.headers)
  176. }
  177. const policy = makePolicy(req, res)
  178. const responseTime = res.headers.get('x-local-cache-time') ||
  179. res.headers.get('date') ||
  180. 0
  181. policy._responseTime = new Date(responseTime)
  182. const bool = !policy.satisfiesWithoutRevalidation(_req)
  183. return bool
  184. }
  185. function mustRevalidate (res) {
  186. return (res.headers.get('cache-control') || '').match(/must-revalidate/i)
  187. }
  188. function conditionalFetch (req, cachedRes, opts) {
  189. const _req = {
  190. url: req.url,
  191. method: req.method,
  192. headers: Object.assign({}, opts.headers || {})
  193. }
  194. const policy = makePolicy(req, cachedRes)
  195. opts.headers = policy.revalidationHeaders(_req)
  196. return remoteFetch(req.url, opts)
  197. .then(condRes => {
  198. const revalidatedPolicy = policy.revalidatedPolicy(_req, {
  199. status: condRes.status,
  200. headers: iterableToObject(condRes.headers)
  201. })
  202. if (condRes.status >= 500 && !mustRevalidate(cachedRes)) {
  203. // 111 Revalidation failed
  204. // MUST be included if a cache returns a stale response because an
  205. // attempt to revalidate the response failed, due to an inability to
  206. // reach the server.
  207. // (https://tools.ietf.org/html/rfc2616#section-14.46)
  208. setWarning(cachedRes, 111, 'Revalidation failed')
  209. return cachedRes
  210. }
  211. if (condRes.status === 304) { // 304 Not Modified
  212. condRes.body = cachedRes.body
  213. return opts.cacheManager.put(req, condRes, opts)
  214. .then(newRes => {
  215. newRes.headers = new fetch.Headers(revalidatedPolicy.policy.responseHeaders())
  216. return newRes
  217. })
  218. }
  219. return condRes
  220. })
  221. .then(res => res)
  222. .catch(err => {
  223. if (mustRevalidate(cachedRes)) {
  224. throw err
  225. } else {
  226. // 111 Revalidation failed
  227. // MUST be included if a cache returns a stale response because an
  228. // attempt to revalidate the response failed, due to an inability to
  229. // reach the server.
  230. // (https://tools.ietf.org/html/rfc2616#section-14.46)
  231. setWarning(cachedRes, 111, 'Revalidation failed')
  232. // 199 Miscellaneous warning
  233. // The warning text MAY include arbitrary information to be presented to
  234. // a human user, or logged. A system receiving this warning MUST NOT take
  235. // any automated action, besides presenting the warning to the user.
  236. // (https://tools.ietf.org/html/rfc2616#section-14.46)
  237. setWarning(
  238. cachedRes,
  239. 199,
  240. `Miscellaneous Warning ${err.code}: ${err.message}`
  241. )
  242. return cachedRes
  243. }
  244. })
  245. }
  246. function remoteFetchHandleIntegrity (res, integrity) {
  247. const oldBod = res.body
  248. const newBod = ssri.integrityStream({
  249. integrity
  250. })
  251. oldBod.pipe(newBod)
  252. res.body = newBod
  253. oldBod.once('error', err => {
  254. newBod.emit('error', err)
  255. })
  256. newBod.once('error', err => {
  257. oldBod.emit('error', err)
  258. })
  259. }
  260. function remoteFetch (uri, opts) {
  261. const agent = getAgent(uri, opts)
  262. const headers = Object.assign({
  263. 'connection': agent ? 'keep-alive' : 'close',
  264. 'user-agent': USER_AGENT
  265. }, opts.headers || {})
  266. const reqOpts = {
  267. agent,
  268. body: opts.body,
  269. compress: opts.compress,
  270. follow: opts.follow,
  271. headers: new fetch.Headers(headers),
  272. method: opts.method,
  273. redirect: 'manual',
  274. size: opts.size,
  275. counter: opts.counter,
  276. timeout: opts.timeout
  277. }
  278. return retry(
  279. (retryHandler, attemptNum) => {
  280. const req = new fetch.Request(uri, reqOpts)
  281. return fetch(req)
  282. .then(res => {
  283. res.headers.set('x-fetch-attempts', attemptNum)
  284. if (opts.integrity) {
  285. remoteFetchHandleIntegrity(res, opts.integrity)
  286. }
  287. const isStream = req.body instanceof Stream
  288. if (opts.cacheManager) {
  289. const isMethodGetHead = req.method === 'GET' ||
  290. req.method === 'HEAD'
  291. const isCachable = opts.cache !== 'no-store' &&
  292. isMethodGetHead &&
  293. makePolicy(req, res).storable() &&
  294. res.status === 200 // No other statuses should be stored!
  295. if (isCachable) {
  296. return opts.cacheManager.put(req, res, opts)
  297. }
  298. if (!isMethodGetHead) {
  299. return opts.cacheManager.delete(req).then(() => {
  300. if (res.status >= 500 && req.method !== 'POST' && !isStream) {
  301. if (typeof opts.onRetry === 'function') {
  302. opts.onRetry(res)
  303. }
  304. return retryHandler(res)
  305. }
  306. return res
  307. })
  308. }
  309. }
  310. const isRetriable = req.method !== 'POST' &&
  311. !isStream && (
  312. res.status === 408 || // Request Timeout
  313. res.status === 420 || // Enhance Your Calm (usually Twitter rate-limit)
  314. res.status === 429 || // Too Many Requests ("standard" rate-limiting)
  315. res.status >= 500 // Assume server errors are momentary hiccups
  316. )
  317. if (isRetriable) {
  318. if (typeof opts.onRetry === 'function') {
  319. opts.onRetry(res)
  320. }
  321. return retryHandler(res)
  322. }
  323. if (!fetch.isRedirect(res.status) || opts.redirect === 'manual') {
  324. return res
  325. }
  326. // handle redirects - matches behavior of npm-fetch: https://github.com/bitinn/node-fetch
  327. if (opts.redirect === 'error') {
  328. const err = new Error(`redirect mode is set to error: ${uri}`)
  329. err.code = 'ENOREDIRECT'
  330. throw err
  331. }
  332. if (!res.headers.get('location')) {
  333. const err = new Error(`redirect location header missing at: ${uri}`)
  334. err.code = 'EINVALIDREDIRECT'
  335. throw err
  336. }
  337. if (req.counter >= req.follow) {
  338. const err = new Error(`maximum redirect reached at: ${uri}`)
  339. err.code = 'EMAXREDIRECT'
  340. throw err
  341. }
  342. const resolvedUrl = url.resolve(req.url, res.headers.get('location'))
  343. let redirectURL = url.parse(resolvedUrl)
  344. if (isURL.test(res.headers.get('location'))) {
  345. redirectURL = url.parse(res.headers.get('location'))
  346. }
  347. // Remove authorization if changing hostnames (but not if just
  348. // changing ports or protocols). This matches the behavior of request:
  349. // https://github.com/request/request/blob/b12a6245/lib/redirect.js#L134-L138
  350. if (url.parse(req.url).hostname !== redirectURL.hostname) {
  351. req.headers.delete('authorization')
  352. }
  353. // for POST request with 301/302 response, or any request with 303 response,
  354. // use GET when following redirect
  355. if (res.status === 303 ||
  356. ((res.status === 301 || res.status === 302) && req.method === 'POST')) {
  357. opts.method = 'GET'
  358. opts.body = null
  359. req.headers.delete('content-length')
  360. }
  361. opts.headers = {}
  362. req.headers.forEach((value, name) => {
  363. opts.headers[name] = value
  364. })
  365. opts.counter = ++req.counter
  366. return cachingFetch(resolvedUrl, opts)
  367. })
  368. .catch(err => {
  369. const code = err.code === 'EPROMISERETRY' ? err.retried.code : err.code
  370. const isRetryError = RETRY_ERRORS.indexOf(code) === -1 &&
  371. RETRY_TYPES.indexOf(err.type) === -1
  372. if (req.method === 'POST' || isRetryError) {
  373. throw err
  374. }
  375. if (typeof opts.onRetry === 'function') {
  376. opts.onRetry(err)
  377. }
  378. return retryHandler(err)
  379. })
  380. },
  381. opts.retry
  382. ).catch(err => {
  383. if (err.status >= 400) {
  384. return err
  385. }
  386. throw err
  387. })
  388. }
  389. function isHeaderConditional (headers) {
  390. if (!headers || typeof headers !== 'object') {
  391. return false
  392. }
  393. const modifiers = [
  394. 'if-modified-since',
  395. 'if-none-match',
  396. 'if-unmodified-since',
  397. 'if-match',
  398. 'if-range'
  399. ]
  400. return Object.keys(headers)
  401. .some(h => modifiers.indexOf(h.toLowerCase()) !== -1)
  402. }