index.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. 'use strict';
  2. const zlib = require('zlib');
  3. const http = require('http');
  4. const https = require('https');
  5. const parse = require('url').parse;
  6. const format = require('url').format;
  7. const debugBody = require('debug')('httpx:body');
  8. const debugHeader = require('debug')('httpx:header');
  9. const httpAgent = new http.Agent({ keepAlive: true });
  10. const httpsAgent = new https.Agent({ keepAlive: true });
  11. const TIMEOUT = 3000; // 3s
  12. const READ_TIMER = Symbol('TIMER::READ_TIMER');
  13. const READ_TIME_OUT = Symbol('TIMER::READ_TIME_OUT');
  14. const READ_TIMER_START_AT = Symbol('TIMER::READ_TIMER_START_AT');
  15. var append = function (err, name, message) {
  16. err.name = name + err.name;
  17. err.message = `${message}. ${err.message}`;
  18. return err;
  19. };
  20. const isNumber = function (num) {
  21. return num !== null && !isNaN(num);
  22. };
  23. exports.request = function (url, opts) {
  24. // request(url)
  25. opts || (opts = {});
  26. const parsed = typeof url === 'string' ? parse(url) : url;
  27. let readTimeout, connectTimeout;
  28. if (isNumber(opts.readTimeout) || isNumber(opts.connectTimeout)) {
  29. readTimeout = isNumber(opts.readTimeout) ? Number(opts.readTimeout) : TIMEOUT;
  30. connectTimeout = isNumber(opts.connectTimeout) ? Number(opts.connectTimeout) : TIMEOUT;
  31. } else if (isNumber(opts.timeout)) {
  32. readTimeout = connectTimeout = Number(opts.timeout);
  33. } else {
  34. readTimeout = connectTimeout = TIMEOUT;
  35. }
  36. const isHttps = parsed.protocol === 'https:';
  37. const method = (opts.method || 'GET').toUpperCase();
  38. const defaultAgent = isHttps ? httpsAgent : httpAgent;
  39. const agent = opts.agent || defaultAgent;
  40. var options = {
  41. host: parsed.hostname || 'localhost',
  42. path: parsed.path || '/',
  43. method: method,
  44. port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
  45. agent: agent,
  46. headers: opts.headers || {},
  47. // ssl config
  48. key: opts.key || '',
  49. cert: opts.cert || '',
  50. ca: opts.ca || '',
  51. // connect timerout
  52. timeout: connectTimeout
  53. };
  54. if (isHttps && typeof opts.rejectUnauthorized !== 'undefined') {
  55. options.rejectUnauthorized = opts.rejectUnauthorized;
  56. }
  57. if (opts.compression) {
  58. options.headers['accept-encoding'] = 'gzip,deflate';
  59. }
  60. const httplib = isHttps ? https : http;
  61. if (typeof opts.beforeRequest === 'function') {
  62. options = opts.beforeRequest(options);
  63. }
  64. return new Promise((resolve, reject) => {
  65. const request = httplib.request(options);
  66. const body = opts.data;
  67. var fulfilled = (response) => {
  68. if (debugHeader.enabled) {
  69. const requestHeaders = response.req._header;
  70. requestHeaders.split('\r\n').forEach((line) => {
  71. debugHeader('> %s', line);
  72. });
  73. debugHeader('< HTTP/%s %s %s', response.httpVersion, response.statusCode, response.statusMessage);
  74. Object.keys(response.headers).forEach((key) => {
  75. debugHeader('< %s: %s', key, response.headers[key]);
  76. });
  77. }
  78. resolve(response);
  79. };
  80. var rejected = (err) => {
  81. err.message += `${method} ${format(parsed)} failed.`;
  82. // clear response timer when error
  83. if (request.socket && request.socket[READ_TIMER]) {
  84. clearTimeout(request.socket[READ_TIMER]);
  85. }
  86. reject(err);
  87. };
  88. var abort = (err) => {
  89. request.abort();
  90. rejected(err);
  91. };
  92. const startResponseTimer = function (socket) {
  93. const timer = setTimeout(() => {
  94. if (socket[READ_TIMER]) {
  95. clearTimeout(socket[READ_TIMER]);
  96. socket[READ_TIMER] = null;
  97. }
  98. var err = new Error();
  99. var message = `ReadTimeout(${readTimeout})`;
  100. abort(append(err, 'RequestTimeout', message));
  101. }, readTimeout);
  102. // start read-timer
  103. socket[READ_TIME_OUT] = readTimeout;
  104. socket[READ_TIMER] = timer;
  105. socket[READ_TIMER_START_AT] = Date.now();
  106. };
  107. // string
  108. if (!body || 'string' === typeof body || body instanceof Buffer) {
  109. if (debugBody.enabled) {
  110. if (!body) {
  111. debugBody('<no request body>');
  112. } else if ('string' === typeof body) {
  113. debugBody(body);
  114. } else {
  115. debugBody(`Buffer <ignored>, Buffer length: ${body.length}`);
  116. }
  117. }
  118. request.end(body);
  119. } else if ('function' === typeof body.pipe) { // stream
  120. body.pipe(request);
  121. if (debugBody.enabled) {
  122. debugBody('<request body is a stream>');
  123. }
  124. body.once('error', (err) => {
  125. abort(append(err, 'HttpX', 'Stream occor error'));
  126. });
  127. }
  128. request.on('response', fulfilled);
  129. request.on('error', rejected);
  130. request.once('socket', function (socket) {
  131. // reuse socket
  132. if (socket.readyState === 'opening') {
  133. socket.once('connect', function () {
  134. startResponseTimer(socket);
  135. });
  136. } else {
  137. startResponseTimer(socket);
  138. }
  139. });
  140. });
  141. };
  142. exports.read = function (response, encoding) {
  143. var readable = response;
  144. switch (response.headers['content-encoding']) {
  145. // or, just use zlib.createUnzip() to handle both cases
  146. case 'gzip':
  147. readable = response.pipe(zlib.createGunzip());
  148. break;
  149. case 'deflate':
  150. readable = response.pipe(zlib.createInflate());
  151. break;
  152. default:
  153. break;
  154. }
  155. return new Promise((resolve, reject) => {
  156. // node.js 14 use response.client
  157. const socket = response.socket || response.client;
  158. const makeReadTimeoutError = () => {
  159. const req = response.req;
  160. var err = new Error();
  161. err.name = 'RequestTimeoutError';
  162. err.message = `ReadTimeout: ${socket[READ_TIME_OUT]}. ${req.method} ${req.path} failed.`;
  163. return err;
  164. };
  165. // check read-timer
  166. let readTimer;
  167. const oldReadTimer = socket[READ_TIMER];
  168. if (!oldReadTimer) {
  169. reject(makeReadTimeoutError());
  170. return;
  171. }
  172. const remainTime = socket[READ_TIME_OUT] - (Date.now() - socket[READ_TIMER_START_AT]);
  173. clearTimeout(oldReadTimer);
  174. if (remainTime <= 0) {
  175. reject(makeReadTimeoutError());
  176. return;
  177. }
  178. readTimer = setTimeout(function () {
  179. reject(makeReadTimeoutError());
  180. }, remainTime);
  181. // start reading data
  182. var onError, onData, onEnd;
  183. var cleanup = function () {
  184. // cleanup
  185. readable.removeListener('error', onError);
  186. readable.removeListener('data', onData);
  187. readable.removeListener('end', onEnd);
  188. // clear read timer
  189. if (readTimer) {
  190. clearTimeout(readTimer);
  191. }
  192. };
  193. const bufs = [];
  194. var size = 0;
  195. onData = function (buf) {
  196. bufs.push(buf);
  197. size += buf.length;
  198. };
  199. onError = function (err) {
  200. cleanup();
  201. reject(err);
  202. };
  203. onEnd = function () {
  204. cleanup();
  205. var buff = Buffer.concat(bufs, size);
  206. debugBody('');
  207. if (encoding) {
  208. const result = buff.toString(encoding);
  209. debugBody(result);
  210. return resolve(result);
  211. }
  212. if (debugBody.enabled) {
  213. debugBody(buff.toString());
  214. }
  215. resolve(buff);
  216. };
  217. readable.on('error', onError);
  218. readable.on('data', onData);
  219. readable.on('end', onEnd);
  220. });
  221. };