UNPKG

3.95 kBJavaScriptView Raw
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