urllib.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. 'use strict';
  2. var debug = require('debug')('urllib');
  3. var http = require('http');
  4. var https = require('https');
  5. var urlutil = require('url');
  6. var util = require('util');
  7. var qs = require('qs');
  8. var querystring = require('querystring');
  9. var zlib = require('zlib');
  10. var ua = require('default-user-agent');
  11. var digestAuthHeader = require('digest-header');
  12. var ms = require('humanize-ms');
  13. var statuses = require('statuses');
  14. var contentTypeParser = require('content-type');
  15. var first = require('ee-first');
  16. var detectProxyAgent = require('./detect_proxy_agent');
  17. var _Promise;
  18. var _iconv;
  19. var pkg = require('../package.json');
  20. var USER_AGENT = exports.USER_AGENT = ua('node-urllib', pkg.version);
  21. // change Agent.maxSockets to 1000
  22. exports.agent = new http.Agent();
  23. exports.agent.maxSockets = 1000;
  24. exports.httpsAgent = new https.Agent();
  25. exports.httpsAgent.maxSockets = 1000;
  26. /**
  27. * The default request timeout(in milliseconds).
  28. * @type {Number}
  29. * @const
  30. */
  31. exports.TIMEOUT = ms('5s');
  32. exports.TIMEOUTS = [ms('5s'), ms('5s')];
  33. var REQUEST_ID = 0;
  34. var MAX_VALUE = Math.pow(2, 31) - 10;
  35. var isNode010 = /^v0\.10\.\d+$/.test(process.version);
  36. var isNode012 = /^v0\.12\.\d+$/.test(process.version);
  37. /**
  38. * support data types
  39. * will auto decode response body
  40. * @type {Array}
  41. */
  42. var TEXT_DATA_TYPES = [
  43. 'json',
  44. 'text'
  45. ];
  46. var PROTO_RE = /^https?:\/\//i;
  47. /**
  48. * Handle all http request, both http and https support well.
  49. *
  50. * @example
  51. *
  52. * // GET http://httptest.cnodejs.net
  53. * urllib.request('http://httptest.cnodejs.net/test/get', function(err, data, res) {});
  54. * // POST http://httptest.cnodejs.net
  55. * var args = { type: 'post', data: { foo: 'bar' } };
  56. * urllib.request('http://httptest.cnodejs.net/test/post', args, function(err, data, res) {});
  57. *
  58. * @param {String|Object} url
  59. * @param {Object} [args], optional
  60. * - {Object} [data]: request data, will auto be query stringify.
  61. * - {Boolean} [dataAsQueryString]: force convert `data` to query string.
  62. * - {String|Buffer} [content]: optional, if set content, `data` will ignore.
  63. * - {ReadStream} [stream]: read stream to sent.
  64. * - {WriteStream} [writeStream]: writable stream to save response data.
  65. * If you use this, callback's data should be null.
  66. * We will just `pipe(ws, {end: true})`.
  67. * - {consumeWriteStream} [true]: consume the writeStream, invoke the callback after writeStream close.
  68. * - {String} [method]: optional, could be GET | POST | DELETE | PUT, default is GET
  69. * - {String} [contentType]: optional, request data type, could be `json`, default is undefined
  70. * - {String} [dataType]: optional, response data type, could be `text` or `json`, default is buffer
  71. * - {Boolean|Function} [fixJSONCtlChars]: optional, fix the control characters (U+0000 through U+001F)
  72. * before JSON parse response. Default is `false`.
  73. * `fixJSONCtlChars` can be a function, will pass data to the first argument. e.g.: `data = fixJSONCtlChars(data)`
  74. * - {Object} [headers]: optional, request headers
  75. * - {Number|Array} [timeout]: request timeout(in milliseconds), default is `exports.TIMEOUTS containing connect timeout and response timeout`
  76. * - {Agent} [agent]: optional, http agent. Set `false` if you does not use agent.
  77. * - {Agent} [httpsAgent]: optional, https agent. Set `false` if you does not use agent.
  78. * - {String} [auth]: Basic authentication i.e. 'user:password' to compute an Authorization header.
  79. * - {String} [digestAuth]: Digest authentication i.e. 'user:password' to compute an Authorization header.
  80. * - {String|Buffer|Array} [ca]: An array of strings or Buffers of trusted certificates.
  81. * If this is omitted several well known "root" CAs will be used, like VeriSign.
  82. * These are used to authorize connections.
  83. * Notes: This is necessary only if the server uses the self-signed certificate
  84. * - {Boolean} [rejectUnauthorized]: If true, the server certificate is verified against the list of supplied CAs.
  85. * An 'error' event is emitted if verification fails. Default: true.
  86. * - {String|Buffer} [pfx]: A string or Buffer containing the private key,
  87. * certificate and CA certs of the server in PFX or PKCS12 format.
  88. * - {String|Buffer} [key]: A string or Buffer containing the private key of the client in PEM format.
  89. * Notes: This is necessary only if using the client certificate authentication
  90. * - {String|Buffer} [cert]: A string or Buffer containing the certificate key of the client in PEM format.
  91. * Notes: This is necessary only if using the client certificate authentication
  92. * - {String} [passphrase]: A string of passphrase for the private key or pfx.
  93. * - {String} [ciphers]: A string describing the ciphers to use or exclude.
  94. * - {String} [secureProtocol]: The SSL method to use, e.g. SSLv3_method to force SSL version 3.
  95. * The possible values depend on your installation of OpenSSL and are defined in the constant SSL_METHODS.
  96. * - {Boolean} [followRedirect]: Follow HTTP 3xx responses as redirects. defaults to false.
  97. * - {Number} [maxRedirects]: The maximum number of redirects to follow, defaults to 10.
  98. * - {Function(from, to)} [formatRedirectUrl]: Format the redirect url by your self. Default is `url.resolve(from, to)`
  99. * - {Function(options)} [beforeRequest]: Before request hook, you can change every thing here.
  100. * - {Boolean} [streaming]: let you get the res object when request connected, default is `false`. alias `customResponse`
  101. * - {Boolean} [gzip]: Accept gzip response content and auto decode it, default is `false`.
  102. * - {Boolean} [timing]: Enable timing or not, default is `false`.
  103. * - {Function} [lookup]: Custom DNS lookup function, default is `dns.lookup`.
  104. * Require node >= 4.0.0 and only work on `http` protocol.
  105. * - {Boolean} [enableProxy]: optional, enable proxy request. Default is `false`.
  106. * - {String|Object} [proxy]: optional proxy agent uri or options. Default is `null`.
  107. * @param {Function} [callback]: callback(error, data, res). If missing callback, will return a promise object.
  108. * @return {HttpRequest} req object.
  109. * @api public
  110. */
  111. exports.request = function request(url, args, callback) {
  112. // request(url, callback)
  113. if (arguments.length === 2 && typeof args === 'function') {
  114. callback = args;
  115. args = null;
  116. }
  117. if (typeof callback === 'function') {
  118. return exports.requestWithCallback(url, args, callback);
  119. }
  120. // Promise
  121. if (!_Promise) {
  122. _Promise = require('any-promise');
  123. }
  124. return new _Promise(function (resolve, reject) {
  125. exports.requestWithCallback(url, args, makeCallback(resolve, reject));
  126. });
  127. };
  128. // alias to curl
  129. exports.curl = exports.request;
  130. function makeCallback(resolve, reject) {
  131. return function (err, data, res) {
  132. if (err) {
  133. return reject(err);
  134. }
  135. resolve({
  136. data: data,
  137. status: res.statusCode,
  138. headers: res.headers,
  139. res: res
  140. });
  141. };
  142. }
  143. // yield urllib.requestThunk(url, args)
  144. exports.requestThunk = function requestThunk(url, args) {
  145. return function (callback) {
  146. exports.requestWithCallback(url, args, function (err, data, res) {
  147. if (err) {
  148. return callback(err);
  149. }
  150. callback(null, {
  151. data: data,
  152. status: res.statusCode,
  153. headers: res.headers,
  154. res: res
  155. });
  156. });
  157. };
  158. };
  159. exports.requestWithCallback = function requestWithCallback(url, args, callback) {
  160. // requestWithCallback(url, callback)
  161. if (!url || (typeof url !== 'string' && typeof url !== 'object')) {
  162. var msg = util.format('expect request url to be a string or a http request options, but got %j', url);
  163. throw new Error(msg);
  164. }
  165. if (arguments.length === 2 && typeof args === 'function') {
  166. callback = args;
  167. args = null;
  168. }
  169. args = args || {};
  170. if (REQUEST_ID >= MAX_VALUE) {
  171. REQUEST_ID = 0;
  172. }
  173. var reqId = ++REQUEST_ID;
  174. args.requestUrls = args.requestUrls || [];
  175. args.timeout = args.timeout || exports.TIMEOUTS;
  176. args.maxRedirects = args.maxRedirects || 10;
  177. args.streaming = args.streaming || args.customResponse;
  178. var requestStartTime = Date.now();
  179. var parsedUrl;
  180. if (typeof url === 'string') {
  181. if (!PROTO_RE.test(url)) {
  182. // Support `request('www.server.com')`
  183. url = 'http://' + url;
  184. }
  185. parsedUrl = urlutil.parse(url);
  186. } else {
  187. parsedUrl = url;
  188. }
  189. var reqMeta = {
  190. requestId: reqId,
  191. url: parsedUrl.href,
  192. args: args,
  193. ctx: args.ctx,
  194. };
  195. if (args.emitter) {
  196. args.emitter.emit('request', reqMeta);
  197. }
  198. var method = (args.type || args.method || parsedUrl.method || 'GET').toUpperCase();
  199. var port = parsedUrl.port || 80;
  200. var httplib = http;
  201. var agent = getAgent(args.agent, exports.agent);
  202. var fixJSONCtlChars = args.fixJSONCtlChars;
  203. if (parsedUrl.protocol === 'https:') {
  204. httplib = https;
  205. agent = getAgent(args.httpsAgent, exports.httpsAgent);
  206. if (!parsedUrl.port) {
  207. port = 443;
  208. }
  209. }
  210. // request through proxy tunnel
  211. var proxyTunnelAgent = detectProxyAgent(parsedUrl, args);
  212. if (proxyTunnelAgent) {
  213. agent = proxyTunnelAgent;
  214. }
  215. var options = {
  216. host: parsedUrl.hostname || parsedUrl.host || 'localhost',
  217. path: parsedUrl.path || '/',
  218. method: method,
  219. port: port,
  220. agent: agent,
  221. headers: {},
  222. // default is dns.lookup
  223. // https://github.com/nodejs/node/blob/master/lib/net.js#L986
  224. // custom dnslookup require node >= 4.0.0
  225. // https://github.com/nodejs/node/blob/archived-io.js-v0.12/lib/net.js#L952
  226. lookup: args.lookup,
  227. };
  228. if (args.headers) {
  229. for (var k in args.headers) {
  230. options.headers[k] = args.headers[k];
  231. }
  232. }
  233. var sslNames = [
  234. 'pfx',
  235. 'key',
  236. 'passphrase',
  237. 'cert',
  238. 'ca',
  239. 'ciphers',
  240. 'rejectUnauthorized',
  241. 'secureProtocol',
  242. 'secureOptions',
  243. ];
  244. for (var i = 0; i < sslNames.length; i++) {
  245. var name = sslNames[i];
  246. if (args.hasOwnProperty(name)) {
  247. options[name] = args[name];
  248. }
  249. }
  250. // don't check ssl
  251. if (options.rejectUnauthorized === false && !options.hasOwnProperty('secureOptions')) {
  252. options.secureOptions = require('constants').SSL_OP_NO_TLSv1_2;
  253. }
  254. var auth = args.auth || parsedUrl.auth;
  255. if (auth) {
  256. options.auth = auth;
  257. }
  258. var body = args.content || args.data;
  259. var dataAsQueryString = method === 'GET' || method === 'HEAD' || args.dataAsQueryString;
  260. if (!args.content) {
  261. if (body && !(typeof body === 'string' || Buffer.isBuffer(body))) {
  262. if (dataAsQueryString) {
  263. // read: GET, HEAD, use query string
  264. body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
  265. } else {
  266. var contentType = options.headers['Content-Type'] || options.headers['content-type'];
  267. // auto add application/x-www-form-urlencoded when using urlencode form request
  268. if (!contentType) {
  269. if (args.contentType === 'json') {
  270. contentType = 'application/json';
  271. } else {
  272. contentType = 'application/x-www-form-urlencoded';
  273. }
  274. options.headers['Content-Type'] = contentType;
  275. }
  276. if (parseContentType(contentType).type === 'application/json') {
  277. body = JSON.stringify(body);
  278. } else {
  279. // 'application/x-www-form-urlencoded'
  280. body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
  281. }
  282. }
  283. }
  284. }
  285. // if it's a GET or HEAD request, data should be sent as query string
  286. if (dataAsQueryString && body) {
  287. options.path += (parsedUrl.query ? '&' : '?') + body;
  288. body = null;
  289. }
  290. var requestSize = 0;
  291. if (body) {
  292. var length = body.length;
  293. if (!Buffer.isBuffer(body)) {
  294. length = Buffer.byteLength(body);
  295. }
  296. requestSize = options.headers['Content-Length'] = length;
  297. }
  298. if (args.dataType === 'json') {
  299. options.headers.Accept = 'application/json';
  300. }
  301. if (typeof args.beforeRequest === 'function') {
  302. // you can use this hook to change every thing.
  303. args.beforeRequest(options);
  304. }
  305. var connectTimer = null;
  306. var responseTimer = null;
  307. var __err = null;
  308. var connected = false; // socket connected or not
  309. var keepAliveSocket = false; // request with keepalive socket
  310. var responseSize = 0;
  311. var statusCode = -1;
  312. var responseAborted = false;
  313. var remoteAddress = '';
  314. var remotePort = '';
  315. var timing = null;
  316. if (args.timing) {
  317. timing = {
  318. // socket assigned
  319. queuing: 0,
  320. // dns lookup time
  321. dnslookup: 0,
  322. // socket connected
  323. connected: 0,
  324. // request sent
  325. requestSent: 0,
  326. // Time to first byte (TTFB)
  327. waiting: 0,
  328. contentDownload: 0,
  329. };
  330. }
  331. function cancelConnectTimer() {
  332. if (connectTimer) {
  333. clearTimeout(connectTimer);
  334. connectTimer = null;
  335. }
  336. }
  337. function cancelResponseTimer() {
  338. if (responseTimer) {
  339. clearTimeout(responseTimer);
  340. responseTimer = null;
  341. }
  342. }
  343. function done(err, data, res) {
  344. cancelResponseTimer();
  345. if (!callback) {
  346. console.warn('[urllib:warn] [%s] [%s] [worker:%s] %s %s callback twice!!!',
  347. Date(), reqId, process.pid, options.method, url);
  348. // https://github.com/node-modules/urllib/pull/30
  349. if (err) {
  350. console.warn('[urllib:warn] [%s] [%s] [worker:%s] %s: %s\nstack: %s',
  351. Date(), reqId, process.pid, err.name, err.message, err.stack);
  352. }
  353. return;
  354. }
  355. var cb = callback;
  356. callback = null;
  357. var headers = {};
  358. if (res) {
  359. statusCode = res.statusCode;
  360. headers = res.headers;
  361. }
  362. // handle digest auth
  363. if (statusCode === 401 && headers['www-authenticate']
  364. && !options.headers.Authorization && args.digestAuth) {
  365. var authenticate = headers['www-authenticate'];
  366. if (authenticate.indexOf('Digest ') >= 0) {
  367. debug('Request#%d %s: got digest auth header WWW-Authenticate: %s', reqId, url, authenticate);
  368. options.headers.Authorization = digestAuthHeader(options.method, options.path, authenticate, args.digestAuth);
  369. debug('Request#%d %s: auth with digest header: %s', reqId, url, options.headers.Authorization);
  370. if (res.headers['set-cookie']) {
  371. options.headers.Cookie = res.headers['set-cookie'].join(';');
  372. }
  373. args.headers = options.headers;
  374. return exports.requestWithCallback(url, args, cb);
  375. }
  376. }
  377. var requestUseTime = Date.now() - requestStartTime;
  378. if (timing) {
  379. timing.contentDownload = requestUseTime;
  380. }
  381. debug('[%sms] done, %s bytes HTTP %s %s %s %s, keepAliveSocket: %s, timing: %j',
  382. requestUseTime, responseSize, statusCode, options.method, options.host, options.path,
  383. keepAliveSocket, timing);
  384. var response = {
  385. status: statusCode,
  386. statusCode: statusCode,
  387. headers: headers,
  388. size: responseSize,
  389. aborted: responseAborted,
  390. rt: requestUseTime,
  391. keepAliveSocket: keepAliveSocket,
  392. data: data,
  393. requestUrls: args.requestUrls,
  394. timing: timing,
  395. remoteAddress: remoteAddress,
  396. remotePort: remotePort,
  397. };
  398. if (err) {
  399. var agentStatus = '';
  400. if (agent && typeof agent.getCurrentStatus === 'function') {
  401. // add current agent status to error message for logging and debug
  402. agentStatus = ', agent status: ' + JSON.stringify(agent.getCurrentStatus());
  403. }
  404. err.message += ', ' + options.method + ' ' + url + ' ' + statusCode
  405. + ' (connected: ' + connected + ', keepalive socket: ' + keepAliveSocket + agentStatus + ')'
  406. + '\nheaders: ' + JSON.stringify(headers);
  407. err.data = data;
  408. err.path = options.path;
  409. err.status = statusCode;
  410. err.headers = headers;
  411. err.res = response;
  412. }
  413. cb(err, data, args.streaming ? res : response);
  414. if (args.emitter) {
  415. // keep to use the same reqMeta object on request event before
  416. reqMeta.url = parsedUrl.href;
  417. reqMeta.socket = req && req.connection;
  418. reqMeta.options = options;
  419. reqMeta.size = requestSize;
  420. args.emitter.emit('response', {
  421. requestId: reqId,
  422. error: err,
  423. ctx: args.ctx,
  424. req: reqMeta,
  425. res: response,
  426. });
  427. }
  428. }
  429. function handleRedirect(res) {
  430. var err = null;
  431. if (args.followRedirect && statuses.redirect[res.statusCode]) { // handle redirect
  432. args._followRedirectCount = (args._followRedirectCount || 0) + 1;
  433. var location = res.headers.location;
  434. if (!location) {
  435. err = new Error('Got statusCode ' + res.statusCode + ' but cannot resolve next location from headers');
  436. err.name = 'FollowRedirectError';
  437. } else if (args._followRedirectCount > args.maxRedirects) {
  438. err = new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + url);
  439. err.name = 'MaxRedirectError';
  440. } else {
  441. var newUrl = args.formatRedirectUrl ? args.formatRedirectUrl(url, location) : urlutil.resolve(url, location);
  442. debug('Request#%d %s: `redirected` from %s to %s', reqId, options.path, url, newUrl);
  443. // make sure timer stop
  444. cancelResponseTimer();
  445. // should clean up headers.Host on `location: http://other-domain/url`
  446. if (options.headers.Host && PROTO_RE.test(location)) {
  447. options.headers.Host = null;
  448. args.headers = options.headers;
  449. }
  450. // avoid done will be execute in the future change.
  451. var cb = callback;
  452. callback = null;
  453. exports.requestWithCallback(newUrl, args, cb);
  454. return {
  455. redirect: true,
  456. error: null
  457. };
  458. }
  459. }
  460. return {
  461. redirect: false,
  462. error: err
  463. };
  464. }
  465. // set user-agent
  466. if (!options.headers['User-Agent'] && !options.headers['user-agent']) {
  467. options.headers['User-Agent'] = USER_AGENT;
  468. }
  469. if (args.gzip) {
  470. if (!options.headers['Accept-Encoding'] && !options.headers['accept-encoding']) {
  471. options.headers['Accept-Encoding'] = 'gzip';
  472. }
  473. }
  474. function decodeContent(res, body, cb) {
  475. var encoding = res.headers['content-encoding'];
  476. if (body.length === 0) {
  477. return cb(null, body, encoding);
  478. }
  479. if (!encoding || encoding.toLowerCase() !== 'gzip') {
  480. return cb(null, body, encoding);
  481. }
  482. debug('gunzip %d length body', body.length);
  483. zlib.gunzip(body, cb);
  484. }
  485. var writeStream = args.writeStream;
  486. debug('Request#%d %s %s with headers %j, options.path: %s',
  487. reqId, method, url, options.headers, options.path);
  488. args.requestUrls.push(parsedUrl.href);
  489. function onResponse(res) {
  490. if (timing) {
  491. timing.waiting = Date.now() - requestStartTime;
  492. }
  493. debug('Request#%d %s `req response` event emit: status %d, headers: %j',
  494. reqId, url, res.statusCode, res.headers);
  495. if (args.streaming) {
  496. var result = handleRedirect(res);
  497. if (result.redirect) {
  498. res.resume();
  499. return;
  500. }
  501. if (result.error) {
  502. res.resume();
  503. return done(result.error, null, res);
  504. }
  505. return done(null, null, res);
  506. }
  507. res.on('close', function () {
  508. debug('Request#%d %s: `res close` event emit, total size %d',
  509. reqId, url, responseSize);
  510. });
  511. res.on('error', function () {
  512. debug('Request#%d %s: `res error` event emit, total size %d',
  513. reqId, url, responseSize);
  514. });
  515. res.on('aborted', function () {
  516. responseAborted = true;
  517. debug('Request#%d %s: `res aborted` event emit, total size %d',
  518. reqId, url, responseSize);
  519. });
  520. if (writeStream) {
  521. // If there's a writable stream to recieve the response data, just pipe the
  522. // response stream to that writable stream and call the callback when it has
  523. // finished writing.
  524. //
  525. // NOTE that when the response stream `res` emits an 'end' event it just
  526. // means that it has finished piping data to another stream. In the
  527. // meanwhile that writable stream may still writing data to the disk until
  528. // it emits a 'close' event.
  529. //
  530. // That means that we should not apply callback until the 'close' of the
  531. // writable stream is emited.
  532. //
  533. // See also:
  534. // - https://github.com/TBEDP/urllib/commit/959ac3365821e0e028c231a5e8efca6af410eabb
  535. // - http://nodejs.org/api/stream.html#stream_event_end
  536. // - http://nodejs.org/api/stream.html#stream_event_close_1
  537. var result = handleRedirect(res);
  538. if (result.redirect) {
  539. res.resume();
  540. return;
  541. }
  542. if (result.error) {
  543. res.resume();
  544. // end ths stream first
  545. writeStream.end();
  546. return done(result.error, null, res);
  547. }
  548. // you can set consumeWriteStream false that only wait response end
  549. if (args.consumeWriteStream === false) {
  550. res.on('end', done.bind(null, null, null, res));
  551. } else {
  552. // node 0.10, 0.12: only emit res aborted, writeStream close not fired
  553. if (isNode010 || isNode012) {
  554. first([
  555. [ writeStream, 'close' ],
  556. [ res, 'aborted' ],
  557. ], function(_, stream, event) {
  558. debug('Request#%d %s: writeStream or res %s event emitted', reqId, url, event);
  559. done(__err || null, null, res);
  560. });
  561. } else {
  562. writeStream.on('close', function() {
  563. debug('Request#%d %s: writeStream close event emitted', reqId, url);
  564. done(__err || null, null, res);
  565. });
  566. }
  567. }
  568. return res.pipe(writeStream);
  569. }
  570. // Otherwise, just concat those buffers.
  571. //
  572. // NOTE that the `chunk` is not a String but a Buffer. It means that if
  573. // you simply concat two chunk with `+` you're actually converting both
  574. // Buffers into Strings before concating them. It'll cause problems when
  575. // dealing with multi-byte characters.
  576. //
  577. // The solution is to store each chunk in an array and concat them with
  578. // 'buffer-concat' when all chunks is recieved.
  579. //
  580. // See also:
  581. // http://cnodejs.org/topic/4faf65852e8fb5bc65113403
  582. var chunks = [];
  583. res.on('data', function (chunk) {
  584. debug('Request#%d %s: `res data` event emit, size %d', reqId, url, chunk.length);
  585. responseSize += chunk.length;
  586. chunks.push(chunk);
  587. });
  588. res.on('end', function () {
  589. var body = Buffer.concat(chunks, responseSize);
  590. debug('Request#%d %s: `res end` event emit, total size %d, _dumped: %s',
  591. reqId, url, responseSize, res._dumped);
  592. if (__err) {
  593. // req.abort() after `res data` event emit.
  594. return done(__err, body, res);
  595. }
  596. var result = handleRedirect(res);
  597. if (result.error) {
  598. return done(result.error, body, res);
  599. }
  600. if (result.redirect) {
  601. return;
  602. }
  603. decodeContent(res, body, function (err, data, encoding) {
  604. if (err) {
  605. return done(err, body, res);
  606. }
  607. // if body not decode, dont touch it
  608. if (!encoding && TEXT_DATA_TYPES.indexOf(args.dataType) >= 0) {
  609. // try to decode charset
  610. try {
  611. data = decodeBodyByCharset(data, res);
  612. } catch (e) {
  613. debug('decodeBodyByCharset error: %s', e);
  614. // if error, dont touch it
  615. return done(null, data, res);
  616. }
  617. if (args.dataType === 'json') {
  618. if (responseSize === 0) {
  619. data = null;
  620. } else {
  621. var r = parseJSON(data, fixJSONCtlChars);
  622. if (r.error) {
  623. err = r.error;
  624. } else {
  625. data = r.data;
  626. }
  627. }
  628. }
  629. }
  630. if (responseAborted) {
  631. // err = new Error('Remote socket was terminated before `response.end()` was called');
  632. // err.name = 'RemoteSocketClosedError';
  633. debug('Request#%d %s: Remote socket was terminated before `response.end()` was called', reqId, url);
  634. }
  635. done(err, data, res);
  636. });
  637. });
  638. }
  639. var connectTimeout, responseTimeout;
  640. if (Array.isArray(args.timeout)) {
  641. connectTimeout = ms(args.timeout[0]);
  642. responseTimeout = ms(args.timeout[1]);
  643. } else { // set both timeout equal
  644. connectTimeout = responseTimeout = ms(args.timeout);
  645. }
  646. debug('ConnectTimeout: %d, ResponseTimeout: %d', connectTimeout, responseTimeout);
  647. function startConnectTimer() {
  648. debug('Connect timer ticking, timeout: %d', connectTimeout);
  649. connectTimer = setTimeout(function () {
  650. connectTimer = null;
  651. if (statusCode === -1) {
  652. statusCode = -2;
  653. }
  654. var msg = 'Connect timeout for ' + connectTimeout + 'ms';
  655. var errorName = 'ConnectionTimeoutError';
  656. if (!req.socket) {
  657. errorName = 'SocketAssignTimeoutError';
  658. msg += ', working sockets is full';
  659. }
  660. __err = new Error(msg);
  661. __err.name = errorName;
  662. __err.requestId = reqId;
  663. debug('ConnectTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
  664. abortRequest();
  665. }, connectTimeout);
  666. }
  667. function startResposneTimer() {
  668. debug('Response timer ticking, timeout: %d', responseTimeout);
  669. responseTimer = setTimeout(function () {
  670. responseTimer = null;
  671. var msg = 'Response timeout for ' + responseTimeout + 'ms';
  672. var errorName = 'ResponseTimeoutError';
  673. __err = new Error(msg);
  674. __err.name = errorName;
  675. __err.requestId = reqId;
  676. debug('ResponseTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
  677. abortRequest();
  678. }, responseTimeout);
  679. }
  680. var req;
  681. // request headers checker will throw error
  682. try {
  683. req = httplib.request(options, onResponse);
  684. } catch (err) {
  685. return done(err);
  686. }
  687. // environment detection: browser or nodejs
  688. if (typeof(window) === 'undefined') {
  689. // start connect timer just after `request` return, and just in nodejs environment
  690. startConnectTimer();
  691. }
  692. function abortRequest() {
  693. debug('Request#%d %s abort, connected: %s', reqId, url, connected);
  694. // it wont case error event when req haven't been assigned a socket yet.
  695. if (!req.socket) {
  696. __err.noSocket = true;
  697. done(__err);
  698. }
  699. req.abort();
  700. }
  701. if (timing) {
  702. // request sent
  703. req.on('finish', function() {
  704. timing.requestSent = Date.now() - requestStartTime;
  705. });
  706. }
  707. req.once('socket', function (socket) {
  708. if (timing) {
  709. // socket queuing time
  710. timing.queuing = Date.now() - requestStartTime;
  711. }
  712. // https://github.com/nodejs/node/blob/master/lib/net.js#L377
  713. // https://github.com/nodejs/node/blob/v0.10.40-release/lib/net.js#L352
  714. // should use socket.socket on 0.10.x
  715. if (isNode010 && socket.socket) {
  716. socket = socket.socket;
  717. }
  718. var readyState = socket.readyState;
  719. if (readyState === 'opening') {
  720. socket.once('lookup', function(err, ip, addressType) {
  721. debug('Request#%d %s lookup: %s, %s, %s', reqId, url, err, ip, addressType);
  722. if (timing) {
  723. timing.dnslookup = Date.now() - requestStartTime;
  724. }
  725. if (ip) {
  726. remoteAddress = ip;
  727. }
  728. });
  729. socket.once('connect', function() {
  730. if (timing) {
  731. // socket connected
  732. timing.connected = Date.now() - requestStartTime;
  733. }
  734. // cancel socket timer at first and start tick for TTFB
  735. cancelConnectTimer();
  736. startResposneTimer();
  737. debug('Request#%d %s new socket connected', reqId, url);
  738. connected = true;
  739. if (!remoteAddress) {
  740. remoteAddress = socket.remoteAddress;
  741. }
  742. remotePort = socket.remotePort;
  743. });
  744. return;
  745. }
  746. debug('Request#%d %s reuse socket connected, readyState: %s', reqId, url, readyState);
  747. connected = true;
  748. keepAliveSocket = true;
  749. if (!remoteAddress) {
  750. remoteAddress = socket.remoteAddress;
  751. }
  752. remotePort = socket.remotePort;
  753. // reuse socket, timer should be canceled.
  754. cancelConnectTimer();
  755. startResposneTimer();
  756. });
  757. req.on('error', function (err) {
  758. if (err.name === 'Error') {
  759. err.name = connected ? 'ResponseError' : 'RequestError';
  760. }
  761. err.message += ' (req "error")';
  762. debug('Request#%d %s `req error` event emit, %s: %s', reqId, url, err.name, err.message);
  763. done(__err || err);
  764. });
  765. if (writeStream) {
  766. writeStream.once('error', function (err) {
  767. err.message += ' (writeStream "error")';
  768. __err = err;
  769. debug('Request#%d %s `writeStream error` event emit, %s: %s', reqId, url, err.name, err.message);
  770. abortRequest();
  771. });
  772. }
  773. if (args.stream) {
  774. args.stream.pipe(req);
  775. args.stream.once('error', function (err) {
  776. err.message += ' (stream "error")';
  777. __err = err;
  778. debug('Request#%d %s `readStream error` event emit, %s: %s', reqId, url, err.name, err.message);
  779. abortRequest();
  780. });
  781. } else {
  782. req.end(body);
  783. }
  784. req.requestId = reqId;
  785. return req;
  786. };
  787. var JSONCtlCharsMap = {
  788. '"': '\\"', // \u0022
  789. '\\': '\\\\', // \u005c
  790. '\b': '\\b', // \u0008
  791. '\f': '\\f', // \u000c
  792. '\n': '\\n', // \u000a
  793. '\r': '\\r', // \u000d
  794. '\t': '\\t' // \u0009
  795. };
  796. var JSONCtlCharsRE = /[\u0000-\u001F\u005C]/g;
  797. function _replaceOneChar(c) {
  798. return JSONCtlCharsMap[c] || '\\u' + (c.charCodeAt(0) + 0x10000).toString(16).substr(1);
  799. }
  800. function replaceJSONCtlChars(str) {
  801. return str.replace(JSONCtlCharsRE, _replaceOneChar);
  802. }
  803. function parseJSON(data, fixJSONCtlChars) {
  804. var result = {
  805. error: null,
  806. data: null
  807. };
  808. if (fixJSONCtlChars) {
  809. if (typeof fixJSONCtlChars === 'function') {
  810. data = fixJSONCtlChars(data);
  811. } else {
  812. // https://github.com/node-modules/urllib/pull/77
  813. // remote the control characters (U+0000 through U+001F)
  814. data = replaceJSONCtlChars(data);
  815. }
  816. }
  817. try {
  818. result.data = JSON.parse(data);
  819. } catch (err) {
  820. if (err.name === 'SyntaxError') {
  821. err.name = 'JSONResponseFormatError';
  822. }
  823. if (data.length > 1024) {
  824. // show 0~512 ... -512~end data
  825. err.message += ' (data json format: ' +
  826. JSON.stringify(data.slice(0, 512)) + ' ...skip... ' + JSON.stringify(data.slice(data.length - 512)) + ')';
  827. } else {
  828. err.message += ' (data json format: ' + JSON.stringify(data) + ')';
  829. }
  830. result.error = err;
  831. }
  832. return result;
  833. }
  834. /**
  835. * decode response body by parse `content-type`'s charset
  836. * @param {Buffer} data
  837. * @param {Http(s)Response} res
  838. * @return {String}
  839. */
  840. function decodeBodyByCharset(data, res) {
  841. var type = res.headers['content-type'];
  842. if (!type) {
  843. return data.toString();
  844. }
  845. var type = parseContentType(type);
  846. var charset = type.parameters.charset || 'utf-8';
  847. if (!Buffer.isEncoding(charset)) {
  848. if (!_iconv) {
  849. _iconv = require('iconv-lite');
  850. }
  851. return _iconv.decode(data, charset);
  852. }
  853. return data.toString(charset);
  854. }
  855. function getAgent(agent, defaultAgent) {
  856. return agent === undefined ? defaultAgent : agent;
  857. }
  858. function parseContentType(str) {
  859. try {
  860. return contentTypeParser.parse(str);
  861. } catch (err) {
  862. // ignore content-type error, tread as default
  863. return { parameters: {} };
  864. }
  865. }