discord-baymax-bot/node_modules/superagent/lib/node/index.js

1094 lines
24 KiB
JavaScript
Raw Permalink Normal View History

2017-03-24 04:52:08 +00:00
/**
* Module dependencies.
*/
var debug = require('debug')('superagent');
var formidable = require('formidable');
var FormData = require('form-data');
var Response = require('./response');
var parse = require('url').parse;
var format = require('url').format;
var resolve = require('url').resolve;
var methods = require('methods');
var Stream = require('stream');
var utils = require('./utils');
var extend = require('extend');
var Part = require('./part');
var mime = require('mime');
var https = require('https');
var http = require('http');
var fs = require('fs');
var qs = require('qs');
var zlib = require('zlib');
var util = require('util');
var pkg = require('../../package.json');
var requestBase = require('../request-base');
var isObject = require('../is-object');
/**
* Expose the request function.
*/
var request = exports = module.exports = require('../request').bind(null, Request);
/**
* Expose the agent function
*/
exports.agent = require('./agent');
/**
* Expose `Part`.
*/
exports.Part = Part;
/**
* Noop.
*/
function noop(){};
/**
* Expose `Response`.
*/
exports.Response = Response;
/**
* Define "form" mime type.
*/
mime.define({
'application/x-www-form-urlencoded': ['form', 'urlencoded', 'form-data']
});
/**
* Protocol map.
*/
exports.protocols = {
'http:': http,
'https:': https
};
/**
* Default serialization map.
*
* superagent.serialize['application/xml'] = function(obj){
* return 'generated xml here';
* };
*
*/
exports.serialize = {
'application/x-www-form-urlencoded': qs.stringify,
'application/json': JSON.stringify
};
/**
* Default parsers.
*
* superagent.parse['application/xml'] = function(res, fn){
* fn(null, res);
* };
*
*/
exports.parse = require('./parsers');
/**
* Initialize internal header tracking properties on a request instance.
*
* @param {Object} req the instance
* @api private
*/
function _initHeaders(req) {
var ua = 'node-superagent/' + pkg.version;
req._header = { // coerces header names to lowercase
'user-agent': ua
};
req.header = { // preserves header name case
'User-Agent': ua
};
}
/**
* Initialize a new `Request` with the given `method` and `url`.
*
* @param {String} method
* @param {String|Object} url
* @api public
*/
function Request(method, url) {
Stream.call(this);
var self = this;
if ('string' != typeof url) url = format(url);
this._agent = false;
this._formData = null;
this.method = method;
this.url = url;
_initHeaders(this);
this.writable = true;
this._redirects = 0;
this.redirects(5);
this.cookies = '';
this.qs = {};
this.qsRaw = [];
this._redirectList = [];
this._streamRequest = false;
this.on('end', this.clearTimeout.bind(this));
}
/**
* Inherit from `Stream` (which inherits from `EventEmitter`).
* Mixin `requestBase`.
*/
util.inherits(Request, Stream);
for (var key in requestBase) {
Request.prototype[key] = requestBase[key];
}
/**
* Queue the given `file` as an attachment to the specified `field`,
* with optional `filename`.
*
* ``` js
* request.post('http://localhost/upload')
* .attach(new Buffer('<b>Hello world</b>'), 'hello.html')
* .end(callback);
* ```
*
* A filename may also be used:
*
* ``` js
* request.post('http://localhost/upload')
* .attach('files', 'image.jpg')
* .end(callback);
* ```
*
* @param {String} field
* @param {String|fs.ReadStream|Buffer} file
* @param {String} filename
* @return {Request} for chaining
* @api public
*/
Request.prototype.attach = function(field, file, filename){
if ('string' == typeof file) {
if (!filename) filename = file;
debug('creating `fs.ReadStream` instance for file: %s', file);
file = fs.createReadStream(file);
} else if (!filename && file.path) {
filename = file.path;
}
this._getFormData().append(field, file, { filename: filename });
return this;
};
Request.prototype._getFormData = function() {
if (!this._formData) {
this._formData = new FormData();
this._formData.on('error', function(err) {
this.emit('error', err);
this.abort();
}.bind(this));
}
return this._formData;
};
/**
* Set the max redirects to `n`.
*
* @param {Number} n
* @return {Request} for chaining
* @api public
*/
Request.prototype.redirects = function(n){
debug('max redirects %s', n);
this._maxRedirects = n;
return this;
};
/**
* Return a new `Part` for this request.
*
* @return {Part}
* @api public
* @deprecated pass a readable stream in to `Request#attach()` instead
*/
Request.prototype.part = util.deprecate(function(){
return new Part(this);
}, '`Request#part()` is deprecated. ' +
'Pass a readable stream in to `Request#attach()` instead.');
/**
* Gets/sets the `Agent` to use for this HTTP request. The default (if this
* function is not called) is to opt out of connection pooling (`agent: false`).
*
* @param {http.Agent} agent
* @return {http.Agent}
* @api public
*/
Request.prototype.agent = function(agent){
if (!arguments.length) return this._agent;
this._agent = agent;
return this;
};
/**
* Set _Content-Type_ response header passed through `mime.lookup()`.
*
* Examples:
*
* request.post('/')
* .type('xml')
* .send(xmlstring)
* .end(callback);
*
* request.post('/')
* .type('json')
* .send(jsonstring)
* .end(callback);
*
* request.post('/')
* .type('application/json')
* .send(jsonstring)
* .end(callback);
*
* @param {String} type
* @return {Request} for chaining
* @api public
*/
Request.prototype.type = function(type){
return this.set('Content-Type', ~type.indexOf('/')
? type
: mime.lookup(type));
};
/**
* Set _Accept_ response header passed through `mime.lookup()`.
*
* Examples:
*
* superagent.types.json = 'application/json';
*
* request.get('/agent')
* .accept('json')
* .end(callback);
*
* request.get('/agent')
* .accept('application/json')
* .end(callback);
*
* @param {String} accept
* @return {Request} for chaining
* @api public
*/
Request.prototype.accept = function(type){
return this.set('Accept', ~type.indexOf('/')
? type
: mime.lookup(type));
};
/**
* Add query-string `val`.
*
* Examples:
*
* request.get('/shoes')
* .query('size=10')
* .query({ color: 'blue' })
*
* @param {Object|String} val
* @return {Request} for chaining
* @api public
*/
Request.prototype.query = function(val){
if ('string' == typeof val) {
this.qsRaw.push(val);
return this;
}
extend(this.qs, val);
return this;
};
/**
* Send `data` as the request body, defaulting the `.type()` to "json" when
* an object is given.
*
* Examples:
*
* // manual json
* request.post('/user')
* .type('json')
* .send('{"name":"tj"}')
* .end(callback)
*
* // auto json
* request.post('/user')
* .send({ name: 'tj' })
* .end(callback)
*
* // manual x-www-form-urlencoded
* request.post('/user')
* .type('form')
* .send('name=tj')
* .end(callback)
*
* // auto x-www-form-urlencoded
* request.post('/user')
* .type('form')
* .send({ name: 'tj' })
* .end(callback)
*
* // string defaults to x-www-form-urlencoded
* request.post('/user')
* .send('name=tj')
* .send('foo=bar')
* .send('bar=baz')
* .end(callback)
*
* @param {String|Object} data
* @return {Request} for chaining
* @api public
*/
Request.prototype.send = function(data){
var obj = isObject(data);
var type = this._header['content-type'];
// merge
if (obj && isObject(this._data)) {
for (var key in data) {
this._data[key] = data[key];
}
// string
} else if ('string' == typeof data) {
// default to x-www-form-urlencoded
if (!type) this.type('form');
type = this._header['content-type'];
// concat &
if ('application/x-www-form-urlencoded' == type) {
this._data = this._data
? this._data + '&' + data
: data;
} else {
this._data = (this._data || '') + data;
}
} else {
this._data = data;
}
if (!obj) return this;
// default to json
if (!type) this.type('json');
return this;
};
/**
* Write raw `data` / `encoding` to the socket.
*
* @param {Buffer|String} data
* @param {String} encoding
* @return {Boolean}
* @api public
*/
Request.prototype.write = function(data, encoding){
var req = this.request();
if (!this._streamRequest) {
this._streamRequest = true;
try {
// ensure querystring is appended before headers are sent
this.appendQueryString(req);
} catch (e) {
return this.emit('error', e);
}
}
return req.write(data, encoding);
};
/**
* Pipe the request body to `stream`.
*
* @param {Stream} stream
* @param {Object} options
* @return {Stream}
* @api public
*/
Request.prototype.pipe = function(stream, options){
this.piped = true; // HACK...
this.buffer(false);
var self = this;
this.end().req.on('response', function(res){
// redirect
var redirect = isRedirect(res.statusCode);
if (redirect && self._redirects++ != self._maxRedirects) {
return self.redirect(res).pipe(stream, options);
}
if (self._shouldUnzip(res)) {
res.pipe(zlib.createUnzip()).pipe(stream, options);
} else {
res.pipe(stream, options);
}
res.on('end', function(){
self.emit('end');
});
});
return stream;
};
/**
* Enable / disable buffering.
*
* @return {Boolean} [val]
* @return {Request} for chaining
* @api public
*/
Request.prototype.buffer = function(val){
this._buffer = false === val
? false
: true;
return this;
};
/**
* Abort and clear timeout.
*
* @api public
*/
Request.prototype.abort = function(){
debug('abort %s %s', this.method, this.url);
this._aborted = true;
this.clearTimeout();
this.req.abort();
this.emit('abort');
};
/**
* Redirect to `url
*
* @param {IncomingMessage} res
* @return {Request} for chaining
* @api private
*/
Request.prototype.redirect = function(res){
var url = res.headers.location;
if (!url) {
return this.callback(new Error('No location header for redirect'), res);
}
debug('redirect %s -> %s', this.url, url);
// location
url = resolve(this.url, url);
// ensure the response is being consumed
// this is required for Node v0.10+
res.resume();
var headers = this.req._headers;
var shouldStripCookie = parse(url).host !== parse(this.url).host;
// implementation of 302 following defacto standard
if (res.statusCode == 301 || res.statusCode == 302){
// strip Content-* related fields
// in case of POST etc
headers = utils.cleanHeader(this.req._headers, shouldStripCookie);
// force GET
this.method = 'HEAD' == this.method
? 'HEAD'
: 'GET';
// clear data
this._data = null;
}
// 303 is always GET
if (res.statusCode == 303) {
// strip Content-* related fields
// in case of POST etc
headers = utils.cleanHeader(this.req._headers, shouldStripCookie);
// force method
this.method = 'GET';
// clear data
this._data = null;
}
// 307 preserves method
// 308 preserves method
delete headers.host;
delete this.req;
delete this._formData;
// remove all add header except User-Agent
_initHeaders(this)
// redirect
this.url = url;
this._redirectList.push(url);
this.emit('redirect', res);
this.qs = {};
this.qsRaw = [];
this.set(headers);
this.end(this._callback);
return this;
};
/**
* Set Authorization field value with `user` and `pass`.
*
* Examples:
*
* .auth('tobi', 'learnboost')
* .auth('tobi:learnboost')
* .auth('tobi')
*
* @param {String} user
* @param {String} pass
* @return {Request} for chaining
* @api public
*/
Request.prototype.auth = function(user, pass){
if (1 === arguments.length) pass = '';
if (!~user.indexOf(':')) user = user + ':';
var str = new Buffer(user + pass).toString('base64');
return this.set('Authorization', 'Basic ' + str);
};
/**
* Set the certificate authority option for https request.
*
* @param {Buffer | Array} cert
* @return {Request} for chaining
* @api public
*/
Request.prototype.ca = function(cert){
this._ca = cert;
return this;
};
/**
* Return an http[s] request.
*
* @return {OutgoingMessage}
* @api private
*/
Request.prototype.request = function(){
if (this.req) return this.req;
var self = this;
var options = {};
var data = this._data;
var url = this.url;
// default to http://
if (0 != url.indexOf('http')) url = 'http://' + url;
url = parse(url);
// options
options.method = this.method;
options.port = url.port;
options.path = url.pathname;
options.host = url.hostname;
options.ca = this._ca;
options.agent = this._agent;
// initiate request
var mod = exports.protocols[url.protocol];
// request
var req = this.req = mod.request(options);
if ('HEAD' != options.method) req.setHeader('Accept-Encoding', 'gzip, deflate');
this.protocol = url.protocol;
this.host = url.host;
// expose events
req.on('drain', function(){ self.emit('drain'); });
req.on('error', function(err){
// flag abortion here for out timeouts
// because node will emit a faux-error "socket hang up"
// when request is aborted before a connection is made
if (self._aborted) return;
// if we've recieved a response then we don't want to let
// an error in the request blow up the response
if (self.response) return;
self.callback(err);
});
// auth
if (url.auth) {
var auth = url.auth.split(':');
this.auth(auth[0], auth[1]);
}
// query
if (url.search)
this.query(url.search.substr(1));
// add cookies
if (this.cookies) req.setHeader('Cookie', this.cookies);
for (var key in this.header) {
req.setHeader(key, this.header[key]);
}
return req;
};
/**
* Invoke the callback with `err` and `res`
* and handle arity check.
*
* @param {Error} err
* @param {Response} res
* @api private
*/
Request.prototype.callback = function(err, res){
// Avoid the error which is emitted from 'socket hang up' to cause the fn undefined error on JS runtime.
var fn = this._callback || noop;
this.clearTimeout();
if (this.called) return console.warn('superagent: double callback bug. Upgrade to v3.2+ to fix this');
this.called = true;
if (err) {
err.response = res;
}
// only emit error event if there is a listener
// otherwise we assume the callback to `.end()` will get the error
if (err && this.listeners('error').length > 0) this.emit('error', err);
if (err) {
return fn(err, res);
}
if (res && res.status >= 200 && res.status < 300) {
return fn(err, res);
}
var msg = 'Unsuccessful HTTP response';
if (res) {
msg = http.STATUS_CODES[res.status] || msg;
}
var new_err = new Error(msg);
new_err.original = err;
new_err.response = res;
new_err.status = (res) ? res.status : undefined;
fn(err || new_err, res);
};
/**
* Compose querystring to append to req.path
*
* @return {String} querystring
* @api private
*/
Request.prototype.appendQueryString = function(req){
var querystring = qs.stringify(this.qs, { indices: false });
querystring += ((querystring.length && this.qsRaw.length) ? '&' : '') + this.qsRaw.join('&');
req.path += querystring.length
? (~req.path.indexOf('?') ? '&' : '?') + querystring
: '';
};
/**
* Initiate request, invoking callback `fn(err, res)`
* with an instanceof `Response`.
*
* @param {Function} fn
* @return {Request} for chaining
* @api public
*/
/**
* Client API parity, irrelevant in a Node context.
*
* @api public
*/
Request.prototype.withCredentials = function(){
return this;
};
Request.prototype.end = function(fn){
var self = this;
var data = this._data;
var req = this.request();
var buffer = this._buffer;
var method = this.method;
var timeout = this._timeout;
debug('%s %s', this.method, this.url);
// store callback
this._callback = fn || noop;
// querystring
try {
this.appendQueryString(req);
} catch (e) {
return this.callback(e);
}
// timeout
if (timeout && !this._timer) {
debug('timeout %sms %s %s', timeout, this.method, this.url);
this._timer = setTimeout(function(){
var err = new Error('timeout of ' + timeout + 'ms exceeded');
err.timeout = timeout;
err.code = 'ECONNABORTED';
self.abort();
self.callback(err);
}, timeout);
}
// body
if ('HEAD' != method && !req._headerSent) {
// serialize stuff
if ('string' != typeof data) {
var contentType = req.getHeader('Content-Type')
// Parse out just the content type from the header (ignore the charset)
if (contentType) contentType = contentType.split(';')[0]
var serialize = exports.serialize[contentType];
if (!serialize && isJSON(contentType)) serialize = exports.serialize['application/json'];
if (serialize) data = serialize(data);
}
// content-length
if (data && !req.getHeader('Content-Length')) {
req.setHeader('Content-Length', Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data));
}
}
// response
req.on('response', function(res){
debug('%s %s -> %s', self.method, self.url, res.statusCode);
var max = self._maxRedirects;
var mime = utils.type(res.headers['content-type'] || '') || 'text/plain';
var len = res.headers['content-length'];
var type = mime.split('/');
var subtype = type[1];
var type = type[0];
var multipart = 'multipart' == type;
var redirect = isRedirect(res.statusCode);
var parser = self._parser;
self.res = res;
if ('HEAD' == self.method) {
var response = new Response(self);
self.response = response;
response.redirects = self._redirectList;
self.emit('response', response);
self.callback(null, response);
self.emit('end');
return;
}
if (self.piped) {
return;
}
// redirect
if (redirect && self._redirects++ != max) {
return self.redirect(res);
}
// zlib support
if (self._shouldUnzip(res)) {
utils.unzip(req, res);
}
// don't buffer multipart
if (multipart) buffer = false;
// TODO: make all parsers take callbacks
if (!parser && multipart) {
var form = new formidable.IncomingForm;
form.parse(res, function(err, fields, files){
if (err) return self.callback(err);
var response = new Response(self);
self.response = response;
response.body = fields;
response.files = files;
response.redirects = self._redirectList;
self.emit('end');
self.callback(null, response);
});
return;
}
// check for images, one more special treatment
if (!parser && isImage(mime)) {
exports.parse.image(res, function(err, obj){
if (err) return self.callback(err);
var response = new Response(self);
self.response = response;
response.body = obj;
response.redirects = self._redirectList;
self.emit('end');
self.callback(null, response);
});
return;
}
// by default only buffer text/*, json and messed up thing from hell
if (null == buffer && isText(mime) || isJSON(mime)) buffer = true;
// parser
var parse = 'text' == type
? exports.parse.text
: exports.parse[mime];
// everyone wants their own white-labeled json
if (!parse && isJSON(mime)) parse = exports.parse['application/json'];
// buffered response
if (buffer) parse = parse || exports.parse.text;
// explicit parser
if (parser) parse = parser;
// parse
if (parse) {
try {
parse(res, function(err, obj){
if (err && !self._aborted) self.callback(err);
res.body = obj;
});
} catch (err) {
self.callback(err);
return;
}
}
// unbuffered
if (!buffer) {
debug('unbuffered %s %s', self.method, self.url);
self.res = res;
var response = new Response(self);
self.response = response;
response.redirects = self._redirectList;
self.emit('response', response);
self.callback(null, response);
if (multipart) return // allow multipart to handle end event
res.on('end', function(){
debug('end %s %s', self.method, self.url);
self.emit('end');
})
return;
}
// terminating events
self.res = res;
res.on('error', function(err){
self.callback(err, null);
});
res.on('end', function(){
debug('end %s %s', self.method, self.url);
// TODO: unless buffering emit earlier to stream
var response = new Response(self);
self.response = response;
response.redirects = self._redirectList;
self.emit('response', response);
self.callback(null, response);
self.emit('end');
});
});
this.emit('request', this);
// if a FormData instance got created, then we send that as the request body
var formData = this._formData;
if (formData) {
// set headers
var headers = formData.getHeaders();
for (var i in headers) {
debug('setting FormData header: "%s: %s"', i, headers[i]);
req.setHeader(i, headers[i]);
}
// attempt to get "Content-Length" header
formData.getLength(function(err, length) {
// TODO: Add chunked encoding when no length (if err)
debug('got FormData Content-Length: %s', length);
if ('number' == typeof length) {
req.setHeader('Content-Length', length);
}
var getProgressMonitor = function () {
var lengthComputable = true;
var total = req.getHeader('Content-Length');
var loaded = 0;
var progress = new Stream.Transform();
progress._transform = function (chunk, encoding, cb) {
loaded += chunk.length;
self.emit('progress', {
direction: 'upload',
lengthComputable: lengthComputable,
loaded: loaded,
total: total
});
cb(null, chunk);
};
return progress;
};
formData.pipe(getProgressMonitor()).pipe(req);
});
} else {
req.end(data);
}
return this;
};
/**
* Check whether response has a non-0-sized gzip-encoded body
*/
Request.prototype._shouldUnzip = function(res){
if (res.statusCode === 204 || res.statusCode === 304) {
// These aren't supposed to have any body
return false;
}
// header content is a string, and distinction between 0 and no information is crucial
if ('0' === res.headers['content-length']) {
// We know that the body is empty (unfortunately, this check does not cover chunked encoding)
return false;
}
// console.log(res);
return /^\s*(?:deflate|gzip)\s*$/.test(res.headers['content-encoding']);
};
/**
* To json.
*
* @return {Object}
* @api public
*/
Request.prototype.toJSON = function(){
return {
method: this.method,
url: this.url,
data: this._data
};
};
/**
* Expose `Request`.
*/
exports.Request = Request;
// generate HTTP verb methods
if (methods.indexOf('del') == -1) {
// create a copy so we don't cause conflicts with
// other packages using the methods package and
// npm 3.x
methods = methods.slice(0);
methods.push('del');
}
methods.forEach(function(method){
var name = method;
method = 'del' == method ? 'delete' : method;
method = method.toUpperCase();
request[name] = function(url, data, fn){
var req = request(method, url);
if ('function' == typeof data) fn = data, data = null;
if (data) req.send(data);
fn && req.end(fn);
return req;
};
});
/**
* Check if `mime` is text and should be buffered.
*
* @param {String} mime
* @return {Boolean}
* @api public
*/
function isText(mime) {
var parts = mime.split('/');
var type = parts[0];
var subtype = parts[1];
return 'text' == type
|| 'x-www-form-urlencoded' == subtype;
}
/**
* Check if `mime` is image
*
* @param {String} mime
* @return {Boolean}
* @api public
*/
function isImage(mime) {
var parts = mime.split('/');
var type = parts[0];
var subtype = parts[1];
return 'image' == type;
}
/**
* Check if `mime` is json or has +json structured syntax suffix.
*
* @param {String} mime
* @return {Boolean}
* @api private
*/
function isJSON(mime) {
return /[\/+]json\b/.test(mime);
}
/**
* Check if we should follow the redirect `code`.
*
* @param {Number} code
* @return {Boolean}
* @api private
*/
function isRedirect(code) {
return ~[301, 302, 303, 305, 307, 308].indexOf(code);
}