fetch.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. 'use strict'
  2. const { FetchError, Request, isRedirect } = require('minipass-fetch')
  3. const url = require('url')
  4. const CachePolicy = require('./cache/policy.js')
  5. const cache = require('./cache/index.js')
  6. const remote = require('./remote.js')
  7. // given a Request, a Response and user options
  8. // return true if the response is a redirect that
  9. // can be followed. we throw errors that will result
  10. // in the fetch being rejected if the redirect is
  11. // possible but invalid for some reason
  12. const canFollowRedirect = (request, response, options) => {
  13. if (!isRedirect(response.status))
  14. return false
  15. if (options.redirect === 'manual')
  16. return false
  17. if (options.redirect === 'error')
  18. throw new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect', { code: 'ENOREDIRECT' })
  19. if (!response.headers.has('location'))
  20. throw new FetchError(`redirect location header missing for: ${request.url}`, 'no-location', { code: 'EINVALIDREDIRECT' })
  21. if (request.counter >= request.follow)
  22. throw new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect', { code: 'EMAXREDIRECT' })
  23. return true
  24. }
  25. // given a Request, a Response, and the user's options return an object
  26. // with a new Request and a new options object that will be used for
  27. // following the redirect
  28. const getRedirect = (request, response, options) => {
  29. const _opts = { ...options }
  30. const location = response.headers.get('location')
  31. const redirectUrl = new url.URL(location, /^https?:/.test(location) ? undefined : request.url)
  32. // Comment below is used under the following license:
  33. // Copyright (c) 2010-2012 Mikeal Rogers
  34. // Licensed under the Apache License, Version 2.0 (the "License");
  35. // you may not use this file except in compliance with the License.
  36. // You may obtain a copy of the License at
  37. // http://www.apache.org/licenses/LICENSE-2.0
  38. // Unless required by applicable law or agreed to in writing,
  39. // software distributed under the License is distributed on an "AS
  40. // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
  41. // express or implied. See the License for the specific language
  42. // governing permissions and limitations under the License.
  43. // Remove authorization if changing hostnames (but not if just
  44. // changing ports or protocols). This matches the behavior of request:
  45. // https://github.com/request/request/blob/b12a6245/lib/redirect.js#L134-L138
  46. if (new url.URL(request.url).hostname !== redirectUrl.hostname)
  47. request.headers.delete('authorization')
  48. // for POST request with 301/302 response, or any request with 303 response,
  49. // use GET when following redirect
  50. if (response.status === 303 || (request.method === 'POST' && [301, 302].includes(response.status))) {
  51. _opts.method = 'GET'
  52. _opts.body = null
  53. request.headers.delete('content-length')
  54. }
  55. _opts.headers = {}
  56. request.headers.forEach((value, key) => {
  57. _opts.headers[key] = value
  58. })
  59. _opts.counter = ++request.counter
  60. const redirectReq = new Request(url.format(redirectUrl), _opts)
  61. return {
  62. request: redirectReq,
  63. options: _opts,
  64. }
  65. }
  66. const fetch = async (request, options) => {
  67. const response = CachePolicy.storable(request, options)
  68. ? await cache(request, options)
  69. : await remote(request, options)
  70. // if the request wasn't a GET or HEAD, and the response
  71. // status is between 200 and 399 inclusive, invalidate the
  72. // request url
  73. if (!['GET', 'HEAD'].includes(request.method) &&
  74. response.status >= 200 &&
  75. response.status <= 399)
  76. await cache.invalidate(request, options)
  77. if (!canFollowRedirect(request, response, options))
  78. return response
  79. const redirect = getRedirect(request, response, options)
  80. return fetch(redirect.request, redirect.options)
  81. }
  82. module.exports = fetch