Skip to content
This repository was archived by the owner on Nov 9, 2023. It is now read-only.

Commit 040a6ba

Browse files
authored
Handle JSON-RPC notifications (#104)
This PR adds [JSON-RPC 2.0](https://www.jsonrpc.org/specification)-compliant notification handling for `JsonRpcEngine`. - JSON-RPC notifications are defined as JSON-RPC request objects without an `id` property. - A new constructor parameter, `notificationHandler`, is introduced. This parameter is a function that accepts JSON-RPC notification objects and returns `void | Promise<void>`. - When `JsonRpcEngine.handle` is called, if a `notificationHandler` exists, any request objects duck-typed as notifications will be handled as such. This means that: - Validation errors that occur after duck-typing will be ignored. At the moment, this just means that no error will be thrown if the `method` field is not a string. - If basic validation succeeds, the notification object will be passed to the handler function without touching the middleware stack. - The response from `handle()` will be `undefined`. - No error will be returned or thrown, unless the notification handler itself throws or rejects. - Notification handlers should not throw or reject, and it is the implementer's responsibility to ensure that they do not. - If `JsonRpcEngine.handle` is called and no `notificationHandler` exists, notifications will be treated just like requests. This is the current behavior.
1 parent 915920b commit 040a6ba

File tree

3 files changed

+232
-38
lines changed

3 files changed

+232
-38
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A tool for processing JSON-RPC requests and responses.
77
```js
88
const { JsonRpcEngine } = require('json-rpc-engine');
99

10-
let engine = new JsonRpcEngine();
10+
const engine = new JsonRpcEngine();
1111
```
1212

1313
Build a stack of JSON-RPC processors by pushing middleware to the engine.
@@ -22,7 +22,7 @@ engine.push(function (req, res, next, end) {
2222
Requests are handled asynchronously, stepping down the stack until complete.
2323

2424
```js
25-
let request = { id: 1, jsonrpc: '2.0', method: 'hello' };
25+
const request = { id: 1, jsonrpc: '2.0', method: 'hello' };
2626

2727
engine.handle(request, function (err, response) {
2828
// Do something with response.result, or handle response.error
@@ -53,6 +53,18 @@ engine.push(function (req, res, next, end) {
5353
});
5454
```
5555

56+
If you specify a `notificationHandler` when constructing the engine, JSON-RPC notifications passed to `handle()` will be handed off directly to this function without touching the middleware stack:
57+
58+
```js
59+
const engine = new JsonRpcEngine({ notificationHandler });
60+
61+
// A notification is defined as a JSON-RPC request without an `id` property.
62+
const notification = { jsonrpc: '2.0', method: 'hello' };
63+
64+
const response = await engine.handle(notification);
65+
console.log(typeof response); // 'undefined'
66+
```
67+
5668
Engines can be nested by converting them to middleware using `JsonRpcEngine.asMiddleware()`:
5769

5870
```js

src/JsonRpcEngine.test.ts

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ const jsonrpc = '2.0' as const;
1111

1212
describe('JsonRpcEngine', () => {
1313
it('handle: throws on truthy, non-function callback', () => {
14-
const engine: any = new JsonRpcEngine();
15-
expect(() => engine.handle({}, true)).toThrow(
14+
const engine = new JsonRpcEngine();
15+
expect(() => engine.handle({} as any, 'foo' as any)).toThrow(
1616
'"callback" must be a function if provided.',
1717
);
1818
});
1919

20-
it('handle: returns error for invalid request parameter', async () => {
20+
it('handle: returns error for invalid request value', async () => {
2121
const engine = new JsonRpcEngine();
2222
let response: any = await engine.handle(null as any);
2323
expect(response.error.code).toStrictEqual(-32600);
@@ -30,15 +30,104 @@ describe('JsonRpcEngine', () => {
3030

3131
it('handle: returns error for invalid request method', async () => {
3232
const engine = new JsonRpcEngine();
33-
let response: any = await engine.handle({ method: null } as any);
33+
const response: any = await engine.handle({ id: 1, method: null } as any);
34+
3435
expect(response.error.code).toStrictEqual(-32600);
3536
expect(response.result).toBeUndefined();
37+
});
38+
39+
it('handle: returns error for invalid request method with nullish id', async () => {
40+
const engine = new JsonRpcEngine();
41+
const response: any = await engine.handle({
42+
id: undefined,
43+
method: null,
44+
} as any);
3645

37-
response = await engine.handle({ method: true } as any);
3846
expect(response.error.code).toStrictEqual(-32600);
3947
expect(response.result).toBeUndefined();
4048
});
4149

50+
it('handle: returns undefined for malformed notifications', async () => {
51+
const middleware = jest.fn();
52+
const notificationHandler = jest.fn();
53+
const engine = new JsonRpcEngine({ notificationHandler });
54+
engine.push(middleware);
55+
56+
expect(
57+
await engine.handle({ jsonrpc, method: true } as any),
58+
).toBeUndefined();
59+
expect(notificationHandler).not.toHaveBeenCalled();
60+
expect(middleware).not.toHaveBeenCalled();
61+
});
62+
63+
it('handle: treats notifications as requests when no notification handler is specified', async () => {
64+
const middleware = jest.fn().mockImplementation((_req, res, _next, end) => {
65+
res.result = 'bar';
66+
end();
67+
});
68+
const engine = new JsonRpcEngine();
69+
engine.push(middleware);
70+
71+
expect(await engine.handle({ jsonrpc, method: 'foo' })).toStrictEqual({
72+
jsonrpc,
73+
result: 'bar',
74+
id: undefined,
75+
});
76+
expect(middleware).toHaveBeenCalledTimes(1);
77+
});
78+
79+
it('handle: forwards notifications to handlers', async () => {
80+
const middleware = jest.fn();
81+
const notificationHandler = jest.fn();
82+
const engine = new JsonRpcEngine({ notificationHandler });
83+
engine.push(middleware);
84+
85+
expect(await engine.handle({ jsonrpc, method: 'foo' })).toBeUndefined();
86+
expect(notificationHandler).toHaveBeenCalledTimes(1);
87+
expect(notificationHandler).toHaveBeenCalledWith({
88+
jsonrpc,
89+
method: 'foo',
90+
});
91+
expect(middleware).not.toHaveBeenCalled();
92+
});
93+
94+
it('handle: re-throws errors from notification handlers (async)', async () => {
95+
const notificationHandler = jest.fn().mockImplementation(() => {
96+
throw new Error('baz');
97+
});
98+
const engine = new JsonRpcEngine({ notificationHandler });
99+
100+
await expect(engine.handle({ jsonrpc, method: 'foo' })).rejects.toThrow(
101+
new Error('baz'),
102+
);
103+
expect(notificationHandler).toHaveBeenCalledTimes(1);
104+
expect(notificationHandler).toHaveBeenCalledWith({
105+
jsonrpc,
106+
method: 'foo',
107+
});
108+
});
109+
110+
it('handle: re-throws errors from notification handlers (callback)', async () => {
111+
const notificationHandler = jest.fn().mockImplementation(() => {
112+
throw new Error('baz');
113+
});
114+
const engine = new JsonRpcEngine({ notificationHandler });
115+
116+
await new Promise<void>((resolve) => {
117+
engine.handle({ jsonrpc, method: 'foo' }, (error, response) => {
118+
expect(error).toStrictEqual(new Error('baz'));
119+
expect(response).toBeUndefined();
120+
121+
expect(notificationHandler).toHaveBeenCalledTimes(1);
122+
expect(notificationHandler).toHaveBeenCalledWith({
123+
jsonrpc,
124+
method: 'foo',
125+
});
126+
resolve();
127+
});
128+
});
129+
});
130+
42131
it('handle: basic middleware test 1', async () => {
43132
const engine = new JsonRpcEngine();
44133

0 commit comments

Comments
 (0)