@metamask/json-rpc-engine
A tool for processing JSON-RPC requests and responses.
Installation
yarn add @metamask/json-rpc-engine
or
npm install @metamask/json-rpc-engine
Usage
const { JsonRpcEngine } = require('@metamask/json-rpc-engine');
const engine = new JsonRpcEngine();
Build a stack of JSON-RPC processors by pushing middleware to the engine.
engine.push(function (req, res, next, end) {
res.result = 42;
end();
});
Requests are handled asynchronously, stepping down the stack until complete.
const request = { id: 1, jsonrpc: '2.0', method: 'hello' };
engine.handle(request, function (err, response) {
});
const response = await engine.handle(request);
Middleware have direct access to the request and response objects.
They can let processing continue down the stack with next()
, or complete the request with end()
.
engine.push(function (req, res, next, end) {
if (req.skipCache) return next();
res.result = getResultFromCache(req);
end();
});
By passing a return handler to the next
function, you can get a peek at the result before it returns.
engine.push(function (req, res, next, end) {
next(function (cb) {
insertIntoCache(res, cb);
});
});
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:
const engine = new JsonRpcEngine({ notificationHandler });
const notification = { jsonrpc: '2.0', method: 'hello' };
const response = await engine.handle(notification);
console.log(typeof response);
Engines can be nested by converting them to middleware using JsonRpcEngine.asMiddleware()
:
const engine = new JsonRpcEngine();
const subengine = new JsonRpcEngine();
engine.push(subengine.asMiddleware());
async
Middleware
If you require your middleware function to be async
, use createAsyncMiddleware
:
const { createAsyncMiddleware } = require('@metamask/json-rpc-engine');
let engine = new RpcEngine();
engine.push(
createAsyncMiddleware(async (req, res, next) => {
res.result = 42;
next();
}),
);
async
middleware do not take an end
callback.
Instead, the request ends if the middleware returns without calling next()
:
engine.push(
createAsyncMiddleware(async (req, res, next) => {
res.result = 42;
}),
);
The next
callback of async
middleware also don't take return handlers.
Instead, you can await next()
.
When the execution of the middleware resumes, you can work with the response again.
engine.push(
createAsyncMiddleware(async (req, res, next) => {
res.result = 42;
await next();
addToMetrics(res);
}),
);
You can freely mix callback-based and async
middleware:
engine.push(function (req, res, next, end) {
if (!isCached(req)) {
return next((cb) => {
insertIntoCache(res, cb);
});
}
res.result = getResultFromCache(req);
end();
});
engine.push(
createAsyncMiddleware(async (req, res, next) => {
res.result = 42;
await next();
addToMetrics(res);
}),
);
Teardown
If your middleware has teardown to perform, you can assign a method destroy()
to your middleware function(s),
and calling JsonRpcEngine.destroy()
will call this method on each middleware that has it.
A destroyed engine can no longer be used.
const middleware = (req, res, next, end) => {
};
middleware.destroy = () => {
};
const engine = new JsonRpcEngine();
engine.push(middleware);
engine.destroy();
engine.handle(req);
Gotchas
Handle errors via end(err)
, NOT next(err)
.
engine.push(function (req, res, next, end) {
next(new Error());
});
engine.push(function (req, res, next, end) {
end(new Error());
});
However, next()
will detect errors on the response object, and cause
end(res.error)
to be called.
engine.push(function (req, res, next, end) {
res.error = new Error();
next();
});
Contributing
This package is part of a monorepo. Instructions for contributing can be found in the monorepo README.