| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- 'use strict';
- const zlib = require('zlib');
- const http = require('http');
- const https = require('https');
- const parse = require('url').parse;
- const format = require('url').format;
- const debugBody = require('debug')('httpx:body');
- const debugHeader = require('debug')('httpx:header');
- const httpAgent = new http.Agent({ keepAlive: true });
- const httpsAgent = new https.Agent({ keepAlive: true });
- const TIMEOUT = 3000; // 3s
- const READ_TIMER = Symbol('TIMER::READ_TIMER');
- const READ_TIME_OUT = Symbol('TIMER::READ_TIME_OUT');
- const READ_TIMER_START_AT = Symbol('TIMER::READ_TIMER_START_AT');
- var append = function (err, name, message) {
- err.name = name + err.name;
- err.message = `${message}. ${err.message}`;
- return err;
- };
- const isNumber = function (num) {
- return num !== null && !isNaN(num);
- };
- exports.request = function (url, opts) {
- // request(url)
- opts || (opts = {});
- const parsed = typeof url === 'string' ? parse(url) : url;
- let readTimeout, connectTimeout;
- if (isNumber(opts.readTimeout) || isNumber(opts.connectTimeout)) {
- readTimeout = isNumber(opts.readTimeout) ? Number(opts.readTimeout) : TIMEOUT;
- connectTimeout = isNumber(opts.connectTimeout) ? Number(opts.connectTimeout) : TIMEOUT;
- } else if (isNumber(opts.timeout)) {
- readTimeout = connectTimeout = Number(opts.timeout);
- } else {
- readTimeout = connectTimeout = TIMEOUT;
- }
- const isHttps = parsed.protocol === 'https:';
- const method = (opts.method || 'GET').toUpperCase();
- const defaultAgent = isHttps ? httpsAgent : httpAgent;
- const agent = opts.agent || defaultAgent;
- var options = {
- host: parsed.hostname || 'localhost',
- path: parsed.path || '/',
- method: method,
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
- agent: agent,
- headers: opts.headers || {},
- // ssl config
- key: opts.key || '',
- cert: opts.cert || '',
- ca: opts.ca || '',
- // connect timerout
- timeout: connectTimeout
- };
- if (isHttps && typeof opts.rejectUnauthorized !== 'undefined') {
- options.rejectUnauthorized = opts.rejectUnauthorized;
- }
- if (opts.compression) {
- options.headers['accept-encoding'] = 'gzip,deflate';
- }
- const httplib = isHttps ? https : http;
- if (typeof opts.beforeRequest === 'function') {
- options = opts.beforeRequest(options);
- }
- return new Promise((resolve, reject) => {
- const request = httplib.request(options);
- const body = opts.data;
- var fulfilled = (response) => {
- if (debugHeader.enabled) {
- const requestHeaders = response.req._header;
- requestHeaders.split('\r\n').forEach((line) => {
- debugHeader('> %s', line);
- });
- debugHeader('< HTTP/%s %s %s', response.httpVersion, response.statusCode, response.statusMessage);
- Object.keys(response.headers).forEach((key) => {
- debugHeader('< %s: %s', key, response.headers[key]);
- });
- }
- resolve(response);
- };
- var rejected = (err) => {
- err.message += `${method} ${format(parsed)} failed.`;
- // clear response timer when error
- if (request.socket && request.socket[READ_TIMER]) {
- clearTimeout(request.socket[READ_TIMER]);
- }
- reject(err);
- };
- var abort = (err) => {
- request.abort();
- rejected(err);
- };
- const startResponseTimer = function (socket) {
- const timer = setTimeout(() => {
- if (socket[READ_TIMER]) {
- clearTimeout(socket[READ_TIMER]);
- socket[READ_TIMER] = null;
- }
- var err = new Error();
- var message = `ReadTimeout(${readTimeout})`;
- abort(append(err, 'RequestTimeout', message));
- }, readTimeout);
- // start read-timer
- socket[READ_TIME_OUT] = readTimeout;
- socket[READ_TIMER] = timer;
- socket[READ_TIMER_START_AT] = Date.now();
- };
- // string
- if (!body || 'string' === typeof body || body instanceof Buffer) {
- if (debugBody.enabled) {
- if (!body) {
- debugBody('<no request body>');
- } else if ('string' === typeof body) {
- debugBody(body);
- } else {
- debugBody(`Buffer <ignored>, Buffer length: ${body.length}`);
- }
- }
- request.end(body);
- } else if ('function' === typeof body.pipe) { // stream
- body.pipe(request);
- if (debugBody.enabled) {
- debugBody('<request body is a stream>');
- }
- body.once('error', (err) => {
- abort(append(err, 'HttpX', 'Stream occor error'));
- });
- }
- request.on('response', fulfilled);
- request.on('error', rejected);
- request.once('socket', function (socket) {
- // reuse socket
- if (socket.readyState === 'opening') {
- socket.once('connect', function () {
- startResponseTimer(socket);
- });
- } else {
- startResponseTimer(socket);
- }
- });
- });
- };
- exports.read = function (response, encoding) {
- var readable = response;
- switch (response.headers['content-encoding']) {
- // or, just use zlib.createUnzip() to handle both cases
- case 'gzip':
- readable = response.pipe(zlib.createGunzip());
- break;
- case 'deflate':
- readable = response.pipe(zlib.createInflate());
- break;
- default:
- break;
- }
- return new Promise((resolve, reject) => {
- // node.js 14 use response.client
- const socket = response.socket || response.client;
- const makeReadTimeoutError = () => {
- const req = response.req;
- var err = new Error();
- err.name = 'RequestTimeoutError';
- err.message = `ReadTimeout: ${socket[READ_TIME_OUT]}. ${req.method} ${req.path} failed.`;
- return err;
- };
- // check read-timer
- let readTimer;
- const oldReadTimer = socket[READ_TIMER];
- if (!oldReadTimer) {
- reject(makeReadTimeoutError());
- return;
- }
- const remainTime = socket[READ_TIME_OUT] - (Date.now() - socket[READ_TIMER_START_AT]);
- clearTimeout(oldReadTimer);
- if (remainTime <= 0) {
- reject(makeReadTimeoutError());
- return;
- }
- readTimer = setTimeout(function () {
- reject(makeReadTimeoutError());
- }, remainTime);
- // start reading data
- var onError, onData, onEnd;
- var cleanup = function () {
- // cleanup
- readable.removeListener('error', onError);
- readable.removeListener('data', onData);
- readable.removeListener('end', onEnd);
- // clear read timer
- if (readTimer) {
- clearTimeout(readTimer);
- }
- };
- const bufs = [];
- var size = 0;
- onData = function (buf) {
- bufs.push(buf);
- size += buf.length;
- };
- onError = function (err) {
- cleanup();
- reject(err);
- };
- onEnd = function () {
- cleanup();
- var buff = Buffer.concat(bufs, size);
- debugBody('');
- if (encoding) {
- const result = buff.toString(encoding);
- debugBody(result);
- return resolve(result);
- }
- if (debugBody.enabled) {
- debugBody(buff.toString());
- }
- resolve(buff);
- };
- readable.on('error', onError);
- readable.on('data', onData);
- readable.on('end', onEnd);
- });
- };
|