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

Commit

Permalink
Handle JSON-RPC notifications (#104)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rekmarks authored Aug 15, 2022
1 parent 915920b commit 040a6ba
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 38 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A tool for processing JSON-RPC requests and responses.
```js
const { JsonRpcEngine } = require('json-rpc-engine');

let engine = new JsonRpcEngine();
const engine = new JsonRpcEngine();
```

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

```js
let request = { id: 1, jsonrpc: '2.0', method: 'hello' };
const request = { id: 1, jsonrpc: '2.0', method: 'hello' };

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

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:

```js
const engine = new JsonRpcEngine({ notificationHandler });

// A notification is defined as a JSON-RPC request without an `id` property.
const notification = { jsonrpc: '2.0', method: 'hello' };

const response = await engine.handle(notification);
console.log(typeof response); // 'undefined'
```

Engines can be nested by converting them to middleware using `JsonRpcEngine.asMiddleware()`:

```js
Expand Down
99 changes: 94 additions & 5 deletions src/JsonRpcEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ const jsonrpc = '2.0' as const;

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

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

it('handle: returns error for invalid request method', async () => {
const engine = new JsonRpcEngine();
let response: any = await engine.handle({ method: null } as any);
const response: any = await engine.handle({ id: 1, method: null } as any);

expect(response.error.code).toStrictEqual(-32600);
expect(response.result).toBeUndefined();
});

it('handle: returns error for invalid request method with nullish id', async () => {
const engine = new JsonRpcEngine();
const response: any = await engine.handle({
id: undefined,
method: null,
} as any);

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

it('handle: returns undefined for malformed notifications', async () => {
const middleware = jest.fn();
const notificationHandler = jest.fn();
const engine = new JsonRpcEngine({ notificationHandler });
engine.push(middleware);

expect(
await engine.handle({ jsonrpc, method: true } as any),
).toBeUndefined();
expect(notificationHandler).not.toHaveBeenCalled();
expect(middleware).not.toHaveBeenCalled();
});

it('handle: treats notifications as requests when no notification handler is specified', async () => {
const middleware = jest.fn().mockImplementation((_req, res, _next, end) => {
res.result = 'bar';
end();
});
const engine = new JsonRpcEngine();
engine.push(middleware);

expect(await engine.handle({ jsonrpc, method: 'foo' })).toStrictEqual({
jsonrpc,
result: 'bar',
id: undefined,
});
expect(middleware).toHaveBeenCalledTimes(1);
});

it('handle: forwards notifications to handlers', async () => {
const middleware = jest.fn();
const notificationHandler = jest.fn();
const engine = new JsonRpcEngine({ notificationHandler });
engine.push(middleware);

expect(await engine.handle({ jsonrpc, method: 'foo' })).toBeUndefined();
expect(notificationHandler).toHaveBeenCalledTimes(1);
expect(notificationHandler).toHaveBeenCalledWith({
jsonrpc,
method: 'foo',
});
expect(middleware).not.toHaveBeenCalled();
});

it('handle: re-throws errors from notification handlers (async)', async () => {
const notificationHandler = jest.fn().mockImplementation(() => {
throw new Error('baz');
});
const engine = new JsonRpcEngine({ notificationHandler });

await expect(engine.handle({ jsonrpc, method: 'foo' })).rejects.toThrow(
new Error('baz'),
);
expect(notificationHandler).toHaveBeenCalledTimes(1);
expect(notificationHandler).toHaveBeenCalledWith({
jsonrpc,
method: 'foo',
});
});

it('handle: re-throws errors from notification handlers (callback)', async () => {
const notificationHandler = jest.fn().mockImplementation(() => {
throw new Error('baz');
});
const engine = new JsonRpcEngine({ notificationHandler });

await new Promise<void>((resolve) => {
engine.handle({ jsonrpc, method: 'foo' }, (error, response) => {
expect(error).toStrictEqual(new Error('baz'));
expect(response).toBeUndefined();

expect(notificationHandler).toHaveBeenCalledTimes(1);
expect(notificationHandler).toHaveBeenCalledWith({
jsonrpc,
method: 'foo',
});
resolve();
});
});
});

it('handle: basic middleware test 1', async () => {
const engine = new JsonRpcEngine();

Expand Down
Loading

0 comments on commit 040a6ba

Please sign in to comment.