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