2023-10-03 11:14:36 +08:00
|
|
|
'use strict';
|
|
|
|
const util = require('util');
|
|
|
|
const urlutil = require('url');
|
|
|
|
const http = require('http');
|
|
|
|
const https = require('https');
|
|
|
|
const debug = require('debug')('urllib');
|
|
|
|
const ms = require('humanize-ms');
|
|
|
|
let REQUEST_ID = 0;
|
|
|
|
const MAX_VALUE = Math.pow(2, 31) - 10;
|
|
|
|
const PROTO_RE = /^https?:\/\//i;
|
|
|
|
|
|
|
|
function getAgent(agent, defaultAgent) {
|
|
|
|
return agent === undefined ? defaultAgent : agent;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseContentType(str) {
|
|
|
|
if (!str) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
return str.split(';')[0].trim().toLowerCase();
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeCallback(resolve, reject) {
|
|
|
|
return function (err, data, res) {
|
|
|
|
if (err) {
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
resolve({
|
|
|
|
data: data,
|
|
|
|
status: res.statusCode,
|
|
|
|
headers: res.headers,
|
|
|
|
res: res
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// exports.TIMEOUT = ms('5s');
|
|
|
|
exports.TIMEOUTS = [ms('300s'), ms('300s')];
|
|
|
|
|
|
|
|
const TEXT_DATA_TYPES = ['json', 'text'];
|
|
|
|
|
|
|
|
exports.request = function request(url, args, callback) {
|
|
|
|
// request(url, callback)
|
|
|
|
if (arguments.length === 2 && typeof args === 'function') {
|
|
|
|
callback = args;
|
|
|
|
args = null;
|
|
|
|
}
|
|
|
|
if (typeof callback === 'function') {
|
|
|
|
return exports.requestWithCallback(url, args, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
exports.requestWithCallback(url, args, makeCallback(resolve, reject));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.requestWithCallback = function requestWithCallback(url, args, callback) {
|
|
|
|
if (!url || (typeof url !== 'string' && typeof url !== 'object')) {
|
|
|
|
const msg = util.format('expect request url to be a string or a http request options, but got' + ' %j', url);
|
|
|
|
throw new Error(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (arguments.length === 2 && typeof args === 'function') {
|
|
|
|
callback = args;
|
|
|
|
args = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
args = args || {};
|
|
|
|
if (REQUEST_ID >= MAX_VALUE) {
|
|
|
|
REQUEST_ID = 0;
|
|
|
|
}
|
|
|
|
const reqId = ++REQUEST_ID;
|
|
|
|
|
|
|
|
args.requestUrls = args.requestUrls || [];
|
|
|
|
|
|
|
|
const reqMeta = {
|
|
|
|
requestId: reqId,
|
|
|
|
url: url,
|
|
|
|
args: args,
|
|
|
|
ctx: args.ctx
|
|
|
|
};
|
|
|
|
if (args.emitter) {
|
|
|
|
args.emitter.emit('request', reqMeta);
|
|
|
|
}
|
|
|
|
|
|
|
|
args.timeout = args.timeout || exports.TIMEOUTS;
|
|
|
|
args.maxRedirects = args.maxRedirects || 10;
|
|
|
|
args.streaming = args.streaming || args.customResponse;
|
|
|
|
const requestStartTime = Date.now();
|
|
|
|
let parsedUrl;
|
|
|
|
|
|
|
|
if (typeof url === 'string') {
|
|
|
|
if (!PROTO_RE.test(url)) {
|
|
|
|
// Support `request('www.server.com')`
|
|
|
|
url = 'https://' + url;
|
|
|
|
}
|
|
|
|
parsedUrl = urlutil.parse(url);
|
|
|
|
} else {
|
|
|
|
parsedUrl = url;
|
|
|
|
}
|
|
|
|
|
|
|
|
const method = (args.type || args.method || parsedUrl.method || 'GET').toUpperCase();
|
|
|
|
let port = parsedUrl.port || 80;
|
|
|
|
let httplib = http;
|
|
|
|
let agent = getAgent(args.agent, exports.agent);
|
|
|
|
const fixJSONCtlChars = args.fixJSONCtlChars;
|
|
|
|
|
|
|
|
if (parsedUrl.protocol === 'https:') {
|
|
|
|
httplib = https;
|
|
|
|
agent = getAgent(args.httpsAgent, exports.httpsAgent);
|
|
|
|
|
|
|
|
if (!parsedUrl.port) {
|
|
|
|
port = 443;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// request through proxy tunnel
|
|
|
|
// var proxyTunnelAgent = detectProxyAgent(parsedUrl, args);
|
|
|
|
// if (proxyTunnelAgent) {
|
|
|
|
// agent = proxyTunnelAgent;
|
|
|
|
// }
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
host: parsedUrl.hostname || parsedUrl.host || 'localhost',
|
|
|
|
path: parsedUrl.path || '/',
|
|
|
|
method: method,
|
|
|
|
port: port,
|
|
|
|
agent: agent,
|
|
|
|
headers: args.headers || {},
|
|
|
|
// default is dns.lookup
|
|
|
|
// https://github.com/nodejs/node/blob/master/lib/net.js#L986
|
|
|
|
// custom dnslookup require node >= 4.0.0
|
|
|
|
// https://github.com/nodejs/node/blob/archived-io.js-v0.12/lib/net.js#L952
|
|
|
|
lookup: args.lookup
|
|
|
|
};
|
|
|
|
|
|
|
|
if (Array.isArray(args.timeout)) {
|
|
|
|
options.requestTimeout = args.timeout[args.timeout.length - 1];
|
|
|
|
} else if (typeof args.timeout !== 'undefined') {
|
|
|
|
options.requestTimeout = args.timeout;
|
|
|
|
}
|
|
|
|
|
|
|
|
// const sslNames = [
|
|
|
|
// 'pfx',
|
|
|
|
// 'key',
|
|
|
|
// 'passphrase',
|
|
|
|
// 'cert',
|
|
|
|
// 'ca',
|
|
|
|
// 'ciphers',
|
|
|
|
// 'rejectUnauthorized',
|
|
|
|
// 'secureProtocol',
|
|
|
|
// 'secureOptions',
|
|
|
|
// ];
|
|
|
|
// for (let i = 0; i < sslNames.length; i++) {
|
|
|
|
// const name = sslNames[i];
|
|
|
|
// if (args.hasOwnProperty(name)) {
|
|
|
|
// options[name] = args[name];
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// don't check ssl
|
|
|
|
// if (options.rejectUnauthorized === false && !options.hasOwnProperty('secureOptions')) {
|
|
|
|
// options.secureOptions = require('constants').SSL_OP_NO_TLSv1_2;
|
|
|
|
// }
|
|
|
|
|
|
|
|
const auth = args.auth || parsedUrl.auth;
|
|
|
|
if (auth) {
|
|
|
|
options.auth = auth;
|
|
|
|
}
|
|
|
|
|
|
|
|
// content undefined data 有值
|
|
|
|
let body = args.content || args.data;
|
|
|
|
const dataAsQueryString = method === 'GET' || method === 'HEAD' || args.dataAsQueryString;
|
|
|
|
if (!args.content) {
|
|
|
|
if (body && !(typeof body === 'string' || Buffer.isBuffer(body))) {
|
|
|
|
if (dataAsQueryString) {
|
|
|
|
// read: GET, HEAD, use query string
|
|
|
|
body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
|
|
|
|
} else {
|
|
|
|
let contentType = options.headers['Content-Type'] || options.headers['content-type'];
|
|
|
|
// auto add application/x-www-form-urlencoded when using urlencode form request
|
|
|
|
if (!contentType) {
|
|
|
|
if (args.contentType === 'json') {
|
|
|
|
contentType = 'application/json';
|
|
|
|
} else {
|
|
|
|
contentType = 'application/x-www-form-urlencoded';
|
|
|
|
}
|
|
|
|
options.headers['Content-Type'] = contentType;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parseContentType(contentType) === 'application/json') {
|
|
|
|
body = JSON.stringify(body);
|
|
|
|
} else {
|
|
|
|
// 'application/x-www-form-urlencoded'
|
|
|
|
body = args.nestedQuerystring ? qs.stringify(body) : querystring.stringify(body);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if it's a GET or HEAD request, data should be sent as query string
|
|
|
|
if (dataAsQueryString && body) {
|
|
|
|
options.path += (parsedUrl.query ? '&' : '?') + body;
|
|
|
|
body = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let requestSize = 0;
|
|
|
|
if (body) {
|
|
|
|
let length = body.length;
|
|
|
|
if (!Buffer.isBuffer(body)) {
|
|
|
|
length = Buffer.byteLength(body);
|
|
|
|
}
|
|
|
|
requestSize = options.headers['Content-Length'] = length;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.dataType === 'json') {
|
|
|
|
options.headers.Accept = 'application/json';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof args.beforeRequest === 'function') {
|
|
|
|
// you can use this hook to change every thing.
|
|
|
|
args.beforeRequest(options);
|
|
|
|
}
|
|
|
|
let connectTimer = null;
|
|
|
|
let responseTimer = null;
|
|
|
|
let __err = null;
|
|
|
|
let connected = false; // socket connected or not
|
|
|
|
let keepAliveSocket = false; // request with keepalive socket
|
|
|
|
let responseSize = 0;
|
|
|
|
let statusCode = -1;
|
|
|
|
let responseAborted = false;
|
|
|
|
let remoteAddress = '';
|
|
|
|
let remotePort = '';
|
|
|
|
let timing = null;
|
|
|
|
if (args.timing) {
|
|
|
|
timing = {
|
|
|
|
// socket assigned
|
|
|
|
queuing: 0,
|
|
|
|
// dns lookup time
|
|
|
|
dnslookup: 0,
|
|
|
|
// socket connected
|
|
|
|
connected: 0,
|
|
|
|
// request sent
|
|
|
|
requestSent: 0,
|
|
|
|
// Time to first byte (TTFB)
|
|
|
|
waiting: 0,
|
|
|
|
contentDownload: 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function cancelConnectTimer() {
|
|
|
|
if (connectTimer) {
|
|
|
|
clearTimeout(connectTimer);
|
|
|
|
connectTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function cancelResponseTimer() {
|
|
|
|
if (responseTimer) {
|
|
|
|
clearTimeout(responseTimer);
|
|
|
|
responseTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function done(err, data, res) {
|
|
|
|
cancelResponseTimer();
|
|
|
|
if (!callback) {
|
|
|
|
console.warn(
|
|
|
|
'[urllib:warn] [%s] [%s] [worker:%s] %s %s callback twice!!!',
|
|
|
|
Date(),
|
|
|
|
reqId,
|
|
|
|
process.pid,
|
|
|
|
options.method,
|
|
|
|
url
|
|
|
|
);
|
|
|
|
// https://github.com/node-modules/urllib/pull/30
|
|
|
|
if (err) {
|
|
|
|
console.warn(
|
|
|
|
'[urllib:warn] [%s] [%s] [worker:%s] %s: %s\nstack: %s',
|
|
|
|
Date(),
|
|
|
|
reqId,
|
|
|
|
process.pid,
|
|
|
|
err.name,
|
|
|
|
err.message,
|
|
|
|
err.stack
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const cb = callback;
|
|
|
|
callback = null;
|
|
|
|
let headers = {};
|
|
|
|
if (res) {
|
|
|
|
statusCode = res.statusCode;
|
|
|
|
headers = res.headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle digest auth
|
|
|
|
// if (statusCode === 401 && headers['www-authenticate']
|
|
|
|
// && (!args.headers || !args.headers.Authorization) && args.digestAuth) {
|
|
|
|
// const authenticate = headers['www-authenticate'];
|
|
|
|
// if (authenticate.indexOf('Digest ') >= 0) {
|
|
|
|
// debug('Request#%d %s: got digest auth header WWW-Authenticate: %s', reqId, url, authenticate);
|
|
|
|
// args.headers = args.headers || {};
|
|
|
|
// args.headers.Authorization = digestAuthHeader(options.method, options.path, authenticate, args.digestAuth);
|
|
|
|
// debug('Request#%d %s: auth with digest header: %s', reqId, url, args.headers.Authorization);
|
|
|
|
// if (res.headers['set-cookie']) {
|
|
|
|
// args.headers.Cookie = res.headers['set-cookie'].join(';');
|
|
|
|
// }
|
|
|
|
// return exports.requestWithCallback(url, args, cb);
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
const requestUseTime = Date.now() - requestStartTime;
|
|
|
|
if (timing) {
|
|
|
|
timing.contentDownload = requestUseTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
debug(
|
|
|
|
'[%sms] done, %s bytes HTTP %s %s %s %s, keepAliveSocket: %s, timing: %j',
|
|
|
|
requestUseTime,
|
|
|
|
responseSize,
|
|
|
|
statusCode,
|
|
|
|
options.method,
|
|
|
|
options.host,
|
|
|
|
options.path,
|
|
|
|
keepAliveSocket,
|
|
|
|
timing
|
|
|
|
);
|
|
|
|
|
|
|
|
const response = {
|
|
|
|
status: statusCode,
|
|
|
|
statusCode: statusCode,
|
|
|
|
headers: headers,
|
|
|
|
size: responseSize,
|
|
|
|
aborted: responseAborted,
|
|
|
|
rt: requestUseTime,
|
|
|
|
keepAliveSocket: keepAliveSocket,
|
|
|
|
data: data,
|
|
|
|
requestUrls: args.requestUrls,
|
|
|
|
timing: timing,
|
|
|
|
remoteAddress: remoteAddress,
|
|
|
|
remotePort: remotePort
|
|
|
|
};
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
let agentStatus = '';
|
|
|
|
if (agent && typeof agent.getCurrentStatus === 'function') {
|
|
|
|
// add current agent status to error message for logging and debug
|
|
|
|
agentStatus = ', agent status: ' + JSON.stringify(agent.getCurrentStatus());
|
|
|
|
}
|
|
|
|
err.message +=
|
|
|
|
', ' +
|
|
|
|
options.method +
|
|
|
|
' ' +
|
|
|
|
url +
|
|
|
|
' ' +
|
|
|
|
statusCode +
|
|
|
|
' (connected: ' +
|
|
|
|
connected +
|
|
|
|
', keepalive socket: ' +
|
|
|
|
keepAliveSocket +
|
|
|
|
agentStatus +
|
|
|
|
')' +
|
|
|
|
'\nheaders: ' +
|
|
|
|
JSON.stringify(headers);
|
|
|
|
err.data = data;
|
|
|
|
err.path = options.path;
|
|
|
|
err.status = statusCode;
|
|
|
|
err.headers = headers;
|
|
|
|
err.res = response;
|
|
|
|
}
|
|
|
|
|
|
|
|
cb(err, data, args.streaming ? res : response);
|
|
|
|
|
|
|
|
if (args.emitter) {
|
|
|
|
// keep to use the same reqMeta object on request event before
|
|
|
|
reqMeta.url = url;
|
|
|
|
reqMeta.socket = req && req.connection;
|
|
|
|
reqMeta.options = options;
|
|
|
|
reqMeta.size = requestSize;
|
|
|
|
|
|
|
|
args.emitter.emit('response', {
|
|
|
|
requestId: reqId,
|
|
|
|
error: err,
|
|
|
|
ctx: args.ctx,
|
|
|
|
req: reqMeta,
|
|
|
|
res: response
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleRedirect(res) {
|
|
|
|
let err = null;
|
|
|
|
if (args.followRedirect && statuses.redirect[res.statusCode]) {
|
|
|
|
// handle redirect
|
|
|
|
args._followRedirectCount = (args._followRedirectCount || 0) + 1;
|
|
|
|
const location = res.headers.location;
|
|
|
|
if (!location) {
|
|
|
|
err = new Error('Got statusCode ' + res.statusCode + ' but cannot resolve next location from headers');
|
|
|
|
err.name = 'FollowRedirectError';
|
|
|
|
} else if (args._followRedirectCount > args.maxRedirects) {
|
|
|
|
err = new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + url);
|
|
|
|
err.name = 'MaxRedirectError';
|
|
|
|
} else {
|
|
|
|
const newUrl = args.formatRedirectUrl ? args.formatRedirectUrl(url, location) : urlutil.resolve(url, location);
|
|
|
|
debug('Request#%d %s: `redirected` from %s to %s', reqId, options.path, url, newUrl);
|
|
|
|
// make sure timer stop
|
|
|
|
cancelResponseTimer();
|
|
|
|
// should clean up headers.Host on `location: http://other-domain/url`
|
|
|
|
if (args.headers && args.headers.Host && PROTO_RE.test(location)) {
|
|
|
|
args.headers.Host = null;
|
|
|
|
}
|
|
|
|
// avoid done will be execute in the future change.
|
|
|
|
const cb = callback;
|
|
|
|
callback = null;
|
|
|
|
exports.requestWithCallback(newUrl, args, cb);
|
|
|
|
return {
|
|
|
|
redirect: true,
|
|
|
|
error: null
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
redirect: false,
|
|
|
|
error: err
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.gzip) {
|
|
|
|
if (!options.headers['Accept-Encoding'] && !options.headers['accept-encoding']) {
|
|
|
|
options.headers['Accept-Encoding'] = 'gzip';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function decodeContent(res, body, cb) {
|
|
|
|
const encoding = res.headers['content-encoding'];
|
|
|
|
// if (body.length === 0) {
|
|
|
|
// return cb(null, body, encoding);
|
|
|
|
// }
|
|
|
|
|
|
|
|
// if (!encoding || encoding.toLowerCase() !== 'gzip') {
|
|
|
|
return cb(null, body, encoding);
|
|
|
|
// }
|
|
|
|
|
|
|
|
// debug('gunzip %d length body', body.length);
|
|
|
|
// zlib.gunzip(body, cb);
|
|
|
|
}
|
|
|
|
|
|
|
|
const writeStream = args.writeStream;
|
|
|
|
|
|
|
|
debug('Request#%d %s %s with headers %j, options.path: %s', reqId, method, url, options.headers, options.path);
|
|
|
|
|
|
|
|
args.requestUrls.push(url);
|
|
|
|
|
|
|
|
function onResponse(res) {
|
|
|
|
if (timing) {
|
|
|
|
timing.waiting = Date.now() - requestStartTime;
|
|
|
|
}
|
|
|
|
debug('Request#%d %s `req response` event emit: status %d, headers: %j', reqId, url, res.statusCode, res.headers);
|
|
|
|
|
|
|
|
if (args.streaming) {
|
|
|
|
const result = handleRedirect(res);
|
|
|
|
if (result.redirect) {
|
|
|
|
res.resume();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (result.error) {
|
|
|
|
res.resume();
|
|
|
|
return done(result.error, null, res);
|
|
|
|
}
|
|
|
|
|
|
|
|
return done(null, null, res);
|
|
|
|
}
|
|
|
|
|
|
|
|
res.on('close', function () {
|
|
|
|
debug('Request#%d %s: `res close` event emit, total size %d', reqId, url, responseSize);
|
|
|
|
});
|
|
|
|
|
|
|
|
res.on('error', function () {
|
|
|
|
debug('Request#%d %s: `res error` event emit, total size %d', reqId, url, responseSize);
|
|
|
|
});
|
|
|
|
|
|
|
|
res.on('aborted', function () {
|
|
|
|
responseAborted = true;
|
|
|
|
debug('Request#%d %s: `res aborted` event emit, total size %d', reqId, url, responseSize);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (writeStream) {
|
|
|
|
// If there's a writable stream to recieve the response data, just pipe the
|
|
|
|
// response stream to that writable stream and call the callback when it has
|
|
|
|
// finished writing.
|
|
|
|
//
|
|
|
|
// NOTE that when the response stream `res` emits an 'end' event it just
|
|
|
|
// means that it has finished piping data to another stream. In the
|
|
|
|
// meanwhile that writable stream may still writing data to the disk until
|
|
|
|
// it emits a 'close' event.
|
|
|
|
//
|
|
|
|
// That means that we should not apply callback until the 'close' of the
|
|
|
|
// writable stream is emited.
|
|
|
|
//
|
|
|
|
// See also:
|
|
|
|
// - https://github.com/TBEDP/urllib/commit/959ac3365821e0e028c231a5e8efca6af410eabb
|
|
|
|
// - http://nodejs.org/api/stream.html#stream_event_end
|
|
|
|
// - http://nodejs.org/api/stream.html#stream_event_close_1
|
|
|
|
const result = handleRedirect(res);
|
|
|
|
if (result.redirect) {
|
|
|
|
res.resume();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (result.error) {
|
|
|
|
res.resume();
|
|
|
|
// end ths stream first
|
|
|
|
writeStream.end();
|
|
|
|
return done(result.error, null, res);
|
|
|
|
}
|
|
|
|
// you can set consumeWriteStream false that only wait response end
|
|
|
|
if (args.consumeWriteStream === false) {
|
|
|
|
res.on('end', done.bind(null, null, null, res));
|
|
|
|
} else {
|
|
|
|
// node 0.10, 0.12: only emit res aborted, writeStream close not fired
|
|
|
|
// if (isNode010 || isNode012) {
|
|
|
|
// first([
|
|
|
|
// [ writeStream, 'close' ],
|
|
|
|
// [ res, 'aborted' ],
|
|
|
|
// ], function(_, stream, event) {
|
|
|
|
// debug('Request#%d %s: writeStream or res %s event emitted', reqId, url, event);
|
|
|
|
// done(__err || null, null, res);
|
|
|
|
// });
|
|
|
|
if (false) {
|
|
|
|
} else {
|
|
|
|
writeStream.on('close', function () {
|
|
|
|
debug('Request#%d %s: writeStream close event emitted', reqId, url);
|
|
|
|
done(__err || null, null, res);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res.pipe(writeStream);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, just concat those buffers.
|
|
|
|
//
|
|
|
|
// NOTE that the `chunk` is not a String but a Buffer. It means that if
|
|
|
|
// you simply concat two chunk with `+` you're actually converting both
|
|
|
|
// Buffers into Strings before concating them. It'll cause problems when
|
|
|
|
// dealing with multi-byte characters.
|
|
|
|
//
|
|
|
|
// The solution is to store each chunk in an array and concat them with
|
|
|
|
// 'buffer-concat' when all chunks is recieved.
|
|
|
|
//
|
|
|
|
// See also:
|
|
|
|
// http://cnodejs.org/topic/4faf65852e8fb5bc65113403
|
|
|
|
|
|
|
|
const chunks = [];
|
|
|
|
|
|
|
|
res.on('data', function (chunk) {
|
|
|
|
debug('Request#%d %s: `res data` event emit, size %d', reqId, url, chunk.length);
|
|
|
|
responseSize += chunk.length;
|
|
|
|
chunks.push(chunk);
|
|
|
|
});
|
|
|
|
|
|
|
|
res.on('end', function () {
|
|
|
|
const body = Buffer.concat(chunks, responseSize);
|
|
|
|
debug('Request#%d %s: `res end` event emit, total size %d, _dumped: %s', reqId, url, responseSize, res._dumped);
|
|
|
|
|
|
|
|
if (__err) {
|
|
|
|
// req.abort() after `res data` event emit.
|
|
|
|
return done(__err, body, res);
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = handleRedirect(res);
|
|
|
|
if (result.error) {
|
|
|
|
return done(result.error, body, res);
|
|
|
|
}
|
|
|
|
if (result.redirect) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
decodeContent(res, body, function (err, data, encoding) {
|
|
|
|
if (err) {
|
|
|
|
return done(err, body, res);
|
|
|
|
}
|
|
|
|
// if body not decode, dont touch it
|
|
|
|
if (!encoding && TEXT_DATA_TYPES.indexOf(args.dataType) >= 0) {
|
|
|
|
// try to decode charset
|
|
|
|
try {
|
|
|
|
data = decodeBodyByCharset(data, res);
|
|
|
|
} catch (e) {
|
|
|
|
debug('decodeBodyByCharset error: %s', e);
|
|
|
|
// if error, dont touch it
|
|
|
|
return done(null, data, res);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.dataType === 'json') {
|
|
|
|
if (responseSize === 0) {
|
|
|
|
data = null;
|
|
|
|
} else {
|
|
|
|
const r = parseJSON(data, fixJSONCtlChars);
|
|
|
|
if (r.error) {
|
|
|
|
err = r.error;
|
|
|
|
} else {
|
|
|
|
data = r.data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (responseAborted) {
|
|
|
|
// err = new Error('Remote socket was terminated before `response.end()` was called');
|
|
|
|
// err.name = 'RemoteSocketClosedError';
|
|
|
|
debug('Request#%d %s: Remote socket was terminated before `response.end()` was called', reqId, url);
|
|
|
|
}
|
|
|
|
|
|
|
|
done(err, data, res);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let connectTimeout, responseTimeout;
|
|
|
|
if (Array.isArray(args.timeout)) {
|
|
|
|
connectTimeout = ms(args.timeout[0]);
|
|
|
|
responseTimeout = ms(args.timeout[1]);
|
|
|
|
} else {
|
|
|
|
// set both timeout equal
|
|
|
|
connectTimeout = responseTimeout = ms(args.timeout);
|
|
|
|
}
|
|
|
|
debug('ConnectTimeout: %d, ResponseTimeout: %d', connectTimeout, responseTimeout);
|
|
|
|
|
|
|
|
function startConnectTimer() {
|
|
|
|
debug('Connect timer ticking, timeout: %d', connectTimeout);
|
|
|
|
connectTimer = setTimeout(function () {
|
|
|
|
connectTimer = null;
|
|
|
|
if (statusCode === -1) {
|
|
|
|
statusCode = -2;
|
|
|
|
}
|
|
|
|
let msg = 'Connect timeout for ' + connectTimeout + 'ms';
|
|
|
|
let errorName = 'ConnectionTimeoutError';
|
|
|
|
if (!req.socket) {
|
|
|
|
errorName = 'SocketAssignTimeoutError';
|
|
|
|
msg += ', working sockets is full';
|
|
|
|
}
|
|
|
|
__err = new Error(msg);
|
|
|
|
__err.name = errorName;
|
|
|
|
__err.requestId = reqId;
|
|
|
|
debug('ConnectTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
|
|
|
|
abortRequest();
|
|
|
|
}, connectTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
function startResposneTimer() {
|
|
|
|
debug('Response timer ticking, timeout: %d', responseTimeout);
|
|
|
|
responseTimer = setTimeout(function () {
|
|
|
|
responseTimer = null;
|
|
|
|
const msg = 'Response timeout for ' + responseTimeout + 'ms';
|
|
|
|
const errorName = 'ResponseTimeoutError';
|
|
|
|
__err = new Error(msg);
|
|
|
|
__err.name = errorName;
|
|
|
|
__err.requestId = reqId;
|
|
|
|
debug('ResponseTimeout: Request#%d %s %s: %s, connected: %s', reqId, url, __err.name, msg, connected);
|
|
|
|
abortRequest();
|
|
|
|
}, responseTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
let req;
|
|
|
|
// request headers checker will throw error
|
|
|
|
options.mode = args.mode ? args.mode : '';
|
|
|
|
try {
|
|
|
|
req = httplib.request(options, onResponse);
|
|
|
|
} catch (err) {
|
|
|
|
return done(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
// environment detection: browser or nodejs
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
// start connect timer just after `request` return, and just in nodejs environment
|
|
|
|
startConnectTimer();
|
|
|
|
} else {
|
|
|
|
req.on('requestTimeout', function () {
|
|
|
|
if (statusCode === -1) {
|
|
|
|
statusCode = -2;
|
|
|
|
}
|
|
|
|
const msg = 'Connect timeout for ' + connectTimeout + 'ms';
|
|
|
|
const errorName = 'ConnectionTimeoutError';
|
|
|
|
__err = new Error(msg);
|
|
|
|
__err.name = errorName;
|
|
|
|
__err.requestId = reqId;
|
|
|
|
abortRequest();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function abortRequest() {
|
|
|
|
debug('Request#%d %s abort, connected: %s', reqId, url, connected);
|
|
|
|
// it wont case error event when req haven't been assigned a socket yet.
|
|
|
|
if (!req.socket) {
|
|
|
|
__err.noSocket = true;
|
|
|
|
done(__err);
|
|
|
|
}
|
|
|
|
req.abort();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (timing) {
|
|
|
|
// request sent
|
|
|
|
req.on('finish', function () {
|
|
|
|
timing.requestSent = Date.now() - requestStartTime;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
req.once('socket', function (socket) {
|
|
|
|
if (timing) {
|
|
|
|
// socket queuing time
|
|
|
|
timing.queuing = Date.now() - requestStartTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://github.com/nodejs/node/blob/master/lib/net.js#L377
|
|
|
|
// https://github.com/nodejs/node/blob/v0.10.40-release/lib/net.js#L352
|
|
|
|
// should use socket.socket on 0.10.x
|
|
|
|
// if (isNode010 && socket.socket) {
|
|
|
|
// socket = socket.socket;
|
|
|
|
// }
|
|
|
|
|
|
|
|
const readyState = socket.readyState;
|
|
|
|
if (readyState === 'opening') {
|
|
|
|
socket.once('lookup', function (err, ip, addressType) {
|
|
|
|
debug('Request#%d %s lookup: %s, %s, %s', reqId, url, err, ip, addressType);
|
|
|
|
if (timing) {
|
|
|
|
timing.dnslookup = Date.now() - requestStartTime;
|
|
|
|
}
|
|
|
|
if (ip) {
|
|
|
|
remoteAddress = ip;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
socket.once('connect', function () {
|
|
|
|
if (timing) {
|
|
|
|
// socket connected
|
|
|
|
timing.connected = Date.now() - requestStartTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
// cancel socket timer at first and start tick for TTFB
|
|
|
|
cancelConnectTimer();
|
|
|
|
startResposneTimer();
|
|
|
|
|
|
|
|
debug('Request#%d %s new socket connected', reqId, url);
|
|
|
|
connected = true;
|
|
|
|
if (!remoteAddress) {
|
|
|
|
remoteAddress = socket.remoteAddress;
|
|
|
|
}
|
|
|
|
remotePort = socket.remotePort;
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
debug('Request#%d %s reuse socket connected, readyState: %s', reqId, url, readyState);
|
|
|
|
connected = true;
|
|
|
|
keepAliveSocket = true;
|
|
|
|
if (!remoteAddress) {
|
|
|
|
remoteAddress = socket.remoteAddress;
|
|
|
|
}
|
|
|
|
remotePort = socket.remotePort;
|
|
|
|
|
|
|
|
// reuse socket, timer should be canceled.
|
|
|
|
cancelConnectTimer();
|
|
|
|
startResposneTimer();
|
|
|
|
});
|
|
|
|
|
|
|
|
req.on('error', function (err) {
|
|
|
|
//TypeError for browser fetch api, Error for browser xmlhttprequest api
|
|
|
|
if (err.name === 'Error' || err.name === 'TypeError') {
|
|
|
|
err.name = connected ? 'ResponseError' : 'RequestError';
|
|
|
|
}
|
|
|
|
err.message += ' (req "error")';
|
|
|
|
debug('Request#%d %s `req error` event emit, %s: %s', reqId, url, err.name, err.message);
|
|
|
|
done(__err || err);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (writeStream) {
|
|
|
|
writeStream.once('error', function (err) {
|
|
|
|
err.message += ' (writeStream "error")';
|
|
|
|
__err = err;
|
|
|
|
debug('Request#%d %s `writeStream error` event emit, %s: %s', reqId, url, err.name, err.message);
|
|
|
|
abortRequest();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.stream) {
|
|
|
|
args.stream.pipe(req);
|
|
|
|
args.stream.once('error', function (err) {
|
|
|
|
err.message += ' (stream "error")';
|
|
|
|
__err = err;
|
|
|
|
debug('Request#%d %s `readStream error` event emit, %s: %s', reqId, url, err.name, err.message);
|
|
|
|
abortRequest();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
req.end(body);
|
|
|
|
}
|
|
|
|
|
|
|
|
req.requestId = reqId;
|
|
|
|
return req;
|
|
|
|
};
|