| 'use strict'; | |
| const util = require('util'); | |
| const urlutil = require('url'); | |
| const http = require('http'); | |
| const https = require('https'); | |
| const debug = require('debug')('urllib'); | |
| const ms = require('humanize-ms'); | |
| let REQUEST_ID = 0; | |
| const MAX_VALUE = Math.pow(2, 31) - 10; | |
| const PROTO_RE = /^https?:\/\//i; | |
| 
 | |
| function getAgent(agent, defaultAgent) { | |
|   return agent === undefined ? defaultAgent : agent; | |
| } | |
| 
 | |
| function parseContentType(str) { | |
|   if (!str) { | |
|     return ''; | |
|   } | |
| 
 | |
|   return str.split(';')[0].trim().toLowerCase(); | |
| } | |
| 
 | |
| function makeCallback(resolve, reject) { | |
|   return function (err, data, res) { | |
|     if (err) { | |
|       return reject(err); | |
|     } | |
|     resolve({ | |
|       data: data, | |
|       status: res.statusCode, | |
|       headers: res.headers, | |
|       res: res | |
|     }); | |
|   }; | |
| } | |
| 
 | |
| // exports.TIMEOUT = ms('5s'); | |
| exports.TIMEOUTS = [ms('300s'), ms('300s')]; | |
| 
 | |
| const TEXT_DATA_TYPES = ['json', 'text']; | |
| 
 | |
| exports.request = function request(url, args, callback) { | |
|   // request(url, callback) | |
|   if (arguments.length === 2 && typeof args === 'function') { | |
|     callback = args; | |
|     args = null; | |
|   } | |
|   if (typeof callback === 'function') { | |
|     return exports.requestWithCallback(url, args, callback); | |
|   } | |
| 
 | |
|   return new Promise(function (resolve, reject) { | |
|     exports.requestWithCallback(url, args, makeCallback(resolve, reject)); | |
|   }); | |
| }; | |
| 
 | |
| exports.requestWithCallback = function requestWithCallback(url, args, callback) { | |
|   if (!url || (typeof url !== 'string' && typeof url !== 'object')) { | |
|     const msg = util.format('expect request url to be a string or a http request options, but got' + ' %j', url); | |
|     throw new Error(msg); | |
|   } | |
| 
 | |
|   if (arguments.length === 2 && typeof args === 'function') { | |
|     callback = args; | |
|     args = null; | |
|   } | |
| 
 | |
|   args = args || {}; | |
|   if (REQUEST_ID >= MAX_VALUE) { | |
|     REQUEST_ID = 0; | |
|   } | |
|   const reqId = ++REQUEST_ID; | |
| 
 | |
|   args.requestUrls = args.requestUrls || []; | |
| 
 | |
|   const reqMeta = { | |
|     requestId: reqId, | |
|     url: url, | |
|     args: args, | |
|     ctx: args.ctx | |
|   }; | |
|   if (args.emitter) { | |
|     args.emitter.emit('request', reqMeta); | |
|   } | |
| 
 | |
|   args.timeout = args.timeout || exports.TIMEOUTS; | |
|   args.maxRedirects = args.maxRedirects || 10; | |
|   args.streaming = args.streaming || args.customResponse; | |
|   const requestStartTime = Date.now(); | |
|   let parsedUrl; | |
| 
 | |
|   if (typeof url === 'string') { | |
|     if (!PROTO_RE.test(url)) { | |
|       // Support `request('www.server.com')` | |
|       url = 'https://' + url; | |
|     } | |
|     parsedUrl = urlutil.parse(url); | |
|   } else { | |
|     parsedUrl = url; | |
|   } | |
| 
 | |
|   const method = (args.type || args.method || parsedUrl.method || 'GET').toUpperCase(); | |
|   let port = parsedUrl.port || 80; | |
|   let httplib = http; | |
|   let agent = getAgent(args.agent, exports.agent); | |
|   const fixJSONCtlChars = args.fixJSONCtlChars; | |
| 
 | |
|   if (parsedUrl.protocol === 'https:') { | |
|     httplib = https; | |
|     agent = getAgent(args.httpsAgent, exports.httpsAgent); | |
| 
 | |
|     if (!parsedUrl.port) { | |
|       port = 443; | |
|     } | |
|   } | |
| 
 | |
|   // request through proxy tunnel | |
|   // var proxyTunnelAgent = detectProxyAgent(parsedUrl, args); | |
|   // if (proxyTunnelAgent) { | |
|   //   agent = proxyTunnelAgent; | |
|   // } | |
|  | |
|   const options = { | |
|     host: parsedUrl.hostname || parsedUrl.host || 'localhost', | |
|     path: parsedUrl.path || '/', | |
|     method: method, | |
|     port: port, | |
|     agent: agent, | |
|     headers: args.headers || {}, | |
|     // default is dns.lookup | |
|     // https://github.com/nodejs/node/blob/master/lib/net.js#L986 | |
|     // custom dnslookup require node >= 4.0.0 | |
|     // https://github.com/nodejs/node/blob/archived-io.js-v0.12/lib/net.js#L952 | |
|     lookup: args.lookup | |
|   }; | |
| 
 | |
|   if (Array.isArray(args.timeout)) { | |
|     options.requestTimeout = args.timeout[args.timeout.length - 1]; | |
|   } else if (typeof args.timeout !== 'undefined') { | |
|     options.requestTimeout = args.timeout; | |
|   } | |
| 
 | |
|   // const sslNames = [ | |
|   //   'pfx', | |
|   //   'key', | |
|   //   'passphrase', | |
|   //   'cert', | |
|   //   'ca', | |
|   //   'ciphers', | |
|   //   'rejectUnauthorized', | |
|   //   'secureProtocol', | |
|   //   'secureOptions', | |
|   // ]; | |
|   // for (let i = 0; i < sslNames.length; i++) { | |
|   //   const name = sslNames[i]; | |
|   //   if (args.hasOwnProperty(name)) { | |
|   //     options[name] = args[name]; | |
|   //   } | |
|   // } | |
|  | |
|   // don't check ssl | |
|   // if (options.rejectUnauthorized === false && !options.hasOwnProperty('secureOptions')) { | |
|   //   options.secureOptions = require('constants').SSL_OP_NO_TLSv1_2; | |
|   // } | |
|  | |
|   const auth = args.auth || parsedUrl.auth; | |
|   if (auth) { | |
|     options.auth = auth; | |
|   } | |
| 
 | |
|   // content undefined  data 有值 | |
|   let body = args.content || args.data; | |
|   const dataAsQueryString = method === 'GET' || method === 'HEAD' || args.dataAsQueryString; | |
|   if (!args.content) { | |
|     if (body && !(typeof body === 'string' || Buffer.isBuffer(body))) { | |
|       if (dataAsQueryString) { | |
|         // read: GET, HEAD, use query string | |
|         body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body); | |
|       } else { | |
|         let contentType = options.headers['Content-Type'] || options.headers['content-type']; | |
|         // auto add application/x-www-form-urlencoded when using urlencode form request | |
|         if (!contentType) { | |
|           if (args.contentType === 'json') { | |
|             contentType = 'application/json'; | |
|           } else { | |
|             contentType = 'application/x-www-form-urlencoded'; | |
|           } | |
|           options.headers['Content-Type'] = contentType; | |
|         } | |
| 
 | |
|         if (parseContentType(contentType) === 'application/json') { | |
|           body = JSON.stringify(body); | |
|         } else { | |
|           // 'application/x-www-form-urlencoded' | |
|           body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body); | |
|         } | |
|       } | |
|     } | |
|   } | |
| 
 | |
|   // if it's a GET or HEAD request, data should be sent as query string | |
|   if (dataAsQueryString && body) { | |
|     options.path += (parsedUrl.query ? '&' : '?') + body; | |
|     body = null; | |
|   } | |
| 
 | |
|   let requestSize = 0; | |
|   if (body) { | |
|     let length = body.length; | |
|     if (!Buffer.isBuffer(body)) { | |
|       length = Buffer.byteLength(body); | |
|     } | |
|     requestSize = options.headers['Content-Length'] = length; | |
|   } | |
| 
 | |
|   if (args.dataType === 'json') { | |
|     options.headers.Accept = 'application/json'; | |
|   } | |
| 
 | |
|   if (typeof args.beforeRequest === 'function') { | |
|     // you can use this hook to change every thing. | |
|     args.beforeRequest(options); | |
|   } | |
|   let connectTimer = null; | |
|   let responseTimer = null; | |
|   let __err = null; | |
|   let connected = false; // socket connected or not | |
|   let keepAliveSocket = false; // request with keepalive socket | |
|   let responseSize = 0; | |
|   let statusCode = -1; | |
|   let responseAborted = false; | |
|   let remoteAddress = ''; | |
|   let remotePort = ''; | |
|   let timing = null; | |
|   if (args.timing) { | |
|     timing = { | |
|       // socket assigned | |
|       queuing: 0, | |
|       // dns lookup time | |
|       dnslookup: 0, | |
|       // socket connected | |
|       connected: 0, | |
|       // request sent | |
|       requestSent: 0, | |
|       // Time to first byte (TTFB) | |
|       waiting: 0, | |
|       contentDownload: 0 | |
|     }; | |
|   } | |
| 
 | |
|   function cancelConnectTimer() { | |
|     if (connectTimer) { | |
|       clearTimeout(connectTimer); | |
|       connectTimer = null; | |
|     } | |
|   } | |
|   function cancelResponseTimer() { | |
|     if (responseTimer) { | |
|       clearTimeout(responseTimer); | |
|       responseTimer = null; | |
|     } | |
|   } | |
| 
 | |
|   function done(err, data, res) { | |
|     cancelResponseTimer(); | |
|     if (!callback) { | |
|       console.warn( | |
|         '[urllib:warn] [%s] [%s] [worker:%s] %s %s callback twice!!!', | |
|         Date(), | |
|         reqId, | |
|         process.pid, | |
|         options.method, | |
|         url | |
|       ); | |
|       // https://github.com/node-modules/urllib/pull/30 | |
|       if (err) { | |
|         console.warn( | |
|           '[urllib:warn] [%s] [%s] [worker:%s] %s: %s\nstack: %s', | |
|           Date(), | |
|           reqId, | |
|           process.pid, | |
|           err.name, | |
|           err.message, | |
|           err.stack | |
|         ); | |
|       } | |
|       return; | |
|     } | |
|     const cb = callback; | |
|     callback = null; | |
|     let headers = {}; | |
|     if (res) { | |
|       statusCode = res.statusCode; | |
|       headers = res.headers; | |
|     } | |
| 
 | |
|     // handle digest auth | |
|     // if (statusCode === 401 && headers['www-authenticate'] | |
|     //   && (!args.headers || !args.headers.Authorization) && args.digestAuth) { | |
|     //   const authenticate = headers['www-authenticate']; | |
|     //   if (authenticate.indexOf('Digest ') >= 0) { | |
|     //     debug('Request#%d %s: got digest auth header WWW-Authenticate: %s', reqId, url, authenticate); | |
|     //     args.headers = args.headers || {}; | |
|     //     args.headers.Authorization = digestAuthHeader(options.method, options.path, authenticate, args.digestAuth); | |
|     //     debug('Request#%d %s: auth with digest header: %s', reqId, url, args.headers.Authorization); | |
|     //     if (res.headers['set-cookie']) { | |
|     //       args.headers.Cookie = res.headers['set-cookie'].join(';'); | |
|     //     } | |
|     //     return exports.requestWithCallback(url, args, cb); | |
|     //   } | |
|     // } | |
|  | |
|     const requestUseTime = Date.now() - requestStartTime; | |
|     if (timing) { | |
|       timing.contentDownload = requestUseTime; | |
|     } | |
| 
 | |
|     debug( | |
|       '[%sms] done, %s bytes HTTP %s %s %s %s, keepAliveSocket: %s, timing: %j', | |
|       requestUseTime, | |
|       responseSize, | |
|       statusCode, | |
|       options.method, | |
|       options.host, | |
|       options.path, | |
|       keepAliveSocket, | |
|       timing | |
|     ); | |
| 
 | |
|     const response = { | |
|       status: statusCode, | |
|       statusCode: statusCode, | |
|       headers: headers, | |
|       size: responseSize, | |
|       aborted: responseAborted, | |
|       rt: requestUseTime, | |
|       keepAliveSocket: keepAliveSocket, | |
|       data: data, | |
|       requestUrls: args.requestUrls, | |
|       timing: timing, | |
|       remoteAddress: remoteAddress, | |
|       remotePort: remotePort | |
|     }; | |
| 
 | |
|     if (err) { | |
|       let agentStatus = ''; | |
|       if (agent && typeof agent.getCurrentStatus === 'function') { | |
|         // add current agent status to error message for logging and debug | |
|         agentStatus = ', agent status: ' + JSON.stringify(agent.getCurrentStatus()); | |
|       } | |
|       err.message += | |
|         ', ' + | |
|         options.method + | |
|         ' ' + | |
|         url + | |
|         ' ' + | |
|         statusCode + | |
|         ' (connected: ' + | |
|         connected + | |
|         ', keepalive socket: ' + | |
|         keepAliveSocket + | |
|         agentStatus + | |
|         ')' + | |
|         '\nheaders: ' + | |
|         JSON.stringify(headers); | |
|       err.data = data; | |
|       err.path = options.path; | |
|       err.status = statusCode; | |
|       err.headers = headers; | |
|       err.res = response; | |
|     } | |
| 
 | |
|     cb(err, data, args.streaming ? res : response); | |
| 
 | |
|     if (args.emitter) { | |
|       // keep to use the same reqMeta object on request event before | |
|       reqMeta.url = url; | |
|       reqMeta.socket = req && req.connection; | |
|       reqMeta.options = options; | |
|       reqMeta.size = requestSize; | |
| 
 | |
|       args.emitter.emit('response', { | |
|         requestId: reqId, | |
|         error: err, | |
|         ctx: args.ctx, | |
|         req: reqMeta, | |
|         res: response | |
|       }); | |
|     } | |
|   } | |
| 
 | |
|   function handleRedirect(res) { | |
|     let err = null; | |
|     if (args.followRedirect && statuses.redirect[res.statusCode]) { | |
|       // handle redirect | |
|       args._followRedirectCount = (args._followRedirectCount || 0) + 1; | |
|       const location = res.headers.location; | |
|       if (!location) { | |
|         err = new Error('Got statusCode ' + res.statusCode + ' but cannot resolve next location from headers'); | |
|         err.name = 'FollowRedirectError'; | |
|       } else if (args._followRedirectCount > args.maxRedirects) { | |
|         err = new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + url); | |
|         err.name = 'MaxRedirectError'; | |
|       } else { | |
|         const newUrl = args.formatRedirectUrl ? args.formatRedirectUrl(url, location) : urlutil.resolve(url, location); | |
|         debug('Request#%d %s: `redirected` from %s to %s', reqId, options.path, url, newUrl); | |
|         // make sure timer stop | |
|         cancelResponseTimer(); | |
|         // should clean up headers.Host on `location: http://other-domain/url` | |
|         if (args.headers && args.headers.Host && PROTO_RE.test(location)) { | |
|           args.headers.Host = null; | |
|         } | |
|         // avoid done will be execute in the future change. | |
|         const cb = callback; | |
|         callback = null; | |
|         exports.requestWithCallback(newUrl, args, cb); | |
|         return { | |
|           redirect: true, | |
|           error: null | |
|         }; | |
|       } | |
|     } | |
|     return { | |
|       redirect: false, | |
|       error: err | |
|     }; | |
|   } | |
| 
 | |
|   if (args.gzip) { | |
|     if (!options.headers['Accept-Encoding'] && !options.headers['accept-encoding']) { | |
|       options.headers['Accept-Encoding'] = 'gzip'; | |
|     } | |
|   } | |
| 
 | |
|   function decodeContent(res, body, cb) { | |
|     const encoding = res.headers['content-encoding']; | |
|     // if (body.length === 0) { | |
|     //   return cb(null, body, encoding); | |
|     // } | |
|  | |
|     // if (!encoding || encoding.toLowerCase() !== 'gzip') { | |
|     return cb(null, body, encoding); | |
|     // } | |
|  | |
|     // debug('gunzip %d length body', body.length); | |
|     // zlib.gunzip(body, cb); | |
|   } | |
| 
 | |
|   const writeStream = args.writeStream; | |
| 
 | |
|   debug('Request#%d %s %s with headers %j, options.path: %s', reqId, method, url, options.headers, options.path); | |
| 
 | |
|   args.requestUrls.push(url); | |
| 
 | |
|   function onResponse(res) { | |
|     if (timing) { | |
|       timing.waiting = Date.now() - requestStartTime; | |
|     } | |
|     debug('Request#%d %s `req response` event emit: status %d, headers: %j', reqId, url, res.statusCode, res.headers); | |
| 
 | |
|     if (args.streaming) { | |
|       const result = handleRedirect(res); | |
|       if (result.redirect) { | |
|         res.resume(); | |
|         return; | |
|       } | |
|       if (result.error) { | |
|         res.resume(); | |
|         return done(result.error, null, res); | |
|       } | |
| 
 | |
|       return done(null, null, res); | |
|     } | |
| 
 | |
|     res.on('close', function () { | |
|       debug('Request#%d %s: `res close` event emit, total size %d', reqId, url, responseSize); | |
|     }); | |
| 
 | |
|     res.on('error', function () { | |
|       debug('Request#%d %s: `res error` event emit, total size %d', reqId, url, responseSize); | |
|     }); | |
| 
 | |
|     res.on('aborted', function () { | |
|       responseAborted = true; | |
|       debug('Request#%d %s: `res aborted` event emit, total size %d', reqId, url, responseSize); | |
|     }); | |
| 
 | |
|     if (writeStream) { | |
|       // If there's a writable stream to recieve the response data, just pipe the | |
|       // response stream to that writable stream and call the callback when it has | |
|       // finished writing. | |
|       // | |
|       // NOTE that when the response stream `res` emits an 'end' event it just | |
|       // means that it has finished piping data to another stream. In the | |
|       // meanwhile that writable stream may still writing data to the disk until | |
|       // it emits a 'close' event. | |
|       // | |
|       // That means that we should not apply callback until the 'close' of the | |
|       // writable stream is emited. | |
|       // | |
|       // See also: | |
|       // - https://github.com/TBEDP/urllib/commit/959ac3365821e0e028c231a5e8efca6af410eabb | |
|       // - http://nodejs.org/api/stream.html#stream_event_end | |
|       // - http://nodejs.org/api/stream.html#stream_event_close_1 | |
|       const result = handleRedirect(res); | |
|       if (result.redirect) { | |
|         res.resume(); | |
|         return; | |
|       } | |
|       if (result.error) { | |
|         res.resume(); | |
|         // end ths stream first | |
|         writeStream.end(); | |
|         return done(result.error, null, res); | |
|       } | |
|       // you can set consumeWriteStream false that only wait response end | |
|       if (args.consumeWriteStream === false) { | |
|         res.on('end', done.bind(null, null, null, res)); | |
|       } else { | |
|         // node 0.10, 0.12: only emit res aborted, writeStream close not fired | |
|         // if (isNode010 || isNode012) { | |
|         //   first([ | |
|         //     [ writeStream, 'close' ], | |
|         //     [ res, 'aborted' ], | |
|         //   ], function(_, stream, event) { | |
|         //     debug('Request#%d %s: writeStream or res %s event emitted', reqId, url, event); | |
|         //     done(__err || null, null, res); | |
|         //   }); | |
|         if (false) { | |
|         } else { | |
|           writeStream.on('close', function () { | |
|             debug('Request#%d %s: writeStream close event emitted', reqId, url); | |
|             done(__err || null, null, res); | |
|           }); | |
|         } | |
|       } | |
|       return res.pipe(writeStream); | |
|     } | |
| 
 | |
|     // Otherwise, just concat those buffers. | |
|     // | |
|     // NOTE that the `chunk` is not a String but a Buffer. It means that if | |
|     // you simply concat two chunk with `+` you're actually converting both | |
|     // Buffers into Strings before concating them. It'll cause problems when | |
|     // dealing with multi-byte characters. | |
|     // | |
|     // The solution is to store each chunk in an array and concat them with | |
|     // 'buffer-concat' when all chunks is recieved. | |
|     // | |
|     // See also: | |
|     // http://cnodejs.org/topic/4faf65852e8fb5bc65113403 | |
|  | |
|     const chunks = []; | |
| 
 | |
|     res.on('data', function (chunk) { | |
|       debug('Request#%d %s: `res data` event emit, size %d', reqId, url, chunk.length); | |
|       responseSize += chunk.length; | |
|       chunks.push(chunk); | |
|     }); | |
| 
 | |
|     res.on('end', function () { | |
|       const body = Buffer.concat(chunks, responseSize); | |
|       debug('Request#%d %s: `res end` event emit, total size %d, _dumped: %s', reqId, url, responseSize, res._dumped); | |
| 
 | |
|       if (__err) { | |
|         // req.abort() after `res data` event emit. | |
|         return done(__err, body, res); | |
|       } | |
| 
 | |
|       const result = handleRedirect(res); | |
|       if (result.error) { | |
|         return done(result.error, body, res); | |
|       } | |
|       if (result.redirect) { | |
|         return; | |
|       } | |
| 
 | |
|       decodeContent(res, body, function (err, data, encoding) { | |
|         if (err) { | |
|           return done(err, body, res); | |
|         } | |
|         // if body not decode, dont touch it | |
|         if (!encoding && TEXT_DATA_TYPES.indexOf(args.dataType) >= 0) { | |
|           // try to decode charset | |
|           try { | |
|             data = decodeBodyByCharset(data, res); | |
|           } catch (e) { | |
|             debug('decodeBodyByCharset error: %s', e); | |
|             // if error, dont touch it | |
|             return done(null, data, res); | |
|           } | |
| 
 | |
|           if (args.dataType === 'json') { | |
|             if (responseSize === 0) { | |
|               data = null; | |
|             } else { | |
|               const r = parseJSON(data, fixJSONCtlChars); | |
|               if (r.error) { | |
|                 err = r.error; | |
|               } else { | |
|                 data = r.data; | |
|               } | |
|             } | |
|           } | |
|         } | |
| 
 | |
|         if (responseAborted) { | |
|           // err = new Error('Remote socket was terminated before `response.end()` was called'); | |
|           // err.name = 'RemoteSocketClosedError'; | |
|           debug('Request#%d %s: Remote socket was terminated before `response.end()` was called', reqId, url); | |
|         } | |
| 
 | |
|         done(err, data, res); | |
|       }); | |
|     }); | |
|   } | |
| 
 | |
|   let connectTimeout, responseTimeout; | |
|   if (Array.isArray(args.timeout)) { | |
|     connectTimeout = ms(args.timeout[0]); | |
|     responseTimeout = ms(args.timeout[1]); | |
|   } else { | |
|     // set both timeout equal | |
|     connectTimeout = responseTimeout = ms(args.timeout); | |
|   } | |
|   debug('ConnectTimeout: %d, ResponseTimeout: %d', connectTimeout, responseTimeout); | |
| 
 | |
|   function startConnectTimer() { | |
|     debug('Connect timer ticking, timeout: %d', connectTimeout); | |
|     connectTimer = setTimeout(function () { | |
|       connectTimer = null; | |
|       if (statusCode === -1) { | |
|         statusCode = -2; | |
|       } | |
|       let msg = 'Connect timeout for ' + connectTimeout + 'ms'; | |
|       let errorName = 'ConnectionTimeoutError'; | |
|       if (!req.socket) { | |
|         errorName = 'SocketAssignTimeoutError'; | |
|         msg += ', working sockets is full'; | |
|       } | |
|       __err = new Error(msg); | |
|       __err.name = errorName; | |
|       __err.requestId = reqId; | |
|       debug('ConnectTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected); | |
|       abortRequest(); | |
|     }, connectTimeout); | |
|   } | |
| 
 | |
|   function startResposneTimer() { | |
|     debug('Response timer ticking, timeout: %d', responseTimeout); | |
|     responseTimer = setTimeout(function () { | |
|       responseTimer = null; | |
|       const msg = 'Response timeout for ' + responseTimeout + 'ms'; | |
|       const errorName = 'ResponseTimeoutError'; | |
|       __err = new Error(msg); | |
|       __err.name = errorName; | |
|       __err.requestId = reqId; | |
|       debug('ResponseTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected); | |
|       abortRequest(); | |
|     }, responseTimeout); | |
|   } | |
| 
 | |
|   let req; | |
|   // request headers checker will throw error | |
|   options.mode = args.mode ? args.mode : ''; | |
|   try { | |
|     req = httplib.request(options, onResponse); | |
|   } catch (err) { | |
|     return done(err); | |
|   } | |
| 
 | |
|   // environment detection: browser or nodejs | |
|   if (typeof window === 'undefined') { | |
|     // start connect timer just after `request` return, and just in nodejs environment | |
|     startConnectTimer(); | |
|   } else { | |
|     req.on('requestTimeout', function () { | |
|       if (statusCode === -1) { | |
|         statusCode = -2; | |
|       } | |
|       const msg = 'Connect timeout for ' + connectTimeout + 'ms'; | |
|       const errorName = 'ConnectionTimeoutError'; | |
|       __err = new Error(msg); | |
|       __err.name = errorName; | |
|       __err.requestId = reqId; | |
|       abortRequest(); | |
|     }); | |
|   } | |
| 
 | |
|   function abortRequest() { | |
|     debug('Request#%d %s abort, connected: %s', reqId, url, connected); | |
|     // it wont case error event when req haven't been assigned a socket yet. | |
|     if (!req.socket) { | |
|       __err.noSocket = true; | |
|       done(__err); | |
|     } | |
|     req.abort(); | |
|   } | |
| 
 | |
|   if (timing) { | |
|     // request sent | |
|     req.on('finish', function () { | |
|       timing.requestSent = Date.now() - requestStartTime; | |
|     }); | |
|   } | |
| 
 | |
|   req.once('socket', function (socket) { | |
|     if (timing) { | |
|       // socket queuing time | |
|       timing.queuing = Date.now() - requestStartTime; | |
|     } | |
| 
 | |
|     // https://github.com/nodejs/node/blob/master/lib/net.js#L377 | |
|     // https://github.com/nodejs/node/blob/v0.10.40-release/lib/net.js#L352 | |
|     // should use socket.socket on 0.10.x | |
|     // if (isNode010 && socket.socket) { | |
|     //   socket = socket.socket; | |
|     // } | |
|  | |
|     const readyState = socket.readyState; | |
|     if (readyState === 'opening') { | |
|       socket.once('lookup', function (err, ip, addressType) { | |
|         debug('Request#%d %s lookup: %s, %s, %s', reqId, url, err, ip, addressType); | |
|         if (timing) { | |
|           timing.dnslookup = Date.now() - requestStartTime; | |
|         } | |
|         if (ip) { | |
|           remoteAddress = ip; | |
|         } | |
|       }); | |
|       socket.once('connect', function () { | |
|         if (timing) { | |
|           // socket connected | |
|           timing.connected = Date.now() - requestStartTime; | |
|         } | |
| 
 | |
|         // cancel socket timer at first and start tick for TTFB | |
|         cancelConnectTimer(); | |
|         startResposneTimer(); | |
| 
 | |
|         debug('Request#%d %s new socket connected', reqId, url); | |
|         connected = true; | |
|         if (!remoteAddress) { | |
|           remoteAddress = socket.remoteAddress; | |
|         } | |
|         remotePort = socket.remotePort; | |
|       }); | |
|       return; | |
|     } | |
| 
 | |
|     debug('Request#%d %s reuse socket connected, readyState: %s', reqId, url, readyState); | |
|     connected = true; | |
|     keepAliveSocket = true; | |
|     if (!remoteAddress) { | |
|       remoteAddress = socket.remoteAddress; | |
|     } | |
|     remotePort = socket.remotePort; | |
| 
 | |
|     // reuse socket, timer should be canceled. | |
|     cancelConnectTimer(); | |
|     startResposneTimer(); | |
|   }); | |
| 
 | |
|   req.on('error', function (err) { | |
|     //TypeError for browser fetch api, Error for browser xmlhttprequest api | |
|     if (err.name === 'Error' || err.name === 'TypeError') { | |
|       err.name = connected ? 'ResponseError' : 'RequestError'; | |
|     } | |
|     err.message += ' (req "error")'; | |
|     debug('Request#%d %s `req error` event emit, %s: %s', reqId, url, err.name, err.message); | |
|     done(__err || err); | |
|   }); | |
| 
 | |
|   if (writeStream) { | |
|     writeStream.once('error', function (err) { | |
|       err.message += ' (writeStream "error")'; | |
|       __err = err; | |
|       debug('Request#%d %s `writeStream error` event emit, %s: %s', reqId, url, err.name, err.message); | |
|       abortRequest(); | |
|     }); | |
|   } | |
| 
 | |
|   if (args.stream) { | |
|     args.stream.pipe(req); | |
|     args.stream.once('error', function (err) { | |
|       err.message += ' (stream "error")'; | |
|       __err = err; | |
|       debug('Request#%d %s `readStream error` event emit, %s: %s', reqId, url, err.name, err.message); | |
|       abortRequest(); | |
|     }); | |
|   } else { | |
|     req.end(body); | |
|   } | |
| 
 | |
|   req.requestId = reqId; | |
|   return req; | |
| };
 |