xhr.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. 'use strict';
  2. const util = require('util');
  3. const urlutil = require('url');
  4. const http = require('http');
  5. const https = require('https');
  6. const debug = require('debug')('urllib');
  7. const ms = require('humanize-ms');
  8. let REQUEST_ID = 0;
  9. const MAX_VALUE = Math.pow(2, 31) - 10;
  10. const PROTO_RE = /^https?:\/\//i;
  11. function getAgent(agent, defaultAgent) {
  12. return agent === undefined ? defaultAgent : agent;
  13. }
  14. function parseContentType(str) {
  15. if (!str) {
  16. return '';
  17. }
  18. return str.split(';')[0].trim().toLowerCase();
  19. }
  20. function makeCallback(resolve, reject) {
  21. return function (err, data, res) {
  22. if (err) {
  23. return reject(err);
  24. }
  25. resolve({
  26. data: data,
  27. status: res.statusCode,
  28. headers: res.headers,
  29. res: res
  30. });
  31. };
  32. }
  33. // exports.TIMEOUT = ms('5s');
  34. exports.TIMEOUTS = [ms('300s'), ms('300s')];
  35. const TEXT_DATA_TYPES = ['json', 'text'];
  36. exports.request = function request(url, args, callback) {
  37. // request(url, callback)
  38. if (arguments.length === 2 && typeof args === 'function') {
  39. callback = args;
  40. args = null;
  41. }
  42. if (typeof callback === 'function') {
  43. return exports.requestWithCallback(url, args, callback);
  44. }
  45. return new Promise(function (resolve, reject) {
  46. exports.requestWithCallback(url, args, makeCallback(resolve, reject));
  47. });
  48. };
  49. exports.requestWithCallback = function requestWithCallback(url, args, callback) {
  50. if (!url || (typeof url !== 'string' && typeof url !== 'object')) {
  51. const msg = util.format('expect request url to be a string or a http request options, but got' + ' %j', url);
  52. throw new Error(msg);
  53. }
  54. if (arguments.length === 2 && typeof args === 'function') {
  55. callback = args;
  56. args = null;
  57. }
  58. args = args || {};
  59. if (REQUEST_ID >= MAX_VALUE) {
  60. REQUEST_ID = 0;
  61. }
  62. const reqId = ++REQUEST_ID;
  63. args.requestUrls = args.requestUrls || [];
  64. const reqMeta = {
  65. requestId: reqId,
  66. url: url,
  67. args: args,
  68. ctx: args.ctx
  69. };
  70. if (args.emitter) {
  71. args.emitter.emit('request', reqMeta);
  72. }
  73. args.timeout = args.timeout || exports.TIMEOUTS;
  74. args.maxRedirects = args.maxRedirects || 10;
  75. args.streaming = args.streaming || args.customResponse;
  76. const requestStartTime = Date.now();
  77. let parsedUrl;
  78. if (typeof url === 'string') {
  79. if (!PROTO_RE.test(url)) {
  80. // Support `request('www.server.com')`
  81. url = 'https://' + url;
  82. }
  83. parsedUrl = urlutil.parse(url);
  84. } else {
  85. parsedUrl = url;
  86. }
  87. const method = (args.type || args.method || parsedUrl.method || 'GET').toUpperCase();
  88. let port = parsedUrl.port || 80;
  89. let httplib = http;
  90. let agent = getAgent(args.agent, exports.agent);
  91. const fixJSONCtlChars = args.fixJSONCtlChars;
  92. if (parsedUrl.protocol === 'https:') {
  93. httplib = https;
  94. agent = getAgent(args.httpsAgent, exports.httpsAgent);
  95. if (!parsedUrl.port) {
  96. port = 443;
  97. }
  98. }
  99. // request through proxy tunnel
  100. // var proxyTunnelAgent = detectProxyAgent(parsedUrl, args);
  101. // if (proxyTunnelAgent) {
  102. // agent = proxyTunnelAgent;
  103. // }
  104. const options = {
  105. host: parsedUrl.hostname || parsedUrl.host || 'localhost',
  106. path: parsedUrl.path || '/',
  107. method: method,
  108. port: port,
  109. agent: agent,
  110. headers: args.headers || {},
  111. // default is dns.lookup
  112. // https://github.com/nodejs/node/blob/master/lib/net.js#L986
  113. // custom dnslookup require node >= 4.0.0
  114. // https://github.com/nodejs/node/blob/archived-io.js-v0.12/lib/net.js#L952
  115. lookup: args.lookup
  116. };
  117. if (Array.isArray(args.timeout)) {
  118. options.requestTimeout = args.timeout[args.timeout.length - 1];
  119. } else if (typeof args.timeout !== 'undefined') {
  120. options.requestTimeout = args.timeout;
  121. }
  122. // const sslNames = [
  123. // 'pfx',
  124. // 'key',
  125. // 'passphrase',
  126. // 'cert',
  127. // 'ca',
  128. // 'ciphers',
  129. // 'rejectUnauthorized',
  130. // 'secureProtocol',
  131. // 'secureOptions',
  132. // ];
  133. // for (let i = 0; i < sslNames.length; i++) {
  134. // const name = sslNames[i];
  135. // if (args.hasOwnProperty(name)) {
  136. // options[name] = args[name];
  137. // }
  138. // }
  139. // don't check ssl
  140. // if (options.rejectUnauthorized === false && !options.hasOwnProperty('secureOptions')) {
  141. // options.secureOptions = require('constants').SSL_OP_NO_TLSv1_2;
  142. // }
  143. const auth = args.auth || parsedUrl.auth;
  144. if (auth) {
  145. options.auth = auth;
  146. }
  147. // content undefined data 有值
  148. let body = args.content || args.data;
  149. const dataAsQueryString = method === 'GET' || method === 'HEAD' || args.dataAsQueryString;
  150. if (!args.content) {
  151. if (body && !(typeof body === 'string' || Buffer.isBuffer(body))) {
  152. if (dataAsQueryString) {
  153. // read: GET, HEAD, use query string
  154. body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
  155. } else {
  156. let contentType = options.headers['Content-Type'] || options.headers['content-type'];
  157. // auto add application/x-www-form-urlencoded when using urlencode form request
  158. if (!contentType) {
  159. if (args.contentType === 'json') {
  160. contentType = 'application/json';
  161. } else {
  162. contentType = 'application/x-www-form-urlencoded';
  163. }
  164. options.headers['Content-Type'] = contentType;
  165. }
  166. if (parseContentType(contentType) === 'application/json') {
  167. body = JSON.stringify(body);
  168. } else {
  169. // 'application/x-www-form-urlencoded'
  170. body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
  171. }
  172. }
  173. }
  174. }
  175. // if it's a GET or HEAD request, data should be sent as query string
  176. if (dataAsQueryString && body) {
  177. options.path += (parsedUrl.query ? '&' : '?') + body;
  178. body = null;
  179. }
  180. let requestSize = 0;
  181. if (body) {
  182. let length = body.length;
  183. if (!Buffer.isBuffer(body)) {
  184. length = Buffer.byteLength(body);
  185. }
  186. requestSize = options.headers['Content-Length'] = length;
  187. }
  188. if (args.dataType === 'json') {
  189. options.headers.Accept = 'application/json';
  190. }
  191. if (typeof args.beforeRequest === 'function') {
  192. // you can use this hook to change every thing.
  193. args.beforeRequest(options);
  194. }
  195. let connectTimer = null;
  196. let responseTimer = null;
  197. let __err = null;
  198. let connected = false; // socket connected or not
  199. let keepAliveSocket = false; // request with keepalive socket
  200. let responseSize = 0;
  201. let statusCode = -1;
  202. let responseAborted = false;
  203. let remoteAddress = '';
  204. let remotePort = '';
  205. let timing = null;
  206. if (args.timing) {
  207. timing = {
  208. // socket assigned
  209. queuing: 0,
  210. // dns lookup time
  211. dnslookup: 0,
  212. // socket connected
  213. connected: 0,
  214. // request sent
  215. requestSent: 0,
  216. // Time to first byte (TTFB)
  217. waiting: 0,
  218. contentDownload: 0
  219. };
  220. }
  221. function cancelConnectTimer() {
  222. if (connectTimer) {
  223. clearTimeout(connectTimer);
  224. connectTimer = null;
  225. }
  226. }
  227. function cancelResponseTimer() {
  228. if (responseTimer) {
  229. clearTimeout(responseTimer);
  230. responseTimer = null;
  231. }
  232. }
  233. function done(err, data, res) {
  234. cancelResponseTimer();
  235. if (!callback) {
  236. console.warn(
  237. '[urllib:warn] [%s] [%s] [worker:%s] %s %s callback twice!!!',
  238. Date(),
  239. reqId,
  240. process.pid,
  241. options.method,
  242. url
  243. );
  244. // https://github.com/node-modules/urllib/pull/30
  245. if (err) {
  246. console.warn(
  247. '[urllib:warn] [%s] [%s] [worker:%s] %s: %s\nstack: %s',
  248. Date(),
  249. reqId,
  250. process.pid,
  251. err.name,
  252. err.message,
  253. err.stack
  254. );
  255. }
  256. return;
  257. }
  258. const cb = callback;
  259. callback = null;
  260. let headers = {};
  261. if (res) {
  262. statusCode = res.statusCode;
  263. headers = res.headers;
  264. }
  265. // handle digest auth
  266. // if (statusCode === 401 && headers['www-authenticate']
  267. // && (!args.headers || !args.headers.Authorization) && args.digestAuth) {
  268. // const authenticate = headers['www-authenticate'];
  269. // if (authenticate.indexOf('Digest ') >= 0) {
  270. // debug('Request#%d %s: got digest auth header WWW-Authenticate: %s', reqId, url, authenticate);
  271. // args.headers = args.headers || {};
  272. // args.headers.Authorization = digestAuthHeader(options.method, options.path, authenticate, args.digestAuth);
  273. // debug('Request#%d %s: auth with digest header: %s', reqId, url, args.headers.Authorization);
  274. // if (res.headers['set-cookie']) {
  275. // args.headers.Cookie = res.headers['set-cookie'].join(';');
  276. // }
  277. // return exports.requestWithCallback(url, args, cb);
  278. // }
  279. // }
  280. const requestUseTime = Date.now() - requestStartTime;
  281. if (timing) {
  282. timing.contentDownload = requestUseTime;
  283. }
  284. debug(
  285. '[%sms] done, %s bytes HTTP %s %s %s %s, keepAliveSocket: %s, timing: %j',
  286. requestUseTime,
  287. responseSize,
  288. statusCode,
  289. options.method,
  290. options.host,
  291. options.path,
  292. keepAliveSocket,
  293. timing
  294. );
  295. const response = {
  296. status: statusCode,
  297. statusCode: statusCode,
  298. headers: headers,
  299. size: responseSize,
  300. aborted: responseAborted,
  301. rt: requestUseTime,
  302. keepAliveSocket: keepAliveSocket,
  303. data: data,
  304. requestUrls: args.requestUrls,
  305. timing: timing,
  306. remoteAddress: remoteAddress,
  307. remotePort: remotePort
  308. };
  309. if (err) {
  310. let agentStatus = '';
  311. if (agent && typeof agent.getCurrentStatus === 'function') {
  312. // add current agent status to error message for logging and debug
  313. agentStatus = ', agent status: ' + JSON.stringify(agent.getCurrentStatus());
  314. }
  315. err.message +=
  316. ', ' +
  317. options.method +
  318. ' ' +
  319. url +
  320. ' ' +
  321. statusCode +
  322. ' (connected: ' +
  323. connected +
  324. ', keepalive socket: ' +
  325. keepAliveSocket +
  326. agentStatus +
  327. ')' +
  328. '\nheaders: ' +
  329. JSON.stringify(headers);
  330. err.data = data;
  331. err.path = options.path;
  332. err.status = statusCode;
  333. err.headers = headers;
  334. err.res = response;
  335. }
  336. cb(err, data, args.streaming ? res : response);
  337. if (args.emitter) {
  338. // keep to use the same reqMeta object on request event before
  339. reqMeta.url = url;
  340. reqMeta.socket = req && req.connection;
  341. reqMeta.options = options;
  342. reqMeta.size = requestSize;
  343. args.emitter.emit('response', {
  344. requestId: reqId,
  345. error: err,
  346. ctx: args.ctx,
  347. req: reqMeta,
  348. res: response
  349. });
  350. }
  351. }
  352. function handleRedirect(res) {
  353. let err = null;
  354. if (args.followRedirect && statuses.redirect[res.statusCode]) {
  355. // handle redirect
  356. args._followRedirectCount = (args._followRedirectCount || 0) + 1;
  357. const location = res.headers.location;
  358. if (!location) {
  359. err = new Error('Got statusCode ' + res.statusCode + ' but cannot resolve next location from headers');
  360. err.name = 'FollowRedirectError';
  361. } else if (args._followRedirectCount > args.maxRedirects) {
  362. err = new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + url);
  363. err.name = 'MaxRedirectError';
  364. } else {
  365. const newUrl = args.formatRedirectUrl ? args.formatRedirectUrl(url, location) : urlutil.resolve(url, location);
  366. debug('Request#%d %s: `redirected` from %s to %s', reqId, options.path, url, newUrl);
  367. // make sure timer stop
  368. cancelResponseTimer();
  369. // should clean up headers.Host on `location: http://other-domain/url`
  370. if (args.headers && args.headers.Host && PROTO_RE.test(location)) {
  371. args.headers.Host = null;
  372. }
  373. // avoid done will be execute in the future change.
  374. const cb = callback;
  375. callback = null;
  376. exports.requestWithCallback(newUrl, args, cb);
  377. return {
  378. redirect: true,
  379. error: null
  380. };
  381. }
  382. }
  383. return {
  384. redirect: false,
  385. error: err
  386. };
  387. }
  388. if (args.gzip) {
  389. if (!options.headers['Accept-Encoding'] && !options.headers['accept-encoding']) {
  390. options.headers['Accept-Encoding'] = 'gzip';
  391. }
  392. }
  393. function decodeContent(res, body, cb) {
  394. const encoding = res.headers['content-encoding'];
  395. // if (body.length === 0) {
  396. // return cb(null, body, encoding);
  397. // }
  398. // if (!encoding || encoding.toLowerCase() !== 'gzip') {
  399. return cb(null, body, encoding);
  400. // }
  401. // debug('gunzip %d length body', body.length);
  402. // zlib.gunzip(body, cb);
  403. }
  404. const writeStream = args.writeStream;
  405. debug('Request#%d %s %s with headers %j, options.path: %s', reqId, method, url, options.headers, options.path);
  406. args.requestUrls.push(url);
  407. function onResponse(res) {
  408. if (timing) {
  409. timing.waiting = Date.now() - requestStartTime;
  410. }
  411. debug('Request#%d %s `req response` event emit: status %d, headers: %j', reqId, url, res.statusCode, res.headers);
  412. if (args.streaming) {
  413. const result = handleRedirect(res);
  414. if (result.redirect) {
  415. res.resume();
  416. return;
  417. }
  418. if (result.error) {
  419. res.resume();
  420. return done(result.error, null, res);
  421. }
  422. return done(null, null, res);
  423. }
  424. res.on('close', function () {
  425. debug('Request#%d %s: `res close` event emit, total size %d', reqId, url, responseSize);
  426. });
  427. res.on('error', function () {
  428. debug('Request#%d %s: `res error` event emit, total size %d', reqId, url, responseSize);
  429. });
  430. res.on('aborted', function () {
  431. responseAborted = true;
  432. debug('Request#%d %s: `res aborted` event emit, total size %d', reqId, url, responseSize);
  433. });
  434. if (writeStream) {
  435. // If there's a writable stream to recieve the response data, just pipe the
  436. // response stream to that writable stream and call the callback when it has
  437. // finished writing.
  438. //
  439. // NOTE that when the response stream `res` emits an 'end' event it just
  440. // means that it has finished piping data to another stream. In the
  441. // meanwhile that writable stream may still writing data to the disk until
  442. // it emits a 'close' event.
  443. //
  444. // That means that we should not apply callback until the 'close' of the
  445. // writable stream is emited.
  446. //
  447. // See also:
  448. // - https://github.com/TBEDP/urllib/commit/959ac3365821e0e028c231a5e8efca6af410eabb
  449. // - http://nodejs.org/api/stream.html#stream_event_end
  450. // - http://nodejs.org/api/stream.html#stream_event_close_1
  451. const result = handleRedirect(res);
  452. if (result.redirect) {
  453. res.resume();
  454. return;
  455. }
  456. if (result.error) {
  457. res.resume();
  458. // end ths stream first
  459. writeStream.end();
  460. return done(result.error, null, res);
  461. }
  462. // you can set consumeWriteStream false that only wait response end
  463. if (args.consumeWriteStream === false) {
  464. res.on('end', done.bind(null, null, null, res));
  465. } else {
  466. // node 0.10, 0.12: only emit res aborted, writeStream close not fired
  467. // if (isNode010 || isNode012) {
  468. // first([
  469. // [ writeStream, 'close' ],
  470. // [ res, 'aborted' ],
  471. // ], function(_, stream, event) {
  472. // debug('Request#%d %s: writeStream or res %s event emitted', reqId, url, event);
  473. // done(__err || null, null, res);
  474. // });
  475. if (false) {
  476. } else {
  477. writeStream.on('close', function () {
  478. debug('Request#%d %s: writeStream close event emitted', reqId, url);
  479. done(__err || null, null, res);
  480. });
  481. }
  482. }
  483. return res.pipe(writeStream);
  484. }
  485. // Otherwise, just concat those buffers.
  486. //
  487. // NOTE that the `chunk` is not a String but a Buffer. It means that if
  488. // you simply concat two chunk with `+` you're actually converting both
  489. // Buffers into Strings before concating them. It'll cause problems when
  490. // dealing with multi-byte characters.
  491. //
  492. // The solution is to store each chunk in an array and concat them with
  493. // 'buffer-concat' when all chunks is recieved.
  494. //
  495. // See also:
  496. // http://cnodejs.org/topic/4faf65852e8fb5bc65113403
  497. const chunks = [];
  498. res.on('data', function (chunk) {
  499. debug('Request#%d %s: `res data` event emit, size %d', reqId, url, chunk.length);
  500. responseSize += chunk.length;
  501. chunks.push(chunk);
  502. });
  503. res.on('end', function () {
  504. const body = Buffer.concat(chunks, responseSize);
  505. debug('Request#%d %s: `res end` event emit, total size %d, _dumped: %s', reqId, url, responseSize, res._dumped);
  506. if (__err) {
  507. // req.abort() after `res data` event emit.
  508. return done(__err, body, res);
  509. }
  510. const result = handleRedirect(res);
  511. if (result.error) {
  512. return done(result.error, body, res);
  513. }
  514. if (result.redirect) {
  515. return;
  516. }
  517. decodeContent(res, body, function (err, data, encoding) {
  518. if (err) {
  519. return done(err, body, res);
  520. }
  521. // if body not decode, dont touch it
  522. if (!encoding && TEXT_DATA_TYPES.indexOf(args.dataType) >= 0) {
  523. // try to decode charset
  524. try {
  525. data = decodeBodyByCharset(data, res);
  526. } catch (e) {
  527. debug('decodeBodyByCharset error: %s', e);
  528. // if error, dont touch it
  529. return done(null, data, res);
  530. }
  531. if (args.dataType === 'json') {
  532. if (responseSize === 0) {
  533. data = null;
  534. } else {
  535. const r = parseJSON(data, fixJSONCtlChars);
  536. if (r.error) {
  537. err = r.error;
  538. } else {
  539. data = r.data;
  540. }
  541. }
  542. }
  543. }
  544. if (responseAborted) {
  545. // err = new Error('Remote socket was terminated before `response.end()` was called');
  546. // err.name = 'RemoteSocketClosedError';
  547. debug('Request#%d %s: Remote socket was terminated before `response.end()` was called', reqId, url);
  548. }
  549. done(err, data, res);
  550. });
  551. });
  552. }
  553. let connectTimeout, responseTimeout;
  554. if (Array.isArray(args.timeout)) {
  555. connectTimeout = ms(args.timeout[0]);
  556. responseTimeout = ms(args.timeout[1]);
  557. } else {
  558. // set both timeout equal
  559. connectTimeout = responseTimeout = ms(args.timeout);
  560. }
  561. debug('ConnectTimeout: %d, ResponseTimeout: %d', connectTimeout, responseTimeout);
  562. function startConnectTimer() {
  563. debug('Connect timer ticking, timeout: %d', connectTimeout);
  564. connectTimer = setTimeout(function () {
  565. connectTimer = null;
  566. if (statusCode === -1) {
  567. statusCode = -2;
  568. }
  569. let msg = 'Connect timeout for ' + connectTimeout + 'ms';
  570. let errorName = 'ConnectionTimeoutError';
  571. if (!req.socket) {
  572. errorName = 'SocketAssignTimeoutError';
  573. msg += ', working sockets is full';
  574. }
  575. __err = new Error(msg);
  576. __err.name = errorName;
  577. __err.requestId = reqId;
  578. debug('ConnectTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
  579. abortRequest();
  580. }, connectTimeout);
  581. }
  582. function startResposneTimer() {
  583. debug('Response timer ticking, timeout: %d', responseTimeout);
  584. responseTimer = setTimeout(function () {
  585. responseTimer = null;
  586. const msg = 'Response timeout for ' + responseTimeout + 'ms';
  587. const errorName = 'ResponseTimeoutError';
  588. __err = new Error(msg);
  589. __err.name = errorName;
  590. __err.requestId = reqId;
  591. debug('ResponseTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
  592. abortRequest();
  593. }, responseTimeout);
  594. }
  595. let req;
  596. // request headers checker will throw error
  597. options.mode = args.mode ? args.mode : '';
  598. try {
  599. req = httplib.request(options, onResponse);
  600. } catch (err) {
  601. return done(err);
  602. }
  603. // environment detection: browser or nodejs
  604. if (typeof window === 'undefined') {
  605. // start connect timer just after `request` return, and just in nodejs environment
  606. startConnectTimer();
  607. } else {
  608. req.on('requestTimeout', function () {
  609. if (statusCode === -1) {
  610. statusCode = -2;
  611. }
  612. const msg = 'Connect timeout for ' + connectTimeout + 'ms';
  613. const errorName = 'ConnectionTimeoutError';
  614. __err = new Error(msg);
  615. __err.name = errorName;
  616. __err.requestId = reqId;
  617. abortRequest();
  618. });
  619. }
  620. function abortRequest() {
  621. debug('Request#%d %s abort, connected: %s', reqId, url, connected);
  622. // it wont case error event when req haven't been assigned a socket yet.
  623. if (!req.socket) {
  624. __err.noSocket = true;
  625. done(__err);
  626. }
  627. req.abort();
  628. }
  629. if (timing) {
  630. // request sent
  631. req.on('finish', function () {
  632. timing.requestSent = Date.now() - requestStartTime;
  633. });
  634. }
  635. req.once('socket', function (socket) {
  636. if (timing) {
  637. // socket queuing time
  638. timing.queuing = Date.now() - requestStartTime;
  639. }
  640. // https://github.com/nodejs/node/blob/master/lib/net.js#L377
  641. // https://github.com/nodejs/node/blob/v0.10.40-release/lib/net.js#L352
  642. // should use socket.socket on 0.10.x
  643. // if (isNode010 && socket.socket) {
  644. // socket = socket.socket;
  645. // }
  646. const readyState = socket.readyState;
  647. if (readyState === 'opening') {
  648. socket.once('lookup', function (err, ip, addressType) {
  649. debug('Request#%d %s lookup: %s, %s, %s', reqId, url, err, ip, addressType);
  650. if (timing) {
  651. timing.dnslookup = Date.now() - requestStartTime;
  652. }
  653. if (ip) {
  654. remoteAddress = ip;
  655. }
  656. });
  657. socket.once('connect', function () {
  658. if (timing) {
  659. // socket connected
  660. timing.connected = Date.now() - requestStartTime;
  661. }
  662. // cancel socket timer at first and start tick for TTFB
  663. cancelConnectTimer();
  664. startResposneTimer();
  665. debug('Request#%d %s new socket connected', reqId, url);
  666. connected = true;
  667. if (!remoteAddress) {
  668. remoteAddress = socket.remoteAddress;
  669. }
  670. remotePort = socket.remotePort;
  671. });
  672. return;
  673. }
  674. debug('Request#%d %s reuse socket connected, readyState: %s', reqId, url, readyState);
  675. connected = true;
  676. keepAliveSocket = true;
  677. if (!remoteAddress) {
  678. remoteAddress = socket.remoteAddress;
  679. }
  680. remotePort = socket.remotePort;
  681. // reuse socket, timer should be canceled.
  682. cancelConnectTimer();
  683. startResposneTimer();
  684. });
  685. req.on('error', function (err) {
  686. //TypeError for browser fetch api, Error for browser xmlhttprequest api
  687. if (err.name === 'Error' || err.name === 'TypeError') {
  688. err.name = connected ? 'ResponseError' : 'RequestError';
  689. }
  690. err.message += ' (req "error")';
  691. debug('Request#%d %s `req error` event emit, %s: %s', reqId, url, err.name, err.message);
  692. done(__err || err);
  693. });
  694. if (writeStream) {
  695. writeStream.once('error', function (err) {
  696. err.message += ' (writeStream "error")';
  697. __err = err;
  698. debug('Request#%d %s `writeStream error` event emit, %s: %s', reqId, url, err.name, err.message);
  699. abortRequest();
  700. });
  701. }
  702. if (args.stream) {
  703. args.stream.pipe(req);
  704. args.stream.once('error', function (err) {
  705. err.message += ' (stream "error")';
  706. __err = err;
  707. debug('Request#%d %s `readStream error` event emit, %s: %s', reqId, url, err.name, err.message);
  708. abortRequest();
  709. });
  710. } else {
  711. req.end(body);
  712. }
  713. req.requestId = reqId;
  714. return req;
  715. };