UNPKG

restler

Version:

An HTTP client library for node.js

543 lines (490 loc) 15.4 kB
var util = require('util'), events = require("events"), http = require('http'), https = require('https'), url = require('url'), qs = require('qs'), multipart = require('./multipartform'), zlib = require('zlib'), iconv = require('iconv-lite'); function mixin(target, source) { source = source || {}; Object.keys(source).forEach(function(key) { target[key] = source[key]; }); return target; } function Request(uri, options) { events.EventEmitter.call(this); this.url = url.parse(uri); this.options = options; this.headers = { 'Accept': '*/*', 'User-Agent': 'Restler for node.js', 'Host': this.url.host }; this.headers['Accept-Encoding'] = 'gzip, deflate'; mixin(this.headers, options.headers || {}); // set port and method defaults if (!this.url.port) this.url.port = (this.url.protocol == 'https:') ? '443' : '80'; if (!this.options.method) this.options.method = (this.options.data) ? 'POST' : 'GET'; if (typeof this.options.followRedirects == 'undefined') this.options.followRedirects = true; // stringify query given in options of not given in URL if (this.options.query && !this.url.query) { if (typeof this.options.query == 'object') this.url.query = qs.stringify(this.options.query); else this.url.query = this.options.query; } this._applyAuth(); if (this.options.multipart) { this.headers['Content-Type'] = 'multipart/form-data; boundary=' + multipart.defaultBoundary; var multipart_size = multipart.sizeOf(this.options.data, multipart.defaultBoundary); if (typeof multipart_size === 'number' && multipart_size === multipart_size) { this.headers['Content-Length'] = multipart_size; } else { console.log("Building multipart request without Content-Length header, please specify all file sizes"); } } else { if (typeof this.options.data == 'object' && !Buffer.isBuffer(this.options.data)) { this.options.data = qs.stringify(this.options.data); this.headers['Content-Type'] = 'application/x-www-form-urlencoded'; this.headers['Content-Length'] = this.options.data.length; } if (typeof this.options.data == 'string') { var buffer = new Buffer(this.options.data, this.options.encoding || 'utf8'); this.options.data = buffer; this.headers['Content-Length'] = buffer.length; } if (!this.options.data) { this.headers['Content-Length'] = 0; } } var proto = (this.url.protocol == 'https:') ? https : http; this.request = proto.request({ host: this.url.hostname, port: this.url.port, path: this._fullPath(), method: this.options.method, headers: this.headers, rejectUnauthorized: this.options.rejectUnauthorized, agent: this.options.agent }); this._makeRequest(); } util.inherits(Request, events.EventEmitter); mixin(Request.prototype, { _isRedirect: function(response) { return ([301, 302, 303, 307].indexOf(response.statusCode) >= 0); }, _fullPath: function() { var path = this.url.pathname || '/'; if (this.url.hash) path += this.url.hash; if (this.url.query) path += '?' + this.url.query; return path; }, _applyAuth: function() { var authParts; if (this.url.auth) { authParts = this.url.auth.split(':'); this.options.username = authParts[0]; this.options.password = authParts[1]; } if (this.options.username && this.options.password !== undefined) { var b = new Buffer([this.options.username, this.options.password].join(':')); this.headers['Authorization'] = "Basic " + b.toString('base64'); } else if (this.options.accessToken) { this.headers['Authorization'] = "Bearer " + this.options.accessToken; } }, _responseHandler: function(response) { var self = this; if (self._isRedirect(response) && self.options.followRedirects) { try { // 303 should redirect and retrieve content with the GET method // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 if (response.statusCode === 303) { self.url = url.parse(url.resolve(self.url.href, response.headers['location'])); self.options.method = 'GET'; delete self.options.data; self._retry(); } else { self.url = url.parse(url.resolve(self.url.href, response.headers['location'])); self._retry(); // todo handle somehow infinite redirects } } catch(err) { err.message = 'Failed to follow redirect: ' + err.message; self._fireError(err, response); } } else { var body = ''; // When using browserify, response.setEncoding is not defined if (typeof response.setEncoding == 'function') response.setEncoding('binary'); response.on('data', function(chunk) { body += chunk; }); response.on('end', function() { response.rawEncoded = body; self._decode(new Buffer(body, 'binary'), response, function(err, body) { if (err) { self._fireError(err, response); return; } response.raw = body; body = self._iconv(body, response); self._encode(body, response, function(err, body) { if (err) { self._fireError(err, response); } else { self._fireSuccess(body, response); } }); }); }); } }, _decode: function(body, response, callback) { var decoder = response.headers['content-encoding']; if (decoder in decoders) { decoders[decoder].call(response, body, callback); } else { callback(null, body); } }, _iconv: function(body, response) { var charset = response.headers['content-type']; if (charset) { charset = /\bcharset=(.+)(?:;|$)/i.exec(charset); if (charset) { charset = charset[1].trim().toUpperCase(); if (charset != 'UTF-8') { try { return iconv.decode(body, charset); } catch (err) {} } } } return body; }, _encode: function(body, response, callback) { var self = this; if (self.options.decoding == 'buffer') { callback(null, body); } else { body = body.toString(self.options.decoding); if (self.options.parser) { self.options.parser.call(response, body, callback); } else { callback(null, body); } } }, _fireError: function(err, response) { this._fireCancelTimeout(); this.emit('error', err, response); this.emit('complete', err, response); }, _fireCancelTimeout: function(){ var self = this; if(self.options.timeout){ clearTimeout(self.options.timeoutFn); } }, _fireTimeout: function(err){ this.emit('timeout', err); this.aborted = true; this.timedout = true; this.request.abort(); }, _fireSuccess: function(body, response) { if (parseInt(response.statusCode) >= 400) { this.emit('fail', body, response); } else { this.emit('success', body, response); } this.emit(response.statusCode.toString().replace(/\d{2}$/, 'XX'), body, response); this.emit(response.statusCode.toString(), body, response); this.emit('complete', body, response); }, _makeRequest: function() { var self = this; var timeoutMs = self.options.timeout; if(timeoutMs){ self.options.timeoutFn = setTimeout(function(){ self._fireTimeout(timeoutMs); },timeoutMs); } this.request.on('response', function(response) { self._fireCancelTimeout(); self.emit('response', response); self._responseHandler(response); }).on('error', function(err) { self._fireCancelTimeout(); if (!self.aborted) { self._fireError(err, null); } }); }, _retry: function() { this.request.removeAllListeners().on('error', function() {}); if (this.request.finished) { this.request.abort(); } Request.call(this, this.url.href, this.options); // reusing request object to handle recursive calls and remember listeners this.run(); }, run: function() { var self = this; if (this.options.multipart) { multipart.write(this.request, this.options.data, function() { self.request.end(); }); } else { if (this.options.data) { this.request.write(this.options.data, this.options.encoding || 'utf8'); } this.request.end(); } return this; }, abort: function(err) { var self = this; if (err) { if (typeof err == 'string') { err = new Error(err); } else if (!(err instanceof Error)) { err = new Error('AbortError'); } err.type = 'abort'; } else { err = null; } self.request.on('close', function() { if (err) { self._fireError(err, null); } else { self.emit('complete', null, null); } }); self.aborted = true; self.request.abort(); self.emit('abort', err); return this; }, retry: function(timeout) { var self = this; timeout = parseInt(timeout); var fn = self._retry.bind(self); if (!isFinite(timeout) || timeout <= 0) { process.nextTick(fn, timeout); } else { setTimeout(fn, timeout); } return this; } }); function shortcutOptions(options, method) { options = options || {}; options.method = method; options.parser = (typeof options.parser !== "undefined") ? options.parser : parsers.auto; parsers.xml.options = (typeof options.xml2js == 'undefined') ? {} : options.xml2js; return options; } function request(url, options) { var request = new Request(url, options); request.on('error', function() {}); process.nextTick(request.run.bind(request)); return request; } function get(url, options) { return request(url, shortcutOptions(options, 'GET')); } function patch(url, options) { return request(url, shortcutOptions(options, 'PATCH')); } function post(url, options) { return request(url, shortcutOptions(options, 'POST')); } function put(url, options) { return request(url, shortcutOptions(options, 'PUT')); } function del(url, options) { return request(url, shortcutOptions(options, 'DELETE')); } function head(url, options) { return request(url, shortcutOptions(options, 'HEAD')); } function json(url, data, options, method) { options = options || {}; options.parser = (typeof options.parser !== "undefined") ? options.parser : parsers.auto; options.headers = options.headers || {}; options.headers['content-type'] = 'application/json'; options.data = JSON.stringify(data || {}); options.method = method || 'GET'; return request(url, options); } function postJson(url, data, options) { return json(url, data, options, 'POST'); } function putJson(url, data, options) { return json(url, data, options, 'PUT'); } function patchJson(url, data, options) { return json(url, data, options, 'PATCH'); } var parsers = { auto: function(data, callback) { var contentType = this.headers['content-type']; var contentParser; if (contentType) { contentType = contentType.replace(/;.+/, ''); // remove all except mime type (eg. text/html; charset=UTF-8) if (contentType in parsers.auto.matchers) { contentParser = parsers.auto.matchers[contentType]; } else { // custom (vendor) mime types var parts = contentType.match(/^([\w-]+)\/vnd((?:\.(?:[\w-]+))+)\+([\w-]+)$/i); if (parts) { var type = parts[1]; var vendors = parts[2].substr(1).split('.'); var subtype = parts[3]; var vendorType; while (vendors.pop() && !(vendorType in parsers.auto.matchers)) { vendorType = vendors.length ? type + '/vnd.' + vendors.join('.') + '+' + subtype : vendorType = type + '/' + subtype; } contentParser = parsers.auto.matchers[vendorType]; } } } if (typeof contentParser == 'function') { contentParser.call(this, data, callback); } else { callback(null, data); } }, json: function(data, callback) { if (data && data.length) { var parsedData; try { parsedData = JSON.parse(data); } catch (err) { err.message = 'Failed to parse JSON body: ' + err.message; callback(err, null); } if (parsedData !== undefined) { callback(null, parsedData); } } else { callback(null, null); } } }; parsers.auto.matchers = { 'application/json': parsers.json }; try { var yaml = require('yaml'); parsers.yaml = function(data, callback) { if (data) { try { callback(null, yaml.eval(data)); } catch (err) { err.message = 'Failed to parse YAML body: ' + err.message; callback(err, null); } } else { callback(null, null); } }; parsers.auto.matchers['application/yaml'] = parsers.yaml; } catch(e) {} try { var xml2js = require('xml2js'); parsers.xml = function(data, callback) { if (data) { var parser = new xml2js.Parser(parsers.xml.options); parser.parseString(data, function(err, data) { if (err) { err.message = 'Failed to parse XML body: ' + err.message; } callback(err, data); }); } else { callback(null, null); } }; parsers.auto.matchers['application/xml'] = parsers.xml; } catch(e) { } var decoders = { gzip: function(buf, callback) { zlib.gunzip(buf, callback); }, deflate: function(buf, callback) { zlib.inflate(buf, callback); } }; function Service(defaults) { if (defaults.baseURL) { this.baseURL = defaults.baseURL; delete defaults.baseURL; } this.defaults = defaults; } mixin(Service.prototype, { request: function(path, options) { return request(this._url(path), this._withDefaults(options)); }, get: function(path, options) { return get(this._url(path), this._withDefaults(options)); }, patch: function(path, options) { return patch(this._url(path), this._withDefaults(options)); }, put: function(path, options) { return put(this._url(path), this._withDefaults(options)); }, post: function(path, options) { return post(this._url(path), this._withDefaults(options)); }, json: function(method, path, data, options) { return json(this._url(path), data, this._withDefaults(options), method); }, del: function(path, options) { return del(this._url(path), this._withDefaults(options)); }, _url: function(path) { if (this.baseURL) return url.resolve(this.baseURL, path); else return path; }, _withDefaults: function(options) { var o = mixin({}, this.defaults); return mixin(o, options); } }); function service(constructor, defaults, methods) { constructor.prototype = new Service(defaults || {}); mixin(constructor.prototype, methods); return constructor; } mixin(exports, { Request: Request, Service: Service, request: request, service: service, get: get, patch: patch, post: post, put: put, del: del, head: head, json: json, postJson: postJson, putJson: putJson, patchJson: patchJson, parsers: parsers, file: multipart.file, data: multipart.data });