diff --git a/.gitignore b/.gitignore
index 68f49f6..d5c8b49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
node_modules
.idea/
node-xmlrpc.iml
+/nbproject
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 90571e4..ba52325 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,12 @@
language: node_js
node_js:
- - "0.8"
- "0.10"
- "0.11"
- "0.12"
+ - "4.0"
+ - "4.1"
+ - "4.2"
+ - "5.0"
+ - "5.1"
+ - "5.2"
+ - "5.3"
diff --git a/README.md b/README.md
index 07328d0..83b3495 100644
--- a/README.md
+++ b/README.md
@@ -36,11 +36,14 @@ server.on('NotFound', function(method, params) {
console.log('Method ' + method + ' does not exist');
})
// Handle method calls by listening for events with the method call name
-server.on('anAction', function (err, params, callback) {
+server.on('anAction', function (err, params, callback, request, response) {
console.log('Method call params for \'anAction\': ' + params)
// ...perform an action...
+ // Use request and response objects directly for any custom processing, e.g.
+ // set or forward cookies
+
// Send a method response with a value
callback(null, 'aResult')
})
@@ -177,6 +180,71 @@ var client = xmlrpc.createClient('YOUR_ENDPOINT');
client.methodCall('YOUR_METHOD', [new YourType(yourVariable)], yourCallback);
```
+### XML RPC Error
+There is a special error type defined - `XmlRpcError`. And a helper function makeError to create errors easily.
+Use it to create an error and pass it to the `callback`.
+
+```javascript
+// Makes an error with only message and code defaults to zero (0)
+xmlrpc.makeError("Error occured")
+```
+
+The resulting response would be:
+```xml
+
+
+
+
+
+
+ code
+
+ 0
+
+
+
+ message
+
+ Error occured
+
+
+
+
+
+
+```
+
+The error with a code example:
+```javascript
+// Makes an error with message and code
+xmlrpc.makeError("Error occured", 123)
+```
+
+The resulting response would be:
+```xml
+
+
+
+
+
+
+ faultCode
+
+ 123
+
+
+
+ faultString
+
+ Error occured
+
+
+
+
+
+
+```
+
### To Debug (client-side)
Error callbacks on the client are enriched with request and response
diff --git a/lib/client.js b/lib/client.js
index b875461..20221d4 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -1,9 +1,9 @@
-var http = require('http')
- , https = require('https')
- , url = require('url')
- , Serializer = require('./serializer')
- , Deserializer = require('./deserializer')
- , Cookies = require('./cookies')
+var http = require('http')
+ , https = require('https')
+ , url = require('url')
+ , Serializer = require('./serializer')
+ , Deserializer = require('./deserializer')
+ , Cookies = require('./cookies')
/**
* Creates a Client object for making XML-RPC method calls.
@@ -17,6 +17,7 @@ var http = require('http')
* - {Number} port
* - {String} url - (optional) - may be used instead of host/port pair
* - {Boolean} cookies - (optional) - if true then cookies returned by server will be stored and sent back on the next calls.
+ * - {Promise} promiselib - (optional) - if set, the methodCall would return promise
* Also it will be possible to access/manipulate cookies via #setCookie/#getCookie methods
* @param {Boolean} isSecure - True if using https for making calls,
* otherwise false.
@@ -24,67 +25,79 @@ var http = require('http')
*/
function Client(options, isSecure) {
- // Invokes with new if called without
- if (false === (this instanceof Client)) {
- return new Client(options, isSecure)
- }
-
- // If a string URI is passed in, converts to URI fields
- if (typeof options === 'string') {
- options = url.parse(options)
- options.host = options.hostname
- options.path = options.pathname
- }
-
- if (typeof options.url !== 'undefined') {
- var parsedUrl = url.parse(options.url);
- options.host = parsedUrl.hostname;
- options.path = parsedUrl.pathname;
- options.port = parsedUrl.port;
- }
-
- // Set the HTTP request headers
- var headers = {
- 'User-Agent' : 'NodeJS XML-RPC Client'
- , 'Content-Type' : 'text/xml'
- , 'Accept' : 'text/xml'
- , 'Accept-Charset' : 'UTF8'
- , 'Connection' : 'Keep-Alive'
- }
- options.headers = options.headers || {}
-
- if (options.headers.Authorization == null &&
- options.basic_auth != null &&
- options.basic_auth.user != null &&
- options.basic_auth.pass != null)
- {
- var auth = options.basic_auth.user + ':' + options.basic_auth.pass
- options.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64')
- }
-
- for (var attribute in headers) {
- if (options.headers[attribute] === undefined) {
- options.headers[attribute] = headers[attribute]
- }
- }
-
- options.method = 'POST'
- this.options = options
-
- this.isSecure = isSecure
- this.headersProcessors = {
- processors: [],
- composeRequest: function(headers) {
- this.processors.forEach(function(p) {p.composeRequest(headers);})
- },
- parseResponse: function(headers) {
- this.processors.forEach(function(p) {p.parseResponse(headers);})
- }
- };
- if (options.cookies) {
- this.cookies = new Cookies();
- this.headersProcessors.processors.unshift(this.cookies);
- }
+ // Invokes with new if called without
+ if (false === (this instanceof Client)) {
+ return new Client(options, isSecure)
+ }
+
+ // If a string URI is passed in, converts to URI fields
+ if (typeof options === 'string') {
+ options = url.parse(options)
+ options.host = options.hostname
+ options.path = options.pathname
+ }
+
+ if (typeof options.url !== 'undefined') {
+ var parsedUrl = url.parse(options.url);
+ options.host = parsedUrl.hostname;
+ options.path = parsedUrl.pathname;
+ options.port = parsedUrl.port;
+ }
+
+ // Set the HTTP request headers
+ var headers = {
+ 'User-Agent': 'NodeJS XML-RPC Client'
+ , 'Content-Type': 'text/xml'
+ , 'Accept': 'text/xml'
+ , 'Accept-Charset': 'UTF8'
+ , 'Connection': 'Keep-Alive'
+ }
+ options.headers = options.headers || {}
+
+ if (options.headers.Authorization == null &&
+ options.basic_auth != null &&
+ options.basic_auth.user != null &&
+ options.basic_auth.pass != null)
+ {
+ var auth = options.basic_auth.user + ':' + options.basic_auth.pass
+ options.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64')
+ }
+
+ for (var attribute in headers) {
+ if (options.headers[attribute] === undefined) {
+ options.headers[attribute] = headers[attribute]
+ }
+ }
+
+ options.method = 'POST'
+ this.options = options
+
+ this.isSecure = isSecure
+ this.headersProcessors = {
+ processors: [],
+ composeRequest: function (headers) {
+ this.processors.forEach(function (p) {
+ p.composeRequest(headers);
+ })
+ },
+ parseResponse: function (headers) {
+ this.processors.forEach(function (p) {
+ p.parseResponse(headers);
+ })
+ }
+ };
+ if (options.cookies) {
+ this.cookies = new Cookies();
+ this.headersProcessors.processors.unshift(this.cookies);
+ }
+}
+
+function __cb(resolve, reject, error, value) {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(value);
+ }
}
/**
@@ -92,49 +105,61 @@ function Client(options, isSecure) {
*
* @param {String} method - The method name.
* @param {Array} params - Params to send in the call.
- * @param {Function} callback - function(error, value) { ... }
+ * @param {Function} callback - (optional) function(error, value) { ... }
* - {Object|null} error - Any errors when making the call, otherwise null.
* - {mixed} value - The value returned in the method response.
+ * If not specified and promiselib was specified in construction options,
+ * .then(function(value){}).catch(function(error){}) can be used.
*/
Client.prototype.methodCall = function methodCall(method, params, callback) {
- var options = this.options
- var xml = Serializer.serializeMethodCall(method, params, options.encoding)
- var transport = this.isSecure ? https : http
-
- options.headers['Content-Length'] = Buffer.byteLength(xml, 'utf8')
- this.headersProcessors.composeRequest(options.headers)
- var request = transport.request(options, function(response) {
-
- var body = []
- response.on('data', function (chunk) { body.push(chunk) })
-
- function __enrichError (err) {
- Object.defineProperty(err, 'req', { value: request })
- Object.defineProperty(err, 'res', { value: response })
- Object.defineProperty(err, 'body', { value: body.join('') })
- return err
- }
-
- if (response.statusCode == 404) {
- callback(__enrichError(new Error('Not Found')))
- }
- else {
- this.headersProcessors.parseResponse(response.headers)
-
- var deserializer = new Deserializer(options.responseEncoding)
-
- deserializer.deserializeMethodResponse(response, function(err, result) {
- if (err) {
- err = __enrichError(err)
- }
- callback(err, result)
- })
- }
- }.bind(this))
-
- request.on('error', callback)
- request.write(xml, 'utf8')
- request.end()
+ var options = this.options
+ var xml = Serializer.serializeMethodCall(method, params, options.encoding)
+ var transport = this.isSecure ? https : http
+
+ var promise = null;
+ if (!callback && options.promiselib) {
+ promise = new options.promiselib(function (resolve, reject) {
+ callback = __cb.bind(null, resolve, reject);
+ });
+ }
+
+ options.headers['Content-Length'] = Buffer.byteLength(xml, 'utf8')
+ this.headersProcessors.composeRequest(options.headers)
+ var request = transport.request(options, function (response) {
+
+ var body = []
+ response.on('data', function (chunk) {
+ body.push(chunk)
+ })
+
+ function __enrichError(err) {
+ Object.defineProperty(err, 'req', {value: request})
+ Object.defineProperty(err, 'res', {value: response})
+ Object.defineProperty(err, 'body', {value: body.join('')})
+ return err
+ }
+
+ if (response.statusCode == 404) {
+ callback(__enrichError(new Error('Not Found')))
+ } else {
+ this.headersProcessors.parseResponse(response.headers)
+
+ var deserializer = new Deserializer(options.responseEncoding)
+
+ deserializer.deserializeMethodResponse(response, function (err, result) {
+ if (err) {
+ err = __enrichError(err)
+ }
+ callback(err, result)
+ })
+ }
+ }.bind(this))
+
+ request.on('error', callback)
+ request.write(xml, 'utf8')
+ request.end()
+
+ return promise;
}
/**
@@ -145,10 +170,10 @@ Client.prototype.methodCall = function methodCall(method, params, callback) {
* @return {*} cookie's value
*/
Client.prototype.getCookie = function getCookie(name) {
- if (!this.cookies) {
- throw 'Cookies support is not turned on for this client instance';
- }
- return this.cookies.get(name);
+ if (!this.cookies) {
+ throw 'Cookies support is not turned on for this client instance';
+ }
+ return this.cookies.get(name);
}
/**
@@ -166,12 +191,11 @@ Client.prototype.getCookie = function getCookie(name) {
* @return {*} client object itself
*/
Client.prototype.setCookie = function setCookie(name, value) {
- if (!this.cookies) {
- throw 'Cookies support is not turned on for this client instance';
- }
- this.cookies.set(name, value);
- return this;
+ if (!this.cookies) {
+ throw 'Cookies support is not turned on for this client instance';
+ }
+ this.cookies.set(name, value);
+ return this;
}
module.exports = Client
-
diff --git a/lib/error.js b/lib/error.js
new file mode 100644
index 0000000..9b24bad
--- /dev/null
+++ b/lib/error.js
@@ -0,0 +1,60 @@
+var error = require('tea-error')
+
+var xmlrpc = exports
+
+/**
+ * The type to generate xml-rpc error
+ */
+xmlrpc.XmlRpcError = error('XmlRpcError');
+
+/**
+ * The helper function to create xml-rpc error
+ *
+ * Examples:
+ * // Makes an error with only message and default (zero 0) code
+ * xmlrpc.makeError("Error occured")
+ * // Makes an error with message and code
+ * xmlrpc.makeError("Error occured", 123)
+ *
+ * @param {String} message The error message
+ * @param {Number} code The error code
+ *
+ * @returns {XmlRpcError} The error object
+ */
+xmlrpc.makeError = function (message, code) {
+ var msg = message;
+ if ('object' === typeof message) {
+ for (var i in message) {
+ msg = message[i];
+ // we need only one field
+ break;
+ }
+ }
+ return new xmlrpc.XmlRpcError(message, {codeArg: code});
+}
+
+/**
+ *
+ * @param {XmlRpcError|Error|Object|String} error The error object
+ * @returns {Object|String} The error object or string to send as a response
+ */
+xmlrpc.makeResponseObjectFromError = function(error) {
+ var message = '';
+ var code = 0;
+ if (error instanceof xmlrpc.XmlRpcError) {
+ message = error.message;
+ if ('number' === typeof error.codeArg) {
+ code = error.codeArg;
+ }
+ } else if (error instanceof Error) {
+ message = (('' !== error.message) ? error.message : error.name);
+ } else if (error instanceof Object) {
+ message = error.toString();
+ } else {
+ message = error + '';
+ }
+ var fault = {};
+ fault['faultString'] = message;
+ fault['faultCode'] = code;
+ return fault;
+}
diff --git a/lib/server.js b/lib/server.js
index baef3b6..f6687c7 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -4,9 +4,10 @@ var http = require('http')
, EventEmitter = require('events').EventEmitter
, Serializer = require('./serializer')
, Deserializer = require('./deserializer')
+ , xmlrpcError = require('./error')
/**
- * Creates a new Server object. Also creates an HTTP server to start listening
+ * Creates a new Server object. Also optionally creates a HTTP server to start listening
* for XML-RPC method calls. Will emit an event with the XML-RPC call's method
* name when receiving a method call.
*
@@ -14,61 +15,98 @@ var http = require('http')
* @param {Object|String} options - The HTTP server options. Either a URI string
* (e.g. 'http://localhost:9090') or an object
* with fields:
- * - {String} host - (optional)
- * - {Number} port
+ * - {http.Server} httpServer - (optional) The external http server object
+ * If supplied, the request handler is supposed to be registered
+ * by the user himself: server.requestHandler
+ * - {String} anyMethodName - (optional) The special method name.
+ * If supplied, all incoming request would be routed to it if not found in specific events.
+ * The actual method name requested is given in a first element of the params array in a callback.
+ * - {String} host - (optional)
+ * - {Number} port
* @param {Boolean} isSecure - True if using https for making calls,
* otherwise false.
* @return {Server}
*/
function Server(options, isSecure, onListening) {
- if (false === (this instanceof Server)) {
- return new Server(options, isSecure)
- }
- onListening = onListening || function() {}
- var that = this
+ if (false === (this instanceof Server)) {
+ return new Server(options, isSecure)
+ }
+ onListening = onListening || function() {}
+ var that = this
- // If a string URI is passed in, converts to URI fields
- if (typeof options === 'string') {
- options = url.parse(options)
- options.host = options.hostname
- options.path = options.pathname
- }
+ // If a string URI is passed in, converts to URI fields
+ if (typeof options === 'string') {
+ options = url.parse(options)
+ options.host = options.hostname
+ options.path = options.pathname
+ }
- function handleMethodCall(request, response) {
- var deserializer = new Deserializer()
- deserializer.deserializeMethodCall(request, function(error, methodName, params) {
- if (that._events.hasOwnProperty(methodName)) {
- that.emit(methodName, null, params, function(error, value) {
- var xml = null
- if (error !== null) {
- xml = Serializer.serializeFault(error)
- }
- else {
- xml = Serializer.serializeMethodResponse(value)
- }
- response.writeHead(200, {'Content-Type': 'text/xml'})
- response.end(xml)
- })
- }
- else {
- that.emit('NotFound', methodName, params)
- response.writeHead(404)
- response.end()
- }
- })
- }
+ function _callback(response, error, value) {
+ var xml = null;
+ if (error !== null) {
+ var fault = xmlrpcError.makeResponseObjectFromError(error);
+ xml = Serializer.serializeFault(fault);
+ }
+ else {
+ xml = Serializer.serializeMethodResponse(value);
+ }
+ response.writeHead(200, {'Content-Type': 'text/xml'});
+ response.end(xml);
+ }
+
+ function _deserializeMethodCallback(request, response, error, methodName, params) {
+ if (that._events.hasOwnProperty(methodName)) {
+ that.emit(methodName, null/*error*/, params, _callback.bind(null, response), request, response);
+ }
+ else if (options.anyMethodName && that._events.hasOwnProperty(options.anyMethodName)) {
+ // Add the methodName as a first element to params
+ params.splice(0, 0, methodName);
+ that.emit(options.anyMethodName, null/*error*/, params, _callback.bind(null, response), request, response);
+ }
+ else {
+ that.emit('NotFound', methodName/*error*/, params);
+ response.writeHead(404);
+ response.end();
+ }
+ }
+
+ function _socketDestroy(socket) {
+ socket.destroy();
+ }
+
+ function _finishCallback(socket) {
+ socket.removeAllListeners('timeout');
+ socket.setTimeout(5000, _socketDestroy.bind(null, socket));
+ }
- this.httpServer = isSecure ? https.createServer(options, handleMethodCall)
- : http.createServer(handleMethodCall)
+ function handleMethodCall(request, response) {
+ // node.js bug with slow socket release workaround
+ // @see http://habrahabr.ru/post/264851/
+ var socket = request.socket;
+ response.on('finish', _finishCallback.bind(null, socket));
- process.nextTick(function() {
- this.httpServer.listen(options.port, options.host, onListening)
- }.bind(this))
- this.close = function(callback) {
- this.httpServer.once('close', callback)
- this.httpServer.close()
- }.bind(this)
+ var deserializer = new Deserializer();
+ deserializer.deserializeMethodCall(request, _deserializeMethodCallback.bind(null, request, response));
+ }
+ this.requestHandler = handleMethodCall;
+
+ if (options.httpServer) {
+ this.httpServer = options.httpServer;
+ } else {
+ if (isSecure) {
+ this.httpServer = https.createServer(options, handleMethodCall);
+ } else {
+ this.httpServer = http.createServer(handleMethodCall);
+ }
+ process.nextTick(function() {
+ this.httpServer.listen(options.port, options.host, onListening)
+ }.bind(this))
+ this.close = function(callback) {
+ this.httpServer.once('close', callback)
+ this.httpServer.close()
+ }.bind(this)
+ }
}
// Inherit from EventEmitter to emit and listen
diff --git a/lib/xmlrpc.js b/lib/xmlrpc.js
index 5355e22..25667a7 100644
--- a/lib/xmlrpc.js
+++ b/lib/xmlrpc.js
@@ -2,6 +2,7 @@ var Client = require('./client')
, Server = require('./server')
, CustomType = require('./customtype')
, dateFormatter = require('./date_formatter')
+ , error = require('./error')
var xmlrpc = exports
@@ -63,3 +64,6 @@ xmlrpc.createSecureServer = function(options, callback) {
xmlrpc.CustomType = CustomType
xmlrpc.dateFormatter = dateFormatter
+
+xmlrpc.XmlRpcError = error.XmlRpcError;
+xmlrpc.makeError = error.makeError;
diff --git a/package.json b/package.json
index bea1d64..4147b3d 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
, "main" : "./lib/xmlrpc.js"
, "dependencies" : {
"sax" : "0.6.x"
+ , "tea-error": "^0.2.0"
, "xmlbuilder" : "2.6.x"
}
, "devDependencies" : {
diff --git a/test/fixtures/good_food/encoded_fault.xml b/test/fixtures/good_food/encoded_fault.xml
new file mode 100644
index 0000000..33c243f
--- /dev/null
+++ b/test/fixtures/good_food/encoded_fault.xml
@@ -0,0 +1 @@
+Foo
\ No newline at end of file
diff --git a/test/fixtures/good_food/string_cdata_fault.xml b/test/fixtures/good_food/string_cdata_fault.xml
new file mode 100644
index 0000000..627acbc
--- /dev/null
+++ b/test/fixtures/good_food/string_cdata_fault.xml
@@ -0,0 +1 @@
+faultStringCongrats