1'use strict' 2const { URL } = require('url') 3const { Minipass } = require('minipass') 4const Headers = require('./headers.js') 5const { exportNodeCompatibleHeaders } = Headers 6const Body = require('./body.js') 7const { clone, extractContentType, getTotalBytes } = Body 8 9const version = require('../package.json').version 10const defaultUserAgent = 11 `minipass-fetch/${version} (+https://github.com/isaacs/minipass-fetch)` 12 13const INTERNALS = Symbol('Request internals') 14 15const isRequest = input => 16 typeof input === 'object' && typeof input[INTERNALS] === 'object' 17 18const isAbortSignal = signal => { 19 const proto = ( 20 signal 21 && typeof signal === 'object' 22 && Object.getPrototypeOf(signal) 23 ) 24 return !!(proto && proto.constructor.name === 'AbortSignal') 25} 26 27class Request extends Body { 28 constructor (input, init = {}) { 29 const parsedURL = isRequest(input) ? new URL(input.url) 30 : input && input.href ? new URL(input.href) 31 : new URL(`${input}`) 32 33 if (isRequest(input)) { 34 init = { ...input[INTERNALS], ...init } 35 } else if (!input || typeof input === 'string') { 36 input = {} 37 } 38 39 const method = (init.method || input.method || 'GET').toUpperCase() 40 const isGETHEAD = method === 'GET' || method === 'HEAD' 41 42 if ((init.body !== null && init.body !== undefined || 43 isRequest(input) && input.body !== null) && isGETHEAD) { 44 throw new TypeError('Request with GET/HEAD method cannot have body') 45 } 46 47 const inputBody = init.body !== null && init.body !== undefined ? init.body 48 : isRequest(input) && input.body !== null ? clone(input) 49 : null 50 51 super(inputBody, { 52 timeout: init.timeout || input.timeout || 0, 53 size: init.size || input.size || 0, 54 }) 55 56 const headers = new Headers(init.headers || input.headers || {}) 57 58 if (inputBody !== null && inputBody !== undefined && 59 !headers.has('Content-Type')) { 60 const contentType = extractContentType(inputBody) 61 if (contentType) { 62 headers.append('Content-Type', contentType) 63 } 64 } 65 66 const signal = 'signal' in init ? init.signal 67 : null 68 69 if (signal !== null && signal !== undefined && !isAbortSignal(signal)) { 70 throw new TypeError('Expected signal must be an instanceof AbortSignal') 71 } 72 73 // TLS specific options that are handled by node 74 const { 75 ca, 76 cert, 77 ciphers, 78 clientCertEngine, 79 crl, 80 dhparam, 81 ecdhCurve, 82 family, 83 honorCipherOrder, 84 key, 85 passphrase, 86 pfx, 87 rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0', 88 secureOptions, 89 secureProtocol, 90 servername, 91 sessionIdContext, 92 } = init 93 94 this[INTERNALS] = { 95 method, 96 redirect: init.redirect || input.redirect || 'follow', 97 headers, 98 parsedURL, 99 signal, 100 ca, 101 cert, 102 ciphers, 103 clientCertEngine, 104 crl, 105 dhparam, 106 ecdhCurve, 107 family, 108 honorCipherOrder, 109 key, 110 passphrase, 111 pfx, 112 rejectUnauthorized, 113 secureOptions, 114 secureProtocol, 115 servername, 116 sessionIdContext, 117 } 118 119 // node-fetch-only options 120 this.follow = init.follow !== undefined ? init.follow 121 : input.follow !== undefined ? input.follow 122 : 20 123 this.compress = init.compress !== undefined ? init.compress 124 : input.compress !== undefined ? input.compress 125 : true 126 this.counter = init.counter || input.counter || 0 127 this.agent = init.agent || input.agent 128 } 129 130 get method () { 131 return this[INTERNALS].method 132 } 133 134 get url () { 135 return this[INTERNALS].parsedURL.toString() 136 } 137 138 get headers () { 139 return this[INTERNALS].headers 140 } 141 142 get redirect () { 143 return this[INTERNALS].redirect 144 } 145 146 get signal () { 147 return this[INTERNALS].signal 148 } 149 150 clone () { 151 return new Request(this) 152 } 153 154 get [Symbol.toStringTag] () { 155 return 'Request' 156 } 157 158 static getNodeRequestOptions (request) { 159 const parsedURL = request[INTERNALS].parsedURL 160 const headers = new Headers(request[INTERNALS].headers) 161 162 // fetch step 1.3 163 if (!headers.has('Accept')) { 164 headers.set('Accept', '*/*') 165 } 166 167 // Basic fetch 168 if (!/^https?:$/.test(parsedURL.protocol)) { 169 throw new TypeError('Only HTTP(S) protocols are supported') 170 } 171 172 if (request.signal && 173 Minipass.isStream(request.body) && 174 typeof request.body.destroy !== 'function') { 175 throw new Error( 176 'Cancellation of streamed requests with AbortSignal is not supported') 177 } 178 179 // HTTP-network-or-cache fetch steps 2.4-2.7 180 const contentLengthValue = 181 (request.body === null || request.body === undefined) && 182 /^(POST|PUT)$/i.test(request.method) ? '0' 183 : request.body !== null && request.body !== undefined 184 ? getTotalBytes(request) 185 : null 186 187 if (contentLengthValue) { 188 headers.set('Content-Length', contentLengthValue + '') 189 } 190 191 // HTTP-network-or-cache fetch step 2.11 192 if (!headers.has('User-Agent')) { 193 headers.set('User-Agent', defaultUserAgent) 194 } 195 196 // HTTP-network-or-cache fetch step 2.15 197 if (request.compress && !headers.has('Accept-Encoding')) { 198 headers.set('Accept-Encoding', 'gzip,deflate') 199 } 200 201 const agent = typeof request.agent === 'function' 202 ? request.agent(parsedURL) 203 : request.agent 204 205 if (!headers.has('Connection') && !agent) { 206 headers.set('Connection', 'close') 207 } 208 209 // TLS specific options that are handled by node 210 const { 211 ca, 212 cert, 213 ciphers, 214 clientCertEngine, 215 crl, 216 dhparam, 217 ecdhCurve, 218 family, 219 honorCipherOrder, 220 key, 221 passphrase, 222 pfx, 223 rejectUnauthorized, 224 secureOptions, 225 secureProtocol, 226 servername, 227 sessionIdContext, 228 } = request[INTERNALS] 229 230 // HTTP-network fetch step 4.2 231 // chunked encoding is handled by Node.js 232 233 // we cannot spread parsedURL directly, so we have to read each property one-by-one 234 // and map them to the equivalent https?.request() method options 235 const urlProps = { 236 auth: parsedURL.username || parsedURL.password 237 ? `${parsedURL.username}:${parsedURL.password}` 238 : '', 239 host: parsedURL.host, 240 hostname: parsedURL.hostname, 241 path: `${parsedURL.pathname}${parsedURL.search}`, 242 port: parsedURL.port, 243 protocol: parsedURL.protocol, 244 } 245 246 return { 247 ...urlProps, 248 method: request.method, 249 headers: exportNodeCompatibleHeaders(headers), 250 agent, 251 ca, 252 cert, 253 ciphers, 254 clientCertEngine, 255 crl, 256 dhparam, 257 ecdhCurve, 258 family, 259 honorCipherOrder, 260 key, 261 passphrase, 262 pfx, 263 rejectUnauthorized, 264 secureOptions, 265 secureProtocol, 266 servername, 267 sessionIdContext, 268 timeout: request.timeout, 269 } 270 } 271} 272 273module.exports = Request 274 275Object.defineProperties(Request.prototype, { 276 method: { enumerable: true }, 277 url: { enumerable: true }, 278 headers: { enumerable: true }, 279 redirect: { enumerable: true }, 280 clone: { enumerable: true }, 281 signal: { enumerable: true }, 282}) 283