Jayson
Jayson is a JSON-RPC 2.0 and 1.0 compliant server and client written in JavaScript for node.js that aims to be as simple as possible to use.
Table of contents
Features
Example
A basic JSON-RPC 2.0 server via HTTP:
Server example in examples/simple_example/server.js:
var jayson = require('jayson');
var server = jayson.server({
add: function(args, callback) {
callback(null, args[0] + args[1]);
}
});
server.http().listen(3000);
Client example in examples/simple_example/client.js invoking add
on the above server:
var jayson = require('jayson');
var client = jayson.client.http({
port: 3000
});
client.request('add', [1, 1], function(err, response) {
if(err) throw err;
console.log(response.result);
});
Installation
Install the latest version of jayson from npm by executing npm install jayson
in your shell. Do a global install with npm install --global jayson
if you want the jayson
client CLI in your PATH.
Changelog (only notable milestones)
- 2.1.0
- Experimental typescript support
- 2.0.6
- 2.0.0
- Added support for promises
- Breaking:
collect: true
is now the default option for a new jayson.Server
and jayson.Method
- 1.2.0
- 1.1.1
- More http server events
- Remove fork server and client
- Add server routing
- 1.0.11
Add support for a HTTPS client
- 1.0.9
Add support for TCP servers and clients
CLI client
There is a basic CLI client in bin/jayson.js
and it should be available as jayson
in your shell if you installed the package globally. Run jayson --help
to see how it works.
Requirements
Jayson does not have any special dependencies that cannot be resolved with a simple npm install
. It is being continuously tested using travis-ci. You can look inside .travis.yml if you want to see which versions are tested against.
Class documentation
In addition to this document, a comprehensive class documentation made with jsdoc is available at jayson.tedeh.net.
Running tests
- Change directory to the repository root
- Install the development packages by executing
npm install --dev
- Run the tests with
npm run test
Typescript
Since v2.1.0
there is experimental typescript support available with jayson. Anyone that would like to improve on the type definitions is most welcome to provide a pull request.
If you would just like to report an issue with the type definitions that you can't solve by yourself, please read the previous discussion here first: https://github.com/tedeh/jayson/issues/99
Usage
Client
The client is available as the Client
or client
property of require('jayson')
.
Client interface description
Name | Description |
---|
Client | Base class |
Client.tcp | TCP interface |
Client.tls | TLS interface |
Client.http | HTTP interface |
Client.https | HTTPS interface |
Client.browser | Browser interface |
Every client supports these options:
Option | Default | Type | Description |
---|
reviver | undefined | Function | JSON.parse reviver |
replacer | undefined | Function | JSON.stringify replacer |
generator | RFC4122 generator | Function | Generates a String for request ID. |
version | 2 | Number | JSON-RPC version to support (1 or 2) |
Client.http
Uses the same options as http.request in addition to these options:
Option | Default | Type | Description |
---|
encoding | utf8 | String | Determines the encoding to use |
headers | undefined | Object | Extend the headers sent by the client |
Client.http Events
The HTTP server will emit the following events:
Event | When | Arguments | Notes |
---|
http request | Created an HTTP request | 1. Instance of http.ClientRequest | |
http response | Received an HTTP response | 1. Instance of http.IncomingMessage 2. Instance of http.ClientRequest | |
http error | Underlying stream emits error | 1. Error | |
http timeout | Underlying stream emits timeout | | Automatically causes the request to abort |
It is possible to pass a string URL as the first argument. The URL will be run through url.parse. Example:
var jayson = require('jayson');
var client = jayson.client.http('http://localhost:3000');
Client.https
Uses the same options as https.request in addition to the same options as Client.http
. This means it is also possible
to pass a string URL as the first argument and have it interpreted by url.parse.
Will emit the same custom events as Client.http
.
Client.tcp
Uses the same options as net.connect in addition to the same options as Client.http
.
Client.tls
Uses the same options as tls.connect in addition to the same options as Client.http
.
Client.browser
The browser client is a simplified version of the regular client for use browser-side. It does not have any dependencies on node.js core libraries (but does depend on the uuid
and lodash
) and also does not know how to "send" a request to a server like the other clients.
Because it does not depend on any core libraries, the browser client is not an instance of JaysonClient
or EventEmitter
and therefore does not emit any of the normal request events that the other clients do.
To use the browser client, require('jayson/lib/client/browser')
and pass a calling/transport function as the first argument. The transport function receives a JSON-RPC string request and is expected to callback with a string response received from the server (not JSON) or an error (not a JSON-RPC error).
The reason for dealing with strings is to support the reviver
and replacer
options like the other clients.
This client example in examples/browser_client/client.js below uses node-fetch in the transport function, but a dropin replacement for use in an actual browser could instead use whatwg-fetch.
var jaysonBrowserClient = require('./../../lib/client/browser');
var fetch = require('node-fetch');
var callServer = function(request, callback) {
var options = {
method: 'POST',
body: request,
headers: {
'Content-Type': 'application/json',
}
};
fetch('http://localhost:3000', options)
.then(function(res) { return res.text(); })
.then(function(text) { callback(null, text); })
.catch(function(err) { callback(err); });
};
var client = jaysonBrowserClient(callServer, {
});
client.request('multiply', [5, 5], function(err, error, result) {
if(err) throw err;
console.log(result);
});
Notifications
Notification requests are for cases where the reply from the server is not important and should be ignored. This is accomplished by setting the id
property of a request object to null
.
Client example in examples/notifications/client.js doing a notification request:
var jayson = require('jayson');
var client = jayson.client.http({
port: 3000
});
client.request('ping', [], null, function(err) {
if(err) throw err;
console.log('ok');
});
Server example in examples/notifications/server.js:
var jayson = require('jayson');
var server = jayson.server({
ping: function(args, callback) {
callback();
}
});
server.http().listen(3000);
Notes
- Any value that the server returns will be discarded when doing a notification request.
- Omitting the third argument
null
to Client.prototype.request
does not generate a notification request. This argument has to be set explicitly to null
for this to happen. - Network errors and the like will still reach the callback. When the callback is invoked (with or without error) one can be certain that the server has received the request.
- See the Official JSON-RPC 2.0 Specification for additional information on how Jayson handles notifications that are erroneous.
Batches
A batch request is an array of individual requests that are sent to the server as one. Doing a batch request is very simple in Jayson and consists of constructing an array of individual requests (created by not passing a callback to Client.prototype.request
) that is then itself passed to Client.prototype.request
.
Combined server/client example in examples/batch_request/index.js:
var jayson = require('jayson');
var server = jayson.server({
add: function(args, callback) {
callback(null, args[0] + args[1]);
}
});
var client = jayson.client(server);
var batch = [
client.request('does_not_exist', [10, 5]),
client.request('add', [1, 1]),
client.request('add', [0, 0], null)
];
client.request(batch, function(err, errors, successes) {
if(err) throw err;
console.log('errors', errors);
console.log('successes', successes);
});
client.request(batch, function(err, responses) {
if(err) throw err;
console.log('responses', responses);
});
Notes
- See the Official JSON-RPC 2.0 Specification for additional information on how Jayson handles different types of batches, mainly with regards to notifications, request errors and so forth.
- There is no guarantee that the results will be in the same order as request Array
request
. To find the right result, compare the ID from the request with the ID in the result yourself.
Client callback syntactic sugar
When the length (number of arguments) of a client callback function is either 2 or 3 it receives slightly different values when invoked.
- 2 arguments: first argument is an error or
null
, second argument is the response object as returned (containing either a result
or a error
property) or null
for notifications. - 3 arguments: first argument is an error or null, second argument is a JSON-RPC
error
property or null
(if success), third argument is a JSON-RPC result
property or null
(if error).
When doing a batch request with a 3-length callback, the second argument will be an array of requests with a error
property and the third argument will be an array of requests with a result
property.
Client events
A client will emit the following events (in addition to any special ones emitted by a specific interface):
Event | When | Arguments | Notes |
---|
request | About to dispatch a request | 1: Request object | |
response | Received a response | 1: Request object 2: Response object received | |
Server
The server classes are available as the Server
or server
property of require('jayson')
.
The server also sports several interfaces that can be accessed as properties of an instance of Server
.
Server interface description
Name | Description |
---|
Server | Base interface for a server that supports receiving JSON-RPC requests |
Server.tcp | TCP server that inherits from net.Server |
Server.tls | TLS server that inherits from tls.Server |
Server.http | HTTP server that inherits from http.Server |
Server.https | HTTPS server that inherits from https.Server |
Server.middleware | Method that returns a Connect/Express compatible middleware function |
Servers supports these options:
Option | Default | Type | Description |
---|
reviver | null | Function | JSON.parse reviver |
replacer | null | Function | JSON.stringify replacer |
router | null | Function | Return the function for method routing |
collect | true | Boolean | Passed to methodConstructor options |
params | undefined | Array/Object/null | Passed to methodConstructor options |
methodConstructor | jayson.Method | Function | Server functions are made an instance of this class |
version | 2 | Number | JSON-RPC version to support (1 or 2) |
Server.tcp
Uses the same options as the base class. Inherits from net.Server.
Server.tls
Uses the same options as the base class. Inherits from tls.Server.
Server.http
Uses the same options as the base class. Inherits from http.Server.
Server.http Events
Event | When | Arguments | Notes |
---|
http request | Incoming HTTP request | 1. Instance of http.IncomingMessage | |
http response | About to send a HTTP response | 1. Instance of http.ServerResponse 2. Instance of http. IncomingMessage | |
Server.https
Uses the same options as the base class. Inherits from https.Server and jayson.Server.http
. For information on how to configure certificates, see the documentation on https.Server.
Will emit the same custom events as Server.http
.
Server.middleware
Uses the same options as the base class. Returns a function that is compatible with Connect or Express. Will expect the request to be req.body
, meaning that the request body must be parsed (typically using connect.bodyParser
) before the middleware is invoked.
The middleware supports the following options:
Option | Default | Type | Description |
---|
end | true | Boolean | If set to false causes the middleware to next() instead of res.end() when finished. |
Middleware example in examples/middleware/server.js:
var jayson = require('jayson');
var jsonParser = require('body-parser').json;
var connect = require('connect');
var app = connect();
var server = jayson.server({
add: function(args, callback) {
callback(null, args[0] + args[1]);
}
});
app.use(jsonParser());
app.use(server.middleware());
app.listen(3000);
Many interfaces at the same time
A Jayson server can use many interfaces at the same time.
Server example in examples/many_interfaces/server.js that listens to both http
and a https
requests:
var jayson = require('jayson');
var server = jayson.server();
var http = server.http();
var https = server.https({
});
http.listen(80, function() {
console.log('Listening on *:80');
});
https.listen(443, function() {
console.log('Listening on *:443');
});
Using the server as a relay
Passing an instance of a client as a method to the server makes the server relay incoming requests to wherever the client is pointing to. This might be used to delegate computationally expensive functions into a separate server or to abstract a cluster of servers behind a common interface.
Frontend server example in examples/relay/server_public.js listening on *:3000
:
var jayson = require('jayson');
var server = jayson.server({
add: jayson.client.http({
port: 3001
})
});
server.http().listen(3000);
Backend server example in examples/relay/server_private.js listening on *:3001
:
var jayson = require('jayson');
var server = jayson.server({
add: function(args, callback) {
callback(null, args[0] + args[1]);
}
});
server.http().listen(3001);
Every request to add
on the public server will now relay the request to the private server. See the client example in examples/relay/client.js.
Method routing
Passing a property named router
in the server options will enable you to write your own logic for routing requests to specific functions.
Server example with custom routing logic in examples/method_routing/server.js:
var jayson = require('jayson');
var methods = {
add: function(args, callback) {
callback(null, args[0] + args[1]);
}
};
var server = jayson.server(methods, {
router: function(method, params) {
if(typeof(this._methods[method]) === 'function') return this._methods[method];
if(method === 'add_2') {
var fn = server.getMethod('add').getHandler();
return new jayson.Method(function(args, done) {
args.unshift(2);
fn(args, done);
});
}
}
});
server.http().listen(3000);
Client example in examples/method_routing/client.js invoking add_2
on the above server:
var jayson = require('jayson');
var client = jayson.client.http({
port: 3000
});
client.request('add_2', [3], function(err, response) {
if(err) throw err;
console.log(response.result);
});
Server example of nested routes where each property is separated by a dot (you do not need to use the router option for this):
var _ = require('lodash');
var jayson = require('jayson');
var methods = {
foo: {
bar: function(callback) {
callback(null, 'ping pong');
}
},
math: {
add: function(args, callback) {
callback(null, args[0] + args[1]);
}
}
};
var map = _.reduce(methods, collapse('', '.'), {});
var server = jayson.server(map);
function collapse(stem, sep) {
return function(map, value, key) {
var prop = stem ? stem + sep + key : key;
if(_.isFunction(value)) map[prop] = value;
else if(_.isObject(value)) map = _.reduce(value, collapse(prop, sep), map);
return map;
}
}
Notes
- If
router
does not return anything, the server will respond with a Method Not Found
error. - The
Server.prototype
methods method
, methods
, removeMethod
and hasMethod
will not use the router
method, but will operate on the internal Server.prototype._methods
map. - The
router
method is expected to return instances of jayson.Method
(>=1.2.0)
Method definition
You can also define server methods inside a wrapping object named jayson.Method
. This allows additional options about the method to be specified. Using this wrapper - explicitly or implicitly (via server options) - makes it trivial to have your method accept a variable amount of arguments.
The method class is available as the Method
or method
property of require('jayson')
. It supports these options:
Option | Default | Type | Description |
---|
handler | | Function | The actual function that will handle a JSON-RPC request to this method |
collect | >= 2.0.0 true before false | Boolean | Collect JSON-RPC parameters in a single function argument |
params | null | `Array | Object |
Server example showcasing most features and options in examples/method_definitions/server.js:
var jayson = require('jayson');
var _ = require('lodash');
var methods = {
sum: function(args, done) {
done(null, sum(args));
},
sumCollect: new jayson.Method({
handler: function(args, done) {
var total = sum(args);
done(null, total);
},
collect: true
}),
sumDefault: jayson.Method(function(args, done) {
var total = sum(args);
done(null, total);
}, {
collect: true,
params: {a: 2, b: 5}
}),
isArray: new jayson.Method({
handler: function(args, done) {
var result = _.isArray(args);
done(null, result);
},
collect: true,
params: Array
})
};
var server = jayson.server(methods, {
collect: true,
params: Array
});
server.http().listen(3000);
function sum(list) {
return _.reduce(list, function(sum, val) {
return sum + val;
}, 0);
}
Client example in examples/method_definitions/client.js:
var jayson = require('jayson');
var client = jayson.client.http({
port: 3000
});
client.request('sumCollect', [3, 5, 9, 11], function(err, response) {
if(err) throw err;
console.log(response.result);
});
client.request('sumCollect', {a: 2, b: 3, c: 4}, function(err, response) {
if(err) throw err;
console.log(response.result);
});
client.request('sumDefault', {b: 10}, function(err, response) {
if(err) throw err;
console.log(response.result);
});
client.request('isArray', {a: 5, b: 2, c: 9}, function(err, response) {
if(err) throw err;
console.log(response.result);
});
client.request('sum', [1, 2, 3], function(err, response) {
if(err) throw err;
console.log(response.result);
});
Notes
- Adding methods as a plain JavaScript function creates an instance of
jayson.Method
internally. For backwards compatibility it will be created with the option "collect" set to false
(v2.0.0). It is possible to affect this by passing the collect
option to the server. This works similarly for the params
option.
Server events
In addition to events that are specific to certain interfaces, all servers will emit the following events:
Event | When | Arguments | Notes |
---|
request | Interpretable non-batch request received | 1: Request object | |
response | Returning a response | 1: Request object 2: Response object | |
batch | Interpretable batch request received | 1. Array of requests | Emits request for every part |
Server Errors
If you should like to return an error from an method request to indicate a failure, remember that the JSON-RPC 2.0 specification requires the error to be an Object
with a code (Integer/Number)
to be regarded as valid. You can also provide a message (String)
and a data (Object)
with additional information. Example:
var jayson = require('jayson');
var server = jayson.server({
i_cant_find_anything: function(args, callback) {
var error = {code: 404, message: 'Cannot find ' + args.id};
callback(error);
},
i_cant_return_a_valid_error: function(callback) {
callback({message: 'I forgot to enter a code'});
}
});
Predefined Errors
It is also possible to cause a method to return one of the predefined JSON-RPC 2.0 error codes using the server helper function Server.prototype.error
inside of a server method. Example:
var jayson = require('jayson');
var server = jayson.server({
invalid_params: function(args, callback) {
var error = this.error(-32602);
callback(error);
}
});
You can even override the default messages:
var jayson = require('jayson');
var server = jayson.server({
error_giver_of_doom: function(callback) {
callback(true)
}
});
server.errorMessages[Server.errors.INTERNAL_ERROR] = 'I has a sad. I cant do anything right';
Server CORS
Jayson does not include functionality for supporting CORS requests natively but it is easy to use a CORS-enabling middleware
like cors. An example of this can be found in examples/cors/server.js:
var jayson = require('jayson');
var cors = require('cors');
var connect = require('connect');
var jsonParser = require('body-parser').json;
var app = connect();
var server = jayson.server({
myNameIs: function(args, callback) {
callback(null, 'Your name is: ' + args.name);
}
});
app.use(cors({methods: ['POST']}));
app.use(jsonParser());
app.use(server.middleware());
app.listen(3000);
Revivers and Replacers
JSON lacks support for representing types other than the simple ones defined in the JSON specification. Fortunately the JSON methods in JavaScript (JSON.parse
and JSON.stringify
) provide options for custom serialization/deserialization routines. Jayson allows you to pass your own routines as options to both clients and servers.
Simple example transferring the state of an object between a client and a server:
Shared code between the server and the client in examples/reviving_and_replacing/shared.js:
var Counter = exports.Counter = function(value) {
this.count = value || 0;
};
Counter.prototype.increment = function() {
this.count += 1;
};
exports.replacer = function(key, value) {
if(value instanceof Counter) {
return {$class: 'counter', $props: {count: value.count}};
}
return value;
};
exports.reviver = function(key, value) {
if(value && value.$class === 'counter') {
var obj = new Counter();
for(var prop in value.$props) obj[prop] = value.$props[prop];
return obj;
}
return value;
};
Server example in examples/reviving_and_replacing/server.js:
var jayson = require('jayson');
var shared = require('./shared');
var options = {
reviver: shared.reviver,
replacer: shared.replacer
};
var server = jayson.server({
increment: function(args, callback) {
args.counter.increment();
callback(null, args.counter);
}
}, options);
server.http().listen(3000);
A client example in examples/reviving_and_replacing/client.js invoking "increment" on the server:
var jayson = require('jayson');
var shared = require('./shared');
var client = jayson.client.http({
port: 3000,
reviver: shared.reviver,
replacer: shared.replacer
});
var params = {
counter: new shared.Counter(2)
}
client.request('increment', params, function(err, response) {
if(err) throw err;
var result = response.result;
console.log(
result instanceof shared.Counter,
result.count,
params.counter === result
);
});
Notes
- Instead of using a replacer, it is possible to define a
toJSON
method for any JavaScript object. Unfortunately there is no corresponding method for reviving objects (that would not work, obviously), so the reviver always has to be set up manually.
Named parameters
It is possible to specify named parameters when doing a client request by passing an Object instead of an Array.
Client example in examples/named_parameters/client.js:
var jayson = require('jayson');
var client = jayson.client.http({
port: 3000
});
client.request('add', {b: 1, a: 2}, function(err, response) {
if(err) throw err;
console.log(response.result);
});
Server example in examples/named_parameters/server.js:
var jayson = require('jayson');
var server = jayson.server({
add: function(a, b, callback) {
callback(null, a + b);
}
}, {
collect: false
});
server.http().listen(3000);
Notes
- If requesting methods on a Jayson server, arguments left out will be
undefined
- Too many arguments or arguments with invalid names will be ignored
- It is assumed that the last argument to a server method is the callback and it will not be filled with something else
- Parsing a function signature and filling in arguments is generally not recommended and should be avoided
Promises
Since version 2.0.0
A separate tree that does limited usage of the ES6 Promise object is available. The internal API remains callback based, with the addition that promises may be used for two things:
- Returning a Promise when requesting a JSON-RPC method using a Client
- Returning a Promise inside of a Server method
To use the separate tree, do a require('jayson/promise')
instead of require('jayson')
.
Server example in examples/promise/server.js showing how to return a Promise
in a server method:
var jayson = require('../../promise');
var _ = require('lodash');
var server = jayson.server({
add: function(args) {
return new Promise(function(resolve, reject) {
var sum = _.reduce(args, function(sum, value) { return sum + value; }, 0);
resolve(sum);
});
},
rejection: function(args) {
return new Promise(function(resolve, reject) {
reject(server.error(501, 'not implemented'));
});
}
});
server.http().listen(3000);
Client example in examples/promise/client.js showing how to do a request:
var jayson = require('../../promise');
var client = jayson.client.http({
port: 3000
});
var reqs = [
client.request('add', [1, 2, 3, 4, 5]),
client.request('rejection', [])
];
Promise.all(reqs).then(function(responses) {
console.log(responses[0].result);
console.log(responses[1].error);
});
Notes
- JSON-RPC errors will not result in rejection of the Promise. It is however possible that a future version will include a client setting to have JSON-RPC errors result in rejection. Please note that network errors and the like will result in rejection.
- A
Promise
is considered to have been returned from a server method if the returned object has a property then
that is a function.
Promise Batches
Since version 2.0.5
Sometimes you may want to return raw requests from a promise client. This needs to be handled differently, because PromiseClient.prototype.request
would normally always be expected to return a Promise which we in this case don't want.
To solve this, we need to set the fourth parameter to PromiseClient.prototype.request
explicitly to false
in order to not return a Promise.
Client example in examples/promise_batches/client.js showing how to properly execute a batch request:
var jayson = require('../../promise');
var client = jayson.client.http({
port: 3000
});
var batch = [
client.request('add', [1, 2, 3, 4, 5], undefined, false),
client.request('add', [5, 6, 7, 8, 9], undefined, false),
];
client.request(batch).then(function(responses) {
console.log(responses[0].result);
console.log(responses[1].result);
});
Notes
- The third parameter to
PromiseClient.prototype.request
above is explicitly set to undefined
- this parameter would normally represent the desired ID of the call. Remember that null
would mean a notification (which does not return a response) and other falsy values may actually be used as ids. Setting undefined
ensures that the id is generated automatically.
Contributing
Highlighting issues or submitting pull
requests on Github is most welcome.
Please make sure to follow the style of the project, and lint your code with npm run lint
before submitting a patch.