Package Exports
- json-rpc-2.0
- json-rpc-2.0/dist/index.js
- json-rpc-2.0/dist/server
- json-rpc-2.0/dist/server.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (json-rpc-2.0) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
json-rpc-2.0
Let your client and server talk over function calls under JSON-RPC 2.0 spec.
- Protocol agnostic
- Use over HTTP, WebSocket, TCP, UDP, inter-process, whatever else
- Easy migration from HTTP to WebSocket, for example
- Use over HTTP, WebSocket, TCP, UDP, inter-process, whatever else
- No external dependencies
- Keep your package small
- Stay away from dependency hell
- Works in both browser and Node.js
- First-class TypeScript support
- Written in TypeScript
- Strongly typed client and server calls
Install
npm install --save json-rpc-2.0
Example
The example uses HTTP for communication protocol, but it can be anything.
Server
const express = require("express");
const bodyParser = require("body-parser");
const { JSONRPCServer } = require("json-rpc-2.0");
const server = new JSONRPCServer();
// First parameter is a method name.
// Second parameter is a method itself.
// A method takes JSON-RPC params and returns a result.
// It can also return a promise of the result.
server.addMethod("echo", ({ text }) => text);
server.addMethod("log", ({ message }) => console.log(message));
const app = express();
app.use(bodyParser.json());
app.post("/json-rpc", (req, res) => {
const jsonRPCRequest = req.body;
// server.receive takes a JSON-RPC request and returns a promise of a JSON-RPC response.
// It can also receive an array of requests, in which case it may return an array of responses.
// Alternatively, you can use server.receiveJSON, which takes JSON string as is (in this case req.body).
server.receive(jsonRPCRequest).then((jsonRPCResponse) => {
if (jsonRPCResponse) {
res.json(jsonRPCResponse);
} else {
// If response is absent, it was a JSON-RPC notification method.
// Respond with no content status (204).
res.sendStatus(204);
}
});
});
app.listen(80);
With authentication
To hook authentication into the API, inject custom params:
const server = new JSONRPCServer();
// The method can also take a custom parameter as the second parameter.
// Use this to inject whatever information that method needs outside the regular JSON-RPC request.
server.addMethod("echo", ({ text }, { userID }) => `${userID} said ${text}`);
app.post("/json-rpc", (req, res) => {
const jsonRPCRequest = req.body;
const userID = getUserID(req);
// server.receive takes an optional second parameter.
// The parameter will be injected to the JSON-RPC method as the second parameter.
server.receive(jsonRPCRequest, { userID }).then((jsonRPCResponse) => {
if (jsonRPCResponse) {
res.json(jsonRPCResponse);
} else {
res.sendStatus(204);
}
});
});
const getUserID = (req) => {
// Do whatever to get user ID out of the request
};
Middleware
Use middleware to intercept request and response:
const server = new JSONRPCServer();
// next will call the next middleware
const logMiddleware = (next, request, serverParams) => {
console.log(`Received ${JSON.stringify(request)}`);
return next(request, serverParams).then((response) => {
console.log(`Responding ${JSON.stringify(response)}`);
return response;
});
};
const exceptionMiddleware = async (next, request, serverParams) => {
try {
return await next(request, serverParams);
} catch (error) {
if (error.code) {
return createJSONRPCErrorResponse(request.id, error.code, error.message);
} else {
throw error;
}
}
};
// Middleware will be called in the same order they are applied
server.applyMiddleware(logMiddleware, exceptionMiddleware);
Constructor Options
Optionally, you can pass options to JSONRPCServer
constructor:
new JSONRPCServer({
errorListener: (message: string, data: unknown): void => {
// Listen to error here. By default, it will use console.warn to log errors.
},
});
Client
import { JSONRPCClient } from "json-rpc-2.0";
// JSONRPCClient needs to know how to send a JSON-RPC request.
// Tell it by passing a function to its constructor. The function must take a JSON-RPC request and send it.
const client = new JSONRPCClient((jsonRPCRequest) =>
fetch("http://localhost/json-rpc", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(jsonRPCRequest),
}).then((response) => {
if (response.status === 200) {
// Use client.receive when you received a JSON-RPC response.
return response
.json()
.then((jsonRPCResponse) => client.receive(jsonRPCResponse));
} else if (jsonRPCRequest.id !== undefined) {
return Promise.reject(new Error(response.statusText));
}
})
);
// Use client.request to make a JSON-RPC request call.
// The function returns a promise of the result.
client
.request("echo", { text: "Hello, World!" })
.then((result) => console.log(result));
// Use client.notify to make a JSON-RPC notification call.
// By definition, JSON-RPC notification does not respond.
client.notify("log", { message: "Hello, World!" });
With authentication
Just like JSONRPCServer
, you can inject custom params to JSONRPCClient
too:
const client = new JSONRPCClient(
// It can also take a custom parameter as the second parameter.
(jsonRPCRequest, { token }) =>
fetch("http://localhost/json-rpc", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${token}`, // Use the passed token
},
body: JSON.stringify(jsonRPCRequest),
}).then((response) => {
// ...
})
);
// Pass the custom params as the third argument.
client.request("echo", { text: "Hello, World!" }, { token: "foo's token" });
client.notify("log", { message: "Hello, World!" }, { token: "foo's token" });
With timeout
Sometimes you don't want to wait for the response indefinitely. You can use timeout
to automatically fail the request after certain delay:
const client = new JSONRPCClient(/* ... */);
client
.timeout(10 * 1000) // Automatically fails if it didn't get a response within 10 sec
.request("echo", { text: "Hello, World!" });
// Create a custom error response
const createTimeoutJSONRPCErrorResponse = (
id: JSONRPCID
): JSONRPCErrorResponse =>
createJSONRPCErrorResponse(id, 123, "Custom error message");
client
.timeout(10 * 1000, createTimeoutJSONRPCErrorResponse)
.request("echo", { text: "Hello, World!" });
Bi-directional JSON-RPC
For bi-directional JSON-RPC, use JSONRPCServerAndClient
.
const webSocket = new WebSocket("ws://localhost");
const serverAndClient = new JSONRPCServerAndClient(
new JSONRPCServer(),
new JSONRPCClient((request) => {
try {
webSocket.send(JSON.stringify(request));
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
})
);
webSocket.onmessage = (event) => {
serverAndClient.receiveAndSend(JSON.parse(event.data.toString()));
};
// On close, make sure to reject all the pending requests to prevent hanging.
webSocket.onclose = (event) => {
serverAndClient.rejectAllPendingRequests(
`Connection is closed (${event.reason}).`
);
};
serverAndClient.addMethod("echo", ({ text }) => text);
serverAndClient
.request("add", { x: 1, y: 2 })
.then((result) => console.log(`1 + 2 = ${result}`));
Constructor Options
Optionally, you can pass options to JSONRPCServerAndClient
constructor:
new JSONRPCServerAndClient(server, client, {
errorListener: (message: string, data: unknown): void => {
// Listen to error here. By default, it will use console.warn to log errors.
},
});
Error handling
To respond an error, reject with an Error
. On the client side, the promise will be rejected with an Error
object with the same message.
server.addMethod("fail", () =>
Promise.reject(new Error("This is an error message."))
);
client.request("fail").then(
() => console.log("This does not get called"),
(error) => console.error(error.message) // Outputs "This is an error message."
);
If you want to return a custom error response, use JSONRPCErrorException
:
import { JSONRPCErrorException } from "json-rpc-2.0";
const server = new JSONRPCServer();
server.addMethod("throws", () => {
const errorCode = 123;
const errorData = {
foo: "bar",
};
throw new JSONRPCErrorException(
"A human readable error message",
errorCode,
errorData
);
});
Alternatively, use advanced APIs or implement mapErrorToJSONRPCErrorResponse
:
import {
createJSONRPCErrorResponse,
JSONRPCErrorResponse,
JSONRPCID,
JSONRPCServer,
} from "json-rpc-2.0";
const server = new JSONRPCServer();
server.mapErrorToJSONRPCErrorResponse = (
id: JSONRPCID,
error: any
): JSONRPCErrorResponse => {
return createJSONRPCErrorResponse(
id,
error?.code || 0,
error?.message || "An unexpected error occurred",
// Optional 4th argument. It maps to error.data of the response.
{ foo: "bar" }
);
};
Advanced APIs
Use the advanced APIs to handle raw JSON-RPC messages.
Server
import { JSONRPC, JSONRPCResponse, JSONRPCServer } from "json-rpc-2.0";
const server = new JSONRPCServer();
// Advanced method takes a raw JSON-RPC request and returns a raw JSON-RPC response
server.addMethodAdvanced(
"doSomething",
(jsonRPCRequest: JSONRPCRequest): PromiseLike<JSONRPCResponse> => {
if (isValid(jsonRPCRequest.params)) {
return {
jsonrpc: JSONRPC,
id: jsonRPCRequest.id,
result: "Params are valid",
};
} else {
return {
jsonrpc: JSONRPC,
id: jsonRPCRequest.id,
error: {
code: -100,
message: "Params are invalid",
data: jsonRPCRequest.params,
},
};
}
}
);
// Remove the added method
server.removeMethod("doSomething");
Client
import {
JSONRPC,
JSONRPCClient,
JSONRPCRequest,
JSONRPCResponse,
} from "json-rpc-2.0";
const send = () => {
// ...
};
let nextID: number = 0;
const createID = () => nextID++;
// To avoid conflict ID between basic and advanced method request, inject a custom ID factory function.
const client = new JSONRPCClient(send, createID);
const jsonRPCRequest: JSONRPCRequest = {
jsonrpc: JSONRPC,
id: createID(),
method: "doSomething",
params: {
foo: "foo",
bar: "bar",
},
};
// Advanced method takes a raw JSON-RPC request and returns a raw JSON-RPC response
// It can also send an array of requests, in which case it returns an array of responses.
client
.requestAdvanced(jsonRPCRequest)
.then((jsonRPCResponse: JSONRPCResponse) => {
if (jsonRPCResponse.error) {
console.log(
`Received an error with code ${jsonRPCResponse.error.code} and message ${jsonRPCResponse.error.message}`
);
} else {
doSomethingWithResult(jsonRPCResponse.result);
}
});
Typed client and server
To strongly type request
and addMethod
methods, use TypedJSONRPCClient
, TypedJSONRPCServer
and TypedJSONRPCServerAndClient
interfaces.
import {
JSONRPCClient,
JSONRPCServer,
JSONRPCServerAndClient,
TypedJSONRPCClient,
TypedJSONRPCServer,
TypedJSONRPCServerAndClient,
} from "json-rpc-2.0";
type Methods = {
echo(params: { message: string }): string;
sum(params: { x: number; y: number }): number;
};
const server: TypedJSONRPCServer<Methods> = new JSONRPCServer(/* ... */);
const client: TypedJSONRPCClient<Methods> = new JSONRPCClient(/* ... */);
// Types are infered from the Methods type
server.addMethod("echo", ({ message }) => message);
server.addMethod("sum", ({ x, y }) => x + y);
// These result in type error
// server.addMethod("ech0", ({ message }) => message); // typo in method name
// server.addMethod("echo", ({ messagE }) => messagE); // typo in param name
// server.addMethod("echo", ({ message }) => 123); // return type must be string
client
.request("echo", { message: "hello" })
.then((result) => console.log(result));
client.request("sum", { x: 1, y: 2 }).then((result) => console.log(result));
// These result in type error
// client.request("ech0", { message: "hello" }); // typo in method name
// client.request("echo", { messagE: "hello" }); // typo in param name
// client.request("echo", { message: 123 }); // message param must be string
// client
// .request("echo", { message: "hello" })
// .then((result: number) => console.log(result)); // return type must be string
// The same rule applies to TypedJSONRPCServerAndClient
type ServerAMethods = {
echo(params: { message: string }): string;
};
type ServerBMethods = {
sum(params: { x: number; y: number }): number;
};
const serverAndClientA: TypedJSONRPCServerAndClient<
ServerAMethods,
ServerBMethods
> = new JSONRPCServerAndClient(/* ... */);
const serverAndClientB: TypedJSONRPCServerAndClient<
ServerBMethods,
ServerAMethods
> = new JSONRPCServerAndClient(/* ... */);
serverAndClientA.addMethod("echo", ({ message }) => message);
serverAndClientB.addMethod("sum", ({ x, y }) => x + y);
serverAndClientA
.request("sum", { x: 1, y: 2 })
.then((result) => console.log(result));
serverAndClientB
.request("echo", { message: "hello" })
.then((result) => console.log(result));
Build
npm run build
Test
npm test