You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
7.0 KiB

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