fetch.js 3.9 KB

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