From 37e0861a7659c94d9c073136ea8d93cc397206ef Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Wed, 11 Nov 2015 01:18:57 +0500 Subject: [PATCH 01/13] node.js bug with slow socket release workaround @see http://habrahabr.ru/post/264851/ The problem is: available socket limit is reached. This workaround solves the problem in some scale. --- lib/server.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/server.js b/lib/server.js index baef3b6..b3483db 100644 --- a/lib/server.js +++ b/lib/server.js @@ -36,6 +36,15 @@ function Server(options, isSecure, onListening) { } 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', function() { + socket.removeAllListeners('timeout'); + socket.setTimeout(5000, function() { + socket.destroy(); + }); + }); var deserializer = new Deserializer() deserializer.deserializeMethodCall(request, function(error, methodName, params) { if (that._events.hasOwnProperty(methodName)) { From b0c1f1a397bc40661dbe410cc05cab848885f02e Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Wed, 30 Dec 2015 15:11:44 +0500 Subject: [PATCH 02/13] Update .travis.yml latest node versions to test --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 90571e4..ebc2273 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,10 @@ node_js: - "0.10" - "0.11" - "0.12" + - "4.0" + - "4.1" + - "4.2" + - "5.0" + - "5.1" + - "5.2" + - "5.3" From badcd4d910210d05bbd3fac5e896cb0a6e19e45e Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Wed, 30 Dec 2015 15:16:24 +0500 Subject: [PATCH 03/13] Update .travis.yml we don't need that shit --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ebc2273..ba52325 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js node_js: - - "0.8" - "0.10" - "0.11" - "0.12" From 1896edd18881222a7630ebf5b6819570a61fdf5f Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Thu, 31 Dec 2015 01:07:44 +0600 Subject: [PATCH 04/13] The ability to specify already existing http server to work with --- lib/server.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/server.js b/lib/server.js index b3483db..98bbb9a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -6,7 +6,7 @@ var http = require('http') , Deserializer = require('./deserializer') /** - * 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,8 +14,11 @@ 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 + * - {String} httpServer - (optional) The external http server object + * If supplied, the request handler is supposed to be registered + * by the user himself: server.requestHandler + * - {String} host - (optional) + * - {Number} port * @param {Boolean} isSecure - True if using https for making calls, * otherwise false. * @return {Server} @@ -67,17 +70,24 @@ function Server(options, isSecure, onListening) { } }) } + this.requestHandler = handleMethodCall; - this.httpServer = isSecure ? https.createServer(options, handleMethodCall) - : 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) + 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 From 1d1807ede27a0c8ef25abcc8ac444aeaab283b5c Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Thu, 31 Dec 2015 13:09:19 +0600 Subject: [PATCH 05/13] The ability to specify the Any method name to respond to any xml-rpc request with a single method supplied. The actual method name requested is given in a first element of the params array in a callback. --- lib/server.js | 105 +++++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/lib/server.js b/lib/server.js index 98bbb9a..21a1684 100644 --- a/lib/server.js +++ b/lib/server.js @@ -14,9 +14,12 @@ 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} httpServer - (optional) The external http server object + * - {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, @@ -25,51 +28,65 @@ var http = require('http') */ 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) { - // node.js bug with slow socket release workaround - // @see http://habrahabr.ru/post/264851/ - var socket = request.socket; - response.on('finish', function() { - socket.removeAllListeners('timeout'); - socket.setTimeout(5000, function() { - socket.destroy(); - }); - }); - 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) { + xml = Serializer.serializeFault(error); + } + else { + xml = Serializer.serializeMethodResponse(value); + } + response.writeHead(200, {'Content-Type': 'text/xml'}); + response.end(xml); + } + + function _deserializeMethodCallback(response, error, methodName, params) { + if (that._events.hasOwnProperty(methodName)) { + that.emit(methodName, null/*error*/, params, _callback.bind(null, 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)); + } + 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)); + } + + 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)); + + var deserializer = new Deserializer(); + deserializer.deserializeMethodCall(request, _deserializeMethodCallback.bind(null, response)); + } this.requestHandler = handleMethodCall; if (options.httpServer) { From 4a8df10d0b010470230a8a3d6b24c459e083daf6 Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Sun, 3 Jan 2016 19:12:43 +0600 Subject: [PATCH 06/13] request and response made available in the user callback - It can be used for cudtom headers manipulation, e.g. setting cookies on the response object --- .gitignore | 1 + README.md | 5 ++++- lib/server.js | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) 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/README.md b/README.md index 07328d0..945ae4e 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') }) diff --git a/lib/server.js b/lib/server.js index 21a1684..ee0ca61 100644 --- a/lib/server.js +++ b/lib/server.js @@ -53,14 +53,14 @@ function Server(options, isSecure, onListening) { response.end(xml); } - function _deserializeMethodCallback(response, error, methodName, params) { + function _deserializeMethodCallback(request, response, error, methodName, params) { if (that._events.hasOwnProperty(methodName)) { - that.emit(methodName, null/*error*/, params, _callback.bind(null, response)); + 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)); + that.emit(options.anyMethodName, null/*error*/, params, _callback.bind(null, response), request, response); } else { that.emit('NotFound', methodName/*error*/, params); @@ -85,7 +85,7 @@ function Server(options, isSecure, onListening) { response.on('finish', _finishCallback.bind(null, socket)); var deserializer = new Deserializer(); - deserializer.deserializeMethodCall(request, _deserializeMethodCallback.bind(null, response)); + deserializer.deserializeMethodCall(request, _deserializeMethodCallback.bind(null, request, response)); } this.requestHandler = handleMethodCall; From e55a56af8992e673017040908a5e2446eb0b9299 Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Tue, 5 Jan 2016 15:43:15 +0600 Subject: [PATCH 07/13] Promise support - If promiselib is specified in the options, e.g. `{promiselib: Promise}` and no callback is specified, then methodCall would return a promise. --- lib/client.js | 255 +++++++++++++++++++++++++++----------------------- 1 file changed, 139 insertions(+), 116 deletions(-) diff --git a/lib/client.js b/lib/client.js index b875461..b8c8fa6 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,60 @@ 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 promise = null; + if (!callback && options.promiselib) { + promise = new options.promiselib(function (resolve, reject) { + callback = __cb.bind(null, resolve, reject); + }); + } + 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() + + return promise; } /** @@ -145,10 +169,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 +190,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 - From e99fba24975203f5b90c41f4c0abd53bd360eea8 Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Tue, 5 Jan 2016 16:38:36 +0600 Subject: [PATCH 08/13] fix --- lib/client.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/client.js b/lib/client.js index b8c8fa6..20221d4 100644 --- a/lib/client.js +++ b/lib/client.js @@ -112,15 +112,16 @@ function __cb(resolve, reject, error, value) { * .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 + var promise = null; if (!callback && options.promiselib) { promise = new options.promiselib(function (resolve, reject) { callback = __cb.bind(null, resolve, reject); }); } - 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) From 8cdf57e1a3577ce67b8cee0fd0d54f1ae3eb417c Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Tue, 5 Jan 2016 22:39:23 +0600 Subject: [PATCH 09/13] The custom error object with helper function to create it are introduced - And proper fault response generation is implemented with or without code. - Tests and documentation included. --- README.md | 83 +++++++++++++++ lib/error.js | 83 +++++++++++++++ lib/server.js | 4 +- lib/xmlrpc.js | 4 + package.json | 1 + test/fixtures/good_food/encoded_fault.xml | 1 + .../fixtures/good_food/string_cdata_fault.xml | 1 + test/fixtures/good_food/string_code_fault.xml | 1 + .../fixtures/good_food/string_empty_fault.xml | 1 + test/fixtures/good_food/string_fault.xml | 1 + .../string_multiline_cdata_fault.xml | 4 + test/serializer_test.js | 100 +++++++++++++++++- 12 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 lib/error.js create mode 100644 test/fixtures/good_food/encoded_fault.xml create mode 100644 test/fixtures/good_food/string_cdata_fault.xml create mode 100644 test/fixtures/good_food/string_code_fault.xml create mode 100644 test/fixtures/good_food/string_empty_fault.xml create mode 100644 test/fixtures/good_food/string_fault.xml create mode 100644 test/fixtures/good_food/string_multiline_cdata_fault.xml diff --git a/README.md b/README.md index 945ae4e..56d62b3 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,89 @@ 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 no code +xmlrpc.makeError("Error occured") +``` + +The resulting response would be: +```xml + + + + + Error occured + + + +``` + +The error with a code example: +```javascript +// Makes an error with message in a field named 'message' and code in a field named 'code' +xmlrpc.makeError("Error occured", 123) +``` + +The resulting response would be: +```xml + + + + + + + code + + 123 + + + + message + + Error occured + + + + + + +``` + +The PHP XML-RPC fault response style: +```javascript +// Makes an error with message in a field named 'faultString' and code in a field named 'faultCode' +xmlrpc.makeError({faultString: "Error occured"}, {faultCode: 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/error.js b/lib/error.js new file mode 100644 index 0000000..b238495 --- /dev/null +++ b/lib/error.js @@ -0,0 +1,83 @@ +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 no code + * xmlrpc.makeError("Error occured") + * // Makes an error with message in a field named 'message' and code in a field named 'code' + * xmlrpc.makeError("Error occured", 123) + * // Makes an error with message in a field named 'faultString' and code in a field named 'faultCode' + * xmlrpc.makeError({faultString: "Error occured"}, {faultCode: 123}) + * + * @param {String|Object} message The error message or object like {faultString: "Error occured"} + * @param {Number|Object} code The error code or object like {faultCode: 123} + * @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(msg, {messageArg: 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) { + if (error instanceof xmlrpc.XmlRpcError) { + var messageFieldName = 'message'; + var message = error.message; + if ('undefined' !== typeof error.messageArg) { + if ('string' === typeof error.messageArg) { + message = error.messageArg; + } else if ('object' === typeof error.messageArg) { + for (var i in error.messageArg) { + messageFieldName = i; + message = error.messageArg[i]; + // we need only one field + break; + } + } + } + var codeFieldName = 'code'; + var code = undefined; + if ('undefined' !== typeof error.codeArg) { + if ('number' === typeof error.codeArg) { + code = error.codeArg; + } else if ('object' === typeof error.codeArg) { + for (var j in error.codeArg) { + codeFieldName = j; + code = error.codeArg[j]; + // we need only one field + break; + } + } + } + var fault = message; + if ('undefined' !== typeof code) { + fault = {}; + fault[messageFieldName] = message; + fault[codeFieldName] = code; + } + return fault; + } else if (error instanceof Object) { + return error.toString(); + } + return error + ''; +} diff --git a/lib/server.js b/lib/server.js index ee0ca61..f6687c7 100644 --- a/lib/server.js +++ b/lib/server.js @@ -4,6 +4,7 @@ var http = require('http') , EventEmitter = require('events').EventEmitter , Serializer = require('./serializer') , Deserializer = require('./deserializer') + , xmlrpcError = require('./error') /** * Creates a new Server object. Also optionally creates a HTTP server to start listening @@ -44,7 +45,8 @@ function Server(options, isSecure, onListening) { function _callback(response, error, value) { var xml = null; if (error !== null) { - xml = Serializer.serializeFault(error); + var fault = xmlrpcError.makeResponseObjectFromError(error); + xml = Serializer.serializeFault(fault); } else { xml = Serializer.serializeMethodResponse(value); 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..01e70da --- /dev/null +++ b/test/fixtures/good_food/string_cdata_fault.xml @@ -0,0 +1 @@ +Congrats]]> diff --git a/test/fixtures/good_food/string_code_fault.xml b/test/fixtures/good_food/string_code_fault.xml new file mode 100644 index 0000000..e710da9 --- /dev/null +++ b/test/fixtures/good_food/string_code_fault.xml @@ -0,0 +1 @@ +faultCode123faultStringtestString diff --git a/test/fixtures/good_food/string_empty_fault.xml b/test/fixtures/good_food/string_empty_fault.xml new file mode 100644 index 0000000..85b4709 --- /dev/null +++ b/test/fixtures/good_food/string_empty_fault.xml @@ -0,0 +1 @@ + diff --git a/test/fixtures/good_food/string_fault.xml b/test/fixtures/good_food/string_fault.xml new file mode 100644 index 0000000..23f7828 --- /dev/null +++ b/test/fixtures/good_food/string_fault.xml @@ -0,0 +1 @@ +testString diff --git a/test/fixtures/good_food/string_multiline_cdata_fault.xml b/test/fixtures/good_food/string_multiline_cdata_fault.xml new file mode 100644 index 0000000..54e14db --- /dev/null +++ b/test/fixtures/good_food/string_multiline_cdata_fault.xml @@ -0,0 +1,4 @@ + +Go testing! +Congrats +]]> diff --git a/test/serializer_test.js b/test/serializer_test.js index dbb6f61..a20e13c 100644 --- a/test/serializer_test.js +++ b/test/serializer_test.js @@ -4,7 +4,8 @@ var vows = require('vows') , assert = require('assert') , Serializer = require('../lib/serializer') , CustomType = require('../lib/customtype') - , util = require('util') + , xmlrpcError= require('../lib/error') + , util = require('util') vows.describe('Serializer').addBatch({ @@ -221,6 +222,103 @@ vows.describe('Serializer').addBatch({ , 'contains the encoding attribute': assertXml('good_food/encoded_call.xml') } } + + ,'serializeFault() called with': { + + 'type': { + + 'string' : { + 'with a regular string param' : { + topic: function () { + var value = 'testString' + return Serializer.serializeFault(value) + } + , 'contains the string': assertXml('good_food/string_fault.xml') + } + , 'with a string param that requires CDATA' : { + topic: function () { + var value = 'Congrats' + return Serializer.serializeFault(value) + } + , 'contains the CDATA-wrapped string': assertXml('good_food/string_cdata_fault.xml') + } + , 'with a multiline string param that requires CDATA' : { + topic: function () { + var value = '\nGo testing!\nCongrats\n' + return Serializer.serializeFault(value) + } + , 'contains the CDATA-wrapped string': assertXml('good_food/string_multiline_cdata_fault.xml') + } + , 'with an empty string' : { + topic: function () { + var value = '' + return Serializer.serializeFault(value) + } + , 'contains the empty string': assertXml('good_food/string_empty_fault.xml') + } + } + + , 'XmlRpcError' : { + 'with a code field' : { + topic: function () { + // PHP XML-RPC style + var error = xmlrpcError.makeError({ + faultString: 'testString' + }, { + faultCode: 123 + }) + var value = xmlrpcError.makeResponseObjectFromError(error); + return Serializer.serializeFault(value) + } + , 'contains the string': assertXml('good_food/string_code_fault.xml') + } + , 'without a code field' : { + topic: function () { + // PHP XML-RPC style + var error = xmlrpcError.makeError({ + faultString: 'testString' + }) + var value = xmlrpcError.makeResponseObjectFromError(error); + return Serializer.serializeFault(value) + } + , 'contains the string': assertXml('good_food/string_fault.xml') + } + } + + , 'Error' : { + 'with message' : { + topic: function () { + // PHP XML-RPC style + var error = new Error('testString'); + var value = xmlrpcError.makeResponseObjectFromError(error); + return Serializer.serializeFault(value) + } + , 'contains the string': assertXml('good_food/string_fault.xml') + } + } + + , 'Object' : { + 'with message' : { + topic: function () { + // PHP XML-RPC style + var error = new Object('testString'); + var value = xmlrpcError.makeResponseObjectFromError(error); + return Serializer.serializeFault(value) + } + , 'contains the string': assertXml('good_food/string_fault.xml') + } + } + + } + + , 'utf-8 encoding': { + topic: function () { + var value = "\x46\x6F\x6F" + return Serializer.serializeFault(value) + } + , 'contains the encoding attribute': assertXml('good_food/encoded_fault.xml') + } + } , 'serializeMethodResponse() called with': { From 1e9be31941829578b08898f79e83df52a24ceedc Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Tue, 5 Jan 2016 22:53:12 +0600 Subject: [PATCH 10/13] fix test failures --- lib/error.js | 2 ++ test/fixtures/good_food/string_code_fault.xml | 2 +- test/fixtures/good_food/string_multiline_cdata_fault.xml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/error.js b/lib/error.js index b238495..7026018 100644 --- a/lib/error.js +++ b/lib/error.js @@ -76,6 +76,8 @@ xmlrpc.makeResponseObjectFromError = function(error) { fault[codeFieldName] = code; } return fault; + } else if (error instanceof Error) { + return (('' !== error.message) ? error.message : error.name); } else if (error instanceof Object) { return error.toString(); } diff --git a/test/fixtures/good_food/string_code_fault.xml b/test/fixtures/good_food/string_code_fault.xml index e710da9..0dda9c9 100644 --- a/test/fixtures/good_food/string_code_fault.xml +++ b/test/fixtures/good_food/string_code_fault.xml @@ -1 +1 @@ -faultCode123faultStringtestString +faultStringtestStringfaultCode123 diff --git a/test/fixtures/good_food/string_multiline_cdata_fault.xml b/test/fixtures/good_food/string_multiline_cdata_fault.xml index 54e14db..3ff9185 100644 --- a/test/fixtures/good_food/string_multiline_cdata_fault.xml +++ b/test/fixtures/good_food/string_multiline_cdata_fault.xml @@ -1,4 +1,4 @@ Go testing! Congrats -]]> +]]> From d3dedb25bc07ec16dacfc20cdc0057b733c860bf Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Wed, 6 Jan 2016 00:53:10 +0600 Subject: [PATCH 11/13] simplify error usage - according to spec: http://xmlrpc.scripting.com/spec.html --- README.md | 28 ++------- lib/error.js | 63 ++++++------------- .../fixtures/good_food/string_cdata_fault.xml | 2 +- .../fixtures/good_food/string_empty_fault.xml | 2 +- test/fixtures/good_food/string_fault.xml | 2 +- .../string_multiline_cdata_fault.xml | 4 +- test/serializer_test.js | 11 +--- 7 files changed, 31 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 56d62b3..83b3495 100644 --- a/README.md +++ b/README.md @@ -185,28 +185,10 @@ There is a special error type defined - `XmlRpcError`. And a helper function mak Use it to create an error and pass it to the `callback`. ```javascript -// Makes an error with only message and no code +// Makes an error with only message and code defaults to zero (0) xmlrpc.makeError("Error occured") ``` -The resulting response would be: -```xml - - - - - Error occured - - - -``` - -The error with a code example: -```javascript -// Makes an error with message in a field named 'message' and code in a field named 'code' -xmlrpc.makeError("Error occured", 123) -``` - The resulting response would be: ```xml @@ -217,7 +199,7 @@ The resulting response would be: code - 123 + 0 @@ -232,10 +214,10 @@ The resulting response would be: ``` -The PHP XML-RPC fault response style: +The error with a code example: ```javascript -// Makes an error with message in a field named 'faultString' and code in a field named 'faultCode' -xmlrpc.makeError({faultString: "Error occured"}, {faultCode: 123}) +// Makes an error with message and code +xmlrpc.makeError("Error occured", 123) ``` The resulting response would be: diff --git a/lib/error.js b/lib/error.js index 7026018..9b24bad 100644 --- a/lib/error.js +++ b/lib/error.js @@ -11,15 +11,14 @@ xmlrpc.XmlRpcError = error('XmlRpcError'); * The helper function to create xml-rpc error * * Examples: - * // Makes an error with only message and no code + * // Makes an error with only message and default (zero 0) code * xmlrpc.makeError("Error occured") - * // Makes an error with message in a field named 'message' and code in a field named 'code' + * // Makes an error with message and code * xmlrpc.makeError("Error occured", 123) - * // Makes an error with message in a field named 'faultString' and code in a field named 'faultCode' - * xmlrpc.makeError({faultString: "Error occured"}, {faultCode: 123}) * - * @param {String|Object} message The error message or object like {faultString: "Error occured"} - * @param {Number|Object} code The error code or object like {faultCode: 123} + * @param {String} message The error message + * @param {Number} code The error code + * * @returns {XmlRpcError} The error object */ xmlrpc.makeError = function (message, code) { @@ -31,7 +30,7 @@ xmlrpc.makeError = function (message, code) { break; } } - return new xmlrpc.XmlRpcError(msg, {messageArg: message, codeArg: code}); + return new xmlrpc.XmlRpcError(message, {codeArg: code}); } /** @@ -40,46 +39,22 @@ xmlrpc.makeError = function (message, code) { * @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) { - var messageFieldName = 'message'; - var message = error.message; - if ('undefined' !== typeof error.messageArg) { - if ('string' === typeof error.messageArg) { - message = error.messageArg; - } else if ('object' === typeof error.messageArg) { - for (var i in error.messageArg) { - messageFieldName = i; - message = error.messageArg[i]; - // we need only one field - break; - } - } - } - var codeFieldName = 'code'; - var code = undefined; - if ('undefined' !== typeof error.codeArg) { - if ('number' === typeof error.codeArg) { - code = error.codeArg; - } else if ('object' === typeof error.codeArg) { - for (var j in error.codeArg) { - codeFieldName = j; - code = error.codeArg[j]; - // we need only one field - break; - } - } - } - var fault = message; - if ('undefined' !== typeof code) { - fault = {}; - fault[messageFieldName] = message; - fault[codeFieldName] = code; + message = error.message; + if ('number' === typeof error.codeArg) { + code = error.codeArg; } - return fault; } else if (error instanceof Error) { - return (('' !== error.message) ? error.message : error.name); + message = (('' !== error.message) ? error.message : error.name); } else if (error instanceof Object) { - return error.toString(); + message = error.toString(); + } else { + message = error + ''; } - return error + ''; + var fault = {}; + fault['faultString'] = message; + fault['faultCode'] = code; + return fault; } diff --git a/test/fixtures/good_food/string_cdata_fault.xml b/test/fixtures/good_food/string_cdata_fault.xml index 01e70da..0883835 100644 --- a/test/fixtures/good_food/string_cdata_fault.xml +++ b/test/fixtures/good_food/string_cdata_fault.xml @@ -1 +1 @@ -Congrats]]> +faultStringCongrats]]>faultCode123 diff --git a/test/fixtures/good_food/string_empty_fault.xml b/test/fixtures/good_food/string_empty_fault.xml index 85b4709..5b23393 100644 --- a/test/fixtures/good_food/string_empty_fault.xml +++ b/test/fixtures/good_food/string_empty_fault.xml @@ -1 +1 @@ - +faultStringfaultCode123 diff --git a/test/fixtures/good_food/string_fault.xml b/test/fixtures/good_food/string_fault.xml index 23f7828..006752a 100644 --- a/test/fixtures/good_food/string_fault.xml +++ b/test/fixtures/good_food/string_fault.xml @@ -1 +1 @@ -testString +faultStringtestStringfaultCode0 diff --git a/test/fixtures/good_food/string_multiline_cdata_fault.xml b/test/fixtures/good_food/string_multiline_cdata_fault.xml index 3ff9185..b265593 100644 --- a/test/fixtures/good_food/string_multiline_cdata_fault.xml +++ b/test/fixtures/good_food/string_multiline_cdata_fault.xml @@ -1,4 +1,4 @@ - +faultString Go testing! Congrats -]]> +]]>faultCode123 diff --git a/test/serializer_test.js b/test/serializer_test.js index a20e13c..b5377af 100644 --- a/test/serializer_test.js +++ b/test/serializer_test.js @@ -261,12 +261,7 @@ vows.describe('Serializer').addBatch({ , 'XmlRpcError' : { 'with a code field' : { topic: function () { - // PHP XML-RPC style - var error = xmlrpcError.makeError({ - faultString: 'testString' - }, { - faultCode: 123 - }) + var error = xmlrpcError.makeError('testString', 123) var value = xmlrpcError.makeResponseObjectFromError(error); return Serializer.serializeFault(value) } @@ -275,9 +270,7 @@ vows.describe('Serializer').addBatch({ , 'without a code field' : { topic: function () { // PHP XML-RPC style - var error = xmlrpcError.makeError({ - faultString: 'testString' - }) + var error = xmlrpcError.makeError('testString') var value = xmlrpcError.makeResponseObjectFromError(error); return Serializer.serializeFault(value) } From 532e94841f03b70481d63268a220f6a2c88dce58 Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Wed, 6 Jan 2016 01:08:50 +0600 Subject: [PATCH 12/13] fix test failures --- test/serializer_test.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/serializer_test.js b/test/serializer_test.js index b5377af..a95453a 100644 --- a/test/serializer_test.js +++ b/test/serializer_test.js @@ -230,28 +230,32 @@ vows.describe('Serializer').addBatch({ 'string' : { 'with a regular string param' : { topic: function () { - var value = 'testString' + var error = 'testString' + var value = xmlrpcError.makeResponseObjectFromError(error); return Serializer.serializeFault(value) } , 'contains the string': assertXml('good_food/string_fault.xml') } , 'with a string param that requires CDATA' : { topic: function () { - var value = 'Congrats' + var error = 'Congrats' + var value = xmlrpcError.makeResponseObjectFromError(error); return Serializer.serializeFault(value) } , 'contains the CDATA-wrapped string': assertXml('good_food/string_cdata_fault.xml') } , 'with a multiline string param that requires CDATA' : { topic: function () { - var value = '\nGo testing!\nCongrats\n' + var error = '\nGo testing!\nCongrats\n' + var value = xmlrpcError.makeResponseObjectFromError(error); return Serializer.serializeFault(value) } , 'contains the CDATA-wrapped string': assertXml('good_food/string_multiline_cdata_fault.xml') } , 'with an empty string' : { topic: function () { - var value = '' + var error = '' + var value = xmlrpcError.makeResponseObjectFromError(error); return Serializer.serializeFault(value) } , 'contains the empty string': assertXml('good_food/string_empty_fault.xml') From c911191c24168eee14598e0c7fbb92cb3d33de31 Mon Sep 17 00:00:00 2001 From: Oleg Abrosimov Date: Wed, 6 Jan 2016 01:26:33 +0600 Subject: [PATCH 13/13] fix test failures --- test/fixtures/good_food/string_cdata_fault.xml | 2 +- test/fixtures/good_food/string_empty_fault.xml | 2 +- test/fixtures/good_food/string_multiline_cdata_fault.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fixtures/good_food/string_cdata_fault.xml b/test/fixtures/good_food/string_cdata_fault.xml index 0883835..627acbc 100644 --- a/test/fixtures/good_food/string_cdata_fault.xml +++ b/test/fixtures/good_food/string_cdata_fault.xml @@ -1 +1 @@ -faultStringCongrats]]>faultCode123 +faultStringCongrats]]>faultCode0 diff --git a/test/fixtures/good_food/string_empty_fault.xml b/test/fixtures/good_food/string_empty_fault.xml index 5b23393..b3ca59e 100644 --- a/test/fixtures/good_food/string_empty_fault.xml +++ b/test/fixtures/good_food/string_empty_fault.xml @@ -1 +1 @@ -faultStringfaultCode123 +faultStringfaultCode0 diff --git a/test/fixtures/good_food/string_multiline_cdata_fault.xml b/test/fixtures/good_food/string_multiline_cdata_fault.xml index b265593..fc29cca 100644 --- a/test/fixtures/good_food/string_multiline_cdata_fault.xml +++ b/test/fixtures/good_food/string_multiline_cdata_fault.xml @@ -1,4 +1,4 @@ faultString Go testing! Congrats -]]>faultCode123 +]]>faultCode0