1'use strict' 2const { URL } = require('url') 3const http = require('http') 4const https = require('https') 5const zlib = require('minizlib') 6const { Minipass } = require('minipass') 7 8const Body = require('./body.js') 9const { writeToStream, getTotalBytes } = Body 10const Response = require('./response.js') 11const Headers = require('./headers.js') 12const { createHeadersLenient } = Headers 13const Request = require('./request.js') 14const { getNodeRequestOptions } = Request 15const FetchError = require('./fetch-error.js') 16const AbortError = require('./abort-error.js') 17 18// XXX this should really be split up and unit-ized for easier testing 19// and better DRY implementation of data/http request aborting 20const fetch = async (url, opts) => { 21 if (/^data:/.test(url)) { 22 const request = new Request(url, opts) 23 // delay 1 promise tick so that the consumer can abort right away 24 return Promise.resolve().then(() => new Promise((resolve, reject) => { 25 let type, data 26 try { 27 const { pathname, search } = new URL(url) 28 const split = pathname.split(',') 29 if (split.length < 2) { 30 throw new Error('invalid data: URI') 31 } 32 const mime = split.shift() 33 const base64 = /;base64$/.test(mime) 34 type = base64 ? mime.slice(0, -1 * ';base64'.length) : mime 35 const rawData = decodeURIComponent(split.join(',') + search) 36 data = base64 ? Buffer.from(rawData, 'base64') : Buffer.from(rawData) 37 } catch (er) { 38 return reject(new FetchError(`[${request.method}] ${ 39 request.url} invalid URL, ${er.message}`, 'system', er)) 40 } 41 42 const { signal } = request 43 if (signal && signal.aborted) { 44 return reject(new AbortError('The user aborted a request.')) 45 } 46 47 const headers = { 'Content-Length': data.length } 48 if (type) { 49 headers['Content-Type'] = type 50 } 51 return resolve(new Response(data, { headers })) 52 })) 53 } 54 55 return new Promise((resolve, reject) => { 56 // build request object 57 const request = new Request(url, opts) 58 let options 59 try { 60 options = getNodeRequestOptions(request) 61 } catch (er) { 62 return reject(er) 63 } 64 65 const send = (options.protocol === 'https:' ? https : http).request 66 const { signal } = request 67 let response = null 68 const abort = () => { 69 const error = new AbortError('The user aborted a request.') 70 reject(error) 71 if (Minipass.isStream(request.body) && 72 typeof request.body.destroy === 'function') { 73 request.body.destroy(error) 74 } 75 if (response && response.body) { 76 response.body.emit('error', error) 77 } 78 } 79 80 if (signal && signal.aborted) { 81 return abort() 82 } 83 84 const abortAndFinalize = () => { 85 abort() 86 finalize() 87 } 88 89 const finalize = () => { 90 req.abort() 91 if (signal) { 92 signal.removeEventListener('abort', abortAndFinalize) 93 } 94 clearTimeout(reqTimeout) 95 } 96 97 // send request 98 const req = send(options) 99 100 if (signal) { 101 signal.addEventListener('abort', abortAndFinalize) 102 } 103 104 let reqTimeout = null 105 if (request.timeout) { 106 req.once('socket', socket => { 107 reqTimeout = setTimeout(() => { 108 reject(new FetchError(`network timeout at: ${ 109 request.url}`, 'request-timeout')) 110 finalize() 111 }, request.timeout) 112 }) 113 } 114 115 req.on('error', er => { 116 // if a 'response' event is emitted before the 'error' event, then by the 117 // time this handler is run it's too late to reject the Promise for the 118 // response. instead, we forward the error event to the response stream 119 // so that the error will surface to the user when they try to consume 120 // the body. this is done as a side effect of aborting the request except 121 // for in windows, where we must forward the event manually, otherwise 122 // there is no longer a ref'd socket attached to the request and the 123 // stream never ends so the event loop runs out of work and the process 124 // exits without warning. 125 // coverage skipped here due to the difficulty in testing 126 // istanbul ignore next 127 if (req.res) { 128 req.res.emit('error', er) 129 } 130 reject(new FetchError(`request to ${request.url} failed, reason: ${ 131 er.message}`, 'system', er)) 132 finalize() 133 }) 134 135 req.on('response', res => { 136 clearTimeout(reqTimeout) 137 138 const headers = createHeadersLenient(res.headers) 139 140 // HTTP fetch step 5 141 if (fetch.isRedirect(res.statusCode)) { 142 // HTTP fetch step 5.2 143 const location = headers.get('Location') 144 145 // HTTP fetch step 5.3 146 let locationURL = null 147 try { 148 locationURL = location === null ? null : new URL(location, request.url).toString() 149 } catch { 150 // error here can only be invalid URL in Location: header 151 // do not throw when options.redirect == manual 152 // let the user extract the errorneous redirect URL 153 if (request.redirect !== 'manual') { 154 /* eslint-disable-next-line max-len */ 155 reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect')) 156 finalize() 157 return 158 } 159 } 160 161 // HTTP fetch step 5.5 162 if (request.redirect === 'error') { 163 reject(new FetchError('uri requested responds with a redirect, ' + 164 `redirect mode is set to error: ${request.url}`, 'no-redirect')) 165 finalize() 166 return 167 } else if (request.redirect === 'manual') { 168 // node-fetch-specific step: make manual redirect a bit easier to 169 // use by setting the Location header value to the resolved URL. 170 if (locationURL !== null) { 171 // handle corrupted header 172 try { 173 headers.set('Location', locationURL) 174 } catch (err) { 175 /* istanbul ignore next: nodejs server prevent invalid 176 response headers, we can't test this through normal 177 request */ 178 reject(err) 179 } 180 } 181 } else if (request.redirect === 'follow' && locationURL !== null) { 182 // HTTP-redirect fetch step 5 183 if (request.counter >= request.follow) { 184 reject(new FetchError(`maximum redirect reached at: ${ 185 request.url}`, 'max-redirect')) 186 finalize() 187 return 188 } 189 190 // HTTP-redirect fetch step 9 191 if (res.statusCode !== 303 && 192 request.body && 193 getTotalBytes(request) === null) { 194 reject(new FetchError( 195 'Cannot follow redirect with body being a readable stream', 196 'unsupported-redirect' 197 )) 198 finalize() 199 return 200 } 201 202 // Update host due to redirection 203 request.headers.set('host', (new URL(locationURL)).host) 204 205 // HTTP-redirect fetch step 6 (counter increment) 206 // Create a new Request object. 207 const requestOpts = { 208 headers: new Headers(request.headers), 209 follow: request.follow, 210 counter: request.counter + 1, 211 agent: request.agent, 212 compress: request.compress, 213 method: request.method, 214 body: request.body, 215 signal: request.signal, 216 timeout: request.timeout, 217 } 218 219 // if the redirect is to a new hostname, strip the authorization and cookie headers 220 const parsedOriginal = new URL(request.url) 221 const parsedRedirect = new URL(locationURL) 222 if (parsedOriginal.hostname !== parsedRedirect.hostname) { 223 requestOpts.headers.delete('authorization') 224 requestOpts.headers.delete('cookie') 225 } 226 227 // HTTP-redirect fetch step 11 228 if (res.statusCode === 303 || ( 229 (res.statusCode === 301 || res.statusCode === 302) && 230 request.method === 'POST' 231 )) { 232 requestOpts.method = 'GET' 233 requestOpts.body = undefined 234 requestOpts.headers.delete('content-length') 235 } 236 237 // HTTP-redirect fetch step 15 238 resolve(fetch(new Request(locationURL, requestOpts))) 239 finalize() 240 return 241 } 242 } // end if(isRedirect) 243 244 // prepare response 245 res.once('end', () => 246 signal && signal.removeEventListener('abort', abortAndFinalize)) 247 248 const body = new Minipass() 249 // if an error occurs, either on the response stream itself, on one of the 250 // decoder streams, or a response length timeout from the Body class, we 251 // forward the error through to our internal body stream. If we see an 252 // error event on that, we call finalize to abort the request and ensure 253 // we don't leave a socket believing a request is in flight. 254 // this is difficult to test, so lacks specific coverage. 255 body.on('error', finalize) 256 // exceedingly rare that the stream would have an error, 257 // but just in case we proxy it to the stream in use. 258 res.on('error', /* istanbul ignore next */ er => body.emit('error', er)) 259 res.on('data', (chunk) => body.write(chunk)) 260 res.on('end', () => body.end()) 261 262 const responseOptions = { 263 url: request.url, 264 status: res.statusCode, 265 statusText: res.statusMessage, 266 headers: headers, 267 size: request.size, 268 timeout: request.timeout, 269 counter: request.counter, 270 trailer: new Promise(resolveTrailer => 271 res.on('end', () => resolveTrailer(createHeadersLenient(res.trailers)))), 272 } 273 274 // HTTP-network fetch step 12.1.1.3 275 const codings = headers.get('Content-Encoding') 276 277 // HTTP-network fetch step 12.1.1.4: handle content codings 278 279 // in following scenarios we ignore compression support 280 // 1. compression support is disabled 281 // 2. HEAD request 282 // 3. no Content-Encoding header 283 // 4. no content response (204) 284 // 5. content not modified response (304) 285 if (!request.compress || 286 request.method === 'HEAD' || 287 codings === null || 288 res.statusCode === 204 || 289 res.statusCode === 304) { 290 response = new Response(body, responseOptions) 291 resolve(response) 292 return 293 } 294 295 // Be less strict when decoding compressed responses, since sometimes 296 // servers send slightly invalid responses that are still accepted 297 // by common browsers. 298 // Always using Z_SYNC_FLUSH is what cURL does. 299 const zlibOptions = { 300 flush: zlib.constants.Z_SYNC_FLUSH, 301 finishFlush: zlib.constants.Z_SYNC_FLUSH, 302 } 303 304 // for gzip 305 if (codings === 'gzip' || codings === 'x-gzip') { 306 const unzip = new zlib.Gunzip(zlibOptions) 307 response = new Response( 308 // exceedingly rare that the stream would have an error, 309 // but just in case we proxy it to the stream in use. 310 body.on('error', /* istanbul ignore next */ er => unzip.emit('error', er)).pipe(unzip), 311 responseOptions 312 ) 313 resolve(response) 314 return 315 } 316 317 // for deflate 318 if (codings === 'deflate' || codings === 'x-deflate') { 319 // handle the infamous raw deflate response from old servers 320 // a hack for old IIS and Apache servers 321 const raw = res.pipe(new Minipass()) 322 raw.once('data', chunk => { 323 // see http://stackoverflow.com/questions/37519828 324 const decoder = (chunk[0] & 0x0F) === 0x08 325 ? new zlib.Inflate() 326 : new zlib.InflateRaw() 327 // exceedingly rare that the stream would have an error, 328 // but just in case we proxy it to the stream in use. 329 body.on('error', /* istanbul ignore next */ er => decoder.emit('error', er)).pipe(decoder) 330 response = new Response(decoder, responseOptions) 331 resolve(response) 332 }) 333 return 334 } 335 336 // for br 337 if (codings === 'br') { 338 // ignoring coverage so tests don't have to fake support (or lack of) for brotli 339 // istanbul ignore next 340 try { 341 var decoder = new zlib.BrotliDecompress() 342 } catch (err) { 343 reject(err) 344 finalize() 345 return 346 } 347 // exceedingly rare that the stream would have an error, 348 // but just in case we proxy it to the stream in use. 349 body.on('error', /* istanbul ignore next */ er => decoder.emit('error', er)).pipe(decoder) 350 response = new Response(decoder, responseOptions) 351 resolve(response) 352 return 353 } 354 355 // otherwise, use response as-is 356 response = new Response(body, responseOptions) 357 resolve(response) 358 }) 359 360 writeToStream(req, request) 361 }) 362} 363 364module.exports = fetch 365 366fetch.isRedirect = code => 367 code === 301 || 368 code === 302 || 369 code === 303 || 370 code === 307 || 371 code === 308 372 373fetch.Headers = Headers 374fetch.Request = Request 375fetch.Response = Response 376fetch.FetchError = FetchError 377fetch.AbortError = AbortError 378