Giter Site home page Giter Site logo

shogowada / json-rpc-2.0 Goto Github PK

View Code? Open in Web Editor NEW
178.0 5.0 24.0 197 KB

Let your client and server talk over function calls under JSON-RPC 2.0 spec. Strongly typed. No external dependencies.

Home Page: https://www.npmjs.com/package/json-rpc-2.0

License: MIT License

TypeScript 100.00%

json-rpc-2.0's Introduction

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
  • No external dependencies
    • Keep your package small
    • Stay away from dependency hell
  • Works in both browser and Node.js
  • First-class TypeScript support

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,
        },
      };
    }
  }
);
// You can remove the added method if needed
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

json-rpc-2.0's People

Contributors

redi-wadash avatar shogowada avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

json-rpc-2.0's Issues

JSONRPCParams type is incomplete

The JSONRPCParams type definition does not allow all valid JSON values. The current type definition is as follows:

export type JSONRPCParams = object | any[];

Source: https://github.com/shogowada/json-rpc-2.0/blob/v1.0.0/src/models.ts#L5

The following are other valid JSON values that should be allowed by JSONRPCParams:

Number:

{ "params": 123 }

String:

{ "params": "foo" }

Boolean:

{ "params": true }

See JSON spec.

One suggestion is for the JSONRPCParams type definition to look something like this:

export type JSONRPCParams = object | any[] | number | string | boolean;

models.ts isJSONRPCResponse logic hole intentional?

The following logic allows a variety of badly formatted JSON string to be reported back as valid JSON-RPC response.

export const isJSONRPCResponse = (payload: any): payload is JSONRPCResponse => {
return !isJSONRPCRequest(payload);
};

Also even if I patched it up to confirm that payload is neither Response nor Request, receiveAndSend() does not reject. Not sure if this was intentional but it seems it would be good to be able to use a .catch with receiveAndSend() when payload is neither Response nor Request?

With authentication documentation section seems to be outdated?

Code provided in documentation wont compile because of "Property 'token' does not exist on type 'void'.ts(2339)"

const client1 = 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) => {
      // ...
    })
);```

How should I provide custom parameters?

feat: Iterable RPC calls.

Would you be interested in a PR for adding iterable RPC methods?

I have just been calling them tasks (Perhaps there is a better name?) but they are just build on top of the existing API and therefore I can see that it is questionable if it should be a part of this library or not.

This would look like the following:

rpc.addTask("async-task", () => {
	return (async function* () {
		for (const value of [1, 2, 3, 4]) {
			yield value;
			await new Promise(resolve => setTimeout(resolve, 500));
		}
	})();
});

for await (const value of rpc.runTask("async-task", {})) {
	console.log(value);
}

Invalid JSON RPC request doesn't return expected result

If you send the following request:
{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]

according to the JSON RPC spec, it should return:
{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}

However, it returns the following:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected token } in JSON at position 95<br> &nbsp; &nbsp;at JSON.parse (&lt;anonymous&gt;)<br> &nbsp; &nbsp;at parse (K:\Development\TypeScript\InlineTypeGuard\node_modules\body-parser\lib\types\json.js:89:19)<br> &nbsp; &nbsp;at K:\Development\TypeScript\InlineTypeGuard\node_modules\body-parser\lib\read.js:128:18<br> &nbsp; &nbsp;at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> &nbsp; &nbsp;at invokeCallback (K:\Development\TypeScript\InlineTypeGuard\node_modules\raw-body\index.js:231:16)<br> &nbsp; &nbsp;at done (K:\Development\TypeScript\InlineTypeGuard\node_modules\raw-body\index.js:220:7)<br> &nbsp; &nbsp;at IncomingMessage.onEnd (K:\Development\TypeScript\InlineTypeGuard\node_modules\raw-body\index.js:280:7)<br> &nbsp; &nbsp;at IncomingMessage.emit (node:events:513:28)<br> &nbsp; &nbsp;at IncomingMessage.emit (node:domain:489:12)<br> &nbsp; &nbsp;at endReadableNT (node:internal/streams/readable:1359:12)</pre>
</body>
</html>

See https://www.jsonrpc.org/specification#request_object for the "rpc call with invalid JSON" example

Client doesn't not support synchronous callback

I got the following exception with this. This is reproducible with android chrome webview and not with ios WkWebView.

caught (in promise) TypeError: Cannot read properties of undefined (reading 'then')
    at push../node_modules/json-rpc-2.0/dist/client.js.JSONRPCClient.requestAdvanced (client.js:145:1)
    at JSONRPCClient.<anonymous> (client.js:108:1)
    at step (client.js:33:1)
    at Object.next (client.js:14:46)
    at client.js:8:1
    at new Promise (<anonymous>)
    at push../node_modules/json-rpc-2.0/dist/client.js.__awaiter (client.js:4:1)
    at push../node_modules/json-rpc-2.0/dist/client.js.JSONRPCClient.requestWithID (client.js:102:1)
    at push../node_modules/json-rpc-2.0/dist/client.js.JSONRPCClient.request (client.js:99:1)

image

it's impossible to throw an error from server with custom error code

Hello!
Currently if a server method throws an exception this library will respond with object:

error: {
  code: 0,
  message: 'Some Error' // This is taken from error object
}

I would like to customize error code instead of constantly having it equaled to zero to let clients distinguish different kinds of errors: authorization errors, internal server errors and etc.

Add optional caching?

Over in rpc-caching-server we have built (and continue to build) a service that's meant to be used to cache expensive RPCs made to Solana clusters. We make heavy use of this repository inside of that package.

I was reviewing some of the code of our project today and it occurred to me that a good chunk of our custom code wouldn't have to exist if caching was implemented in some way in this package. Given that we're certainly not the only people who have to make expensive RPCs and need to be able to cache the results, I think it would make sense to implement inside of this package as an option developers can turn on.

What do you think @shogowada? I would be happy to take on this task.

error response data not available

Sometimes a JSON-RPC-2.0 server attaches data to an error response. The data is lost after the JSON is run through Client.receiveAndSend. It would be good to have that data be attached to the error object.

feat: Extensions

Following on from #47, do you think it be a good idea to add extensions to the classes that would provided a way to handle third party extensions that could be added as a configuration option?

const server = new JSONRPCServer({
  extensions: {
    iterable: iterableExt(),
    cancelable: cancelableExt()
  }
});

// Use an extension.
server.ext.iterable.add(/* ... */);

This way anyone can make use of official and private extensions that follow the JSON-RPC 2.0 Extension specification.

Not returning response if request is invalid

If request is invalid (not object, hasn't required fields, etc.) library just logs it to console and rejects promise:
https://github.com/shogowada/json-rpc-2.0/blob/master/src/server.ts#L94-L97

So user should handle this error by himself, i.e. compose -32700 error response, and return it.

It is better to return such response from the library.

This library: https://www.npmjs.com/package/simple-jsonrpc-js has more accurate JSON-RPC 2.0 implementation, and it includes such logic.

idToResolveMap's key value can be number but the request's id type is string.

In this case, it can't find the resolve function.

responses.forEach((response) => {
      const resolve = this.idToResolveMap.get(response.id); <--- r**esponse.id is string but the idToResolveMap key value is number so it goes to else case. And then the request never return promise**
      if (resolve) {
        this.idToResolveMap.delete(response.id);
        resolve(response);
      }   
    });

Can you please check this bug please?

Thank you

client receive function issue?

I'm pretty new to Javascript and Web development in general, currently learning to code with React and tried your project to run JSON-RPC 2.0 over websocket. Followed your JSONRPCServerAndClient example, but noticed that in code like this

   rpcServerClient.request("echo2", {"Hello": "hello"}).then(
    result => console.log("This function doesn't run for some reason"),
    error => console.error(`bad, error message = ${error.message}`)

line "result => console.log("This function doesn't run for some reason")," does not run.
BTW the React app is running in Safari browser. I don't know Typescript. I dug through the distributed javascript and patched the client.receive function like the following and it worked. Basically the response is coming in as a string so it had to be parsed into object first:

JSONRPCClient.prototype.receive = function (response) {
    var parsed = JSON.parse(response);
    var resolve = this.idToResolveMap.get(parsed.id);
    if (resolve) {
        this.idToResolveMap.delete(parsed.id);
        resolve(parsed);
    }
};
return JSONRPCClient; 

JSONRPCParams typing issue

export type JSONRPCParams = object | any[];

Should probably be:

export type JSONRPCParams = any | any[];

Or just:

export type JSONRPCParams = any;

Or you will get errors from TypeScript while trying to destructure params or to type it as object is akin to {}, an object with no known props.

JSONRPCErrorException: Request failed with status code 404

JSONRPCErrorException: Request failed with status code 404

// server.js
import { JSONRPCServer } from 'json-rpc-2.0'
import Koa from 'koa'
import Router from 'koa-router'
import serve from 'koa-static'
import bodyParser from 'koa-bodyparser'

const app = new Koa()
const router = new Router()
const server = new JSONRPCServer()



server.addMethod('echo', text => {
  console.log('text::: ', text)
  return text
})

server.addMethod('log', ({ message }) => {
  console.log('message::: ', message)
})

router.post('/json-rpc', ctx => {
  /**@type {any}*/
  const body = ctx.request.body
  console.log('ctx.request.body::: ', body)

  server
    .receive(body)
    .then(response => {
      console.log('response::: ', response)
      if (response) {
        ctx.status = 200
        ctx.body = response
      } else {
        ctx.status = 204
      }
    })
    // @ts-ignore
    .catch(err => {
      console.log(err)
    })
})
// Comment out this line and it will cause an error.
// app.use(serve('./public'))
app.use(bodyParser())
app.use(async (ctx, next) => {
  await next()
})
app.use(router.routes())
app.listen(5000)

console.log(`listen:: http://127.0.0.1:5000`)
//client.js
import { JSONRPCClient } from 'json-rpc-2.0'

import axios from 'axios'

// @ts-ignore
const client = new JSONRPCClient(options => {
  console.log('JSONRPCClient options::: ', options)
  const request = axios({
    baseURL: 'http://127.0.0.1:5000',
    url: '/json-rpc',
    method: 'POST',
    headers: {
      'content-type': 'application/json'
    },
    data: options
  }).then(response => {
    if (response.status === 200) {
      client.receive(response.data)
    } else if (options.id !== undefined) {
      return Promise.reject(new Error(response.statusText))
    }
  })

  return request
})

client
  .request('echo', { text: 'Hello, World!' })
  .then(result => {
    console.log('result  2:', result)
  })
  // @ts-ignore
  .catch(err => {
    console.log(err)
  })

client.notify('log', { message: 'Hello, World!' })
# server output
ctx.request.body:::  {
  jsonrpc: '2.0',
  id: 1,
  method: 'echo',
  params: { text: 'Hello, World!' }
}
text:::  { text: 'Hello, World!' }
response:::  { jsonrpc: '2.0', id: 1, result: { text: 'Hello, World!' } }
# client output
JSONRPCClient options:::  {
  jsonrpc: '2.0',
  id: 1,
  method: 'echo',
  params: { text: 'Hello, World!' }
}
JSONRPCErrorException: Request failed with status code 404
    at new JSONRPCErrorException (E:\code\node\koa-demo\node_modules\json-rpc-2.0\dist\models.js:55:28)
    at JSONRPCClient.<anonymous> (E:\code\node\koa-demo\node_modules\json-rpc-2.0\dist\client.js:115:66)
    at step (E:\code\node\koa-demo\node_modules\json-rpc-2.0\dist\client.js:33:23)
    at Object.next (E:\code\node\koa-demo\node_modules\json-rpc-2.0\dist\client.js:14:53)
    at fulfilled (E:\code\node\koa-demo\node_modules\json-rpc-2.0\dist\client.js:5:58)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  code: 0,
  data: undefined
}

Please help me ,What is the reason? thank you.

Method callback not working

Using 1.5.1 and 1.4.1 with this code :

import fs from 'fs'
import http from 'http'
import https from 'https'
import path from 'path'

import cors from 'cors'
import express from 'express'
import bodyParser from 'body-parser'

import {JSONRPCServer } from 'json-rpc-2.0'

export interface ModuleRpcRequest {

endpointName: string
moduleName: string
params: any

}

export interface ModuleRpcEndpoint {

name: string
route: string

}

let PORT = process.env.PORT ? process.env.PORT : 8000

export default class WebServer {
server: https.Server | http.Server | undefined

app:any

appListener: any

constructor( public serverConfig: any ) {

this.app = express()
this.app.use(bodyParser.json());

if(serverConfig.port){
  PORT = serverConfig.port
} 

}

async start( ): Promise {

const jserver = new JSONRPCServer();
jserver.addMethod("echo", ( ) => {
  console.log('echo')
  return 'echo'
});

this.app.post("/", (req:any, res:any) => {
    
const jsonRPCRequest = req.body;
console.log('got post',jsonRPCRequest)
// 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).
jserver.receive(jsonRPCRequest).then((jsonRPCResponse:any) => {

  console.log('meep', jsonRPCResponse)
  if (jsonRPCResponse) {
    console.log('returning ', jsonRPCResponse)
    res.json(jsonRPCResponse);
  } else {
    console.log('no response ', jsonRPCResponse)
    // If response is absent, it was a JSON-RPC notification method.
    // Respond with no content status (204).
    res.sendStatus(204);
  }
});

});

  this.appListener = this.app.listen(PORT);

  console.log(`Backend Server listening on port ${PORT} using http`)

}

async stop( ){
if(this.appListener){
this.appListener.close()
}

}
}

When i hit it with this POST :

{
"jsonrpc": "2.0",
"method": "echo",
"params": [ "dogs" ]

}

i get this :

Backend Server listening on port 6102 using http
got post { jsonrpc: '2.0', method: 'echo', params: [ 'dogs' ] }
echo
meep null
no response null

which makes absolutely no sense because the method 'echo' is running (its printing echo) but it wont return into the .then and into the jsonRPCResponse at all. that is null.. HOW

Question: mapErrorToJSONRPCErrorResponse vs exception middleware

From the README, it is not clear to me what the purpose is of mapErrorToJSONRPCErrorResponse. When should I register a mapErrorToJSONRPCErrorResponse function instead of creating a middleware?

mapErrorToJSONRPCErrorResponse

server.mapErrorToJSONRPCErrorResponse = (
  id: JSONRPCID,
  error: any
): JSONRPCErrorResponse => {
  return createJSONRPCErrorResponse(
    id,
    error?.code || 0,
    error?.message || 'An unexpected error occurred',
  );
};

middleware

const exceptionMiddleware = async (next, request, serverParams) => {
  try {
    return await next(request, serverParams);
  } catch (error) {
    return createJSONRPCErrorResponse(
      request.id!, 
      error?.code || 0, 
      error?.message || 'An unexpected error occurred'
    );
  }
};

server.applyMiddleware(exceptionMiddleware);

[Enhancement] Timeout when waiting for a reply

Hi,
Thank you for the library! I've been tinkering around a bit and I've successfully implemented it in my backend. (First time using node.js, yay!)

I'm now testing some stress scenario where the devices cannot reply in a reasonable amount of time or the request gets lost (maybe the MCU was busing doing something else and needs a heads up). It would be great if some kind of timeout for reply could be implemented.

Current use:

1. Connect WS 
2. Send RPC Request to get Device Status
3. await the reply
4. Wait 5 seconds
5. Go back to point 2 and request Dev Status

If the device never replies the await will never resolve and the process breaks.

Example with timeout specified:

client
  .request("echo", { text: "Hello, World!" }, 2000) // <--- here's the timeout
  .then((result) => console.log(result));

This will also remove the request from the idToResolveMap once the timeout has occurred.

Pseudo code implementation (first time using Typescript, there is be a better way for sure for doing this, please don't be harsh ๐Ÿ˜„ )

requestAdvanced(
  request: JSONRPCRequest,
  clientParams?: ClientParams
): PromiseLike<JSONRPCResponse> {
  const promise: PromiseLike<JSONRPCResponse> = new Promise((resolve) =>
    this.idToResolveMap.set(request.id!, resolve)
  );
  // TIMEOUT STUFF - Handle the timeout 
  timeoutTimer = setTimeout(function() { 
    // Remove the request from the map
    this.idToResolveMap.delete(request.id!);
    // Throw an exception (?) in some way to let the caller know timeout has occurred
  }, 5000);
  return this.send(request, clientParams).then(
    () => promise,
    // TIMEOUT STUFF - (somewhere here) When a reply is received, clear the timeout
    clearTimeout(timeoutTimer)
    (error) => {
      this.receive(
        createJSONRPCErrorResponse(
          request.id!,
          0,
          (error && error.message) || "Failed to send a request"
        )
      );
      return promise;
    }
  );
}

I would love to make the PR for this feature myself but I'm still getting a grasp on how typescript works.

Incorrect documentation

Hi, @shogowada
I read the documentation and I have missunderstood the section "With authentication".

Your code example is:

const server = new JSONRPCServer();

// If the method is a higher-order function (a function that returns a function),
// it will pass the custom parameter to the returned function.
// 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 if it was a higher-order function.
  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

But according TS definitions I see that server.addMethod takes a value of type:
export declare type SimpleJSONRPCMethod = (params?: Partial<JSONRPCParams>) => any;

Did you mean that we need to use addMethodAdvanced instead? I see it has the correct type definition.

feature request: strict protocol types

Background

I recently encountered an issue while using the json_rpc_2 package in Dart for communication. Upon integration, the package rejected some requests and responses due to a specification that only allows parameter types of array or object.

Initially, I implemented the transmission part using this package. However, I found it necessary to modify the existing implementation. Consequently, I propose changing the JSONRPCParams type to Record<string, unknown> | unknown[].

Steps to Reproduce

  1. Setup project with the following commands.
pnpm init
pnpm i json-rpc-2.0
code .
  1. Copy and paste the following code into vscode:
import { JSONRPCClient, JSONRPCRequest, JSONRPCResponse, JSONRPCServer } from 'json-rpc-2.0'

new JSONRPCServer().addMethod('add', (url: string) => {});
new JSONRPCClient().request('remove', 'this');

Expected Behavior

Lines 3 and 4 should not pass type checking.

Actual Behavior

Lines 3 and 4 pass type checking because the parameter type is any.

How to remove method added to server?

I'm finding a way to remove the method added by server.addMethod(...), but it does not seem to exist.

I can recreate server and add methods from start, but I doubt I should do that.

Is there any way or will one be added? It would be nice to see this added like const removeMethod = server.addMethod(...)

Middleware can't intercept errors in case of async method's function

Here is an example of the issue:

const {
  JSONRPCServer, createJSONRPCErrorResponse, JSONRPCErrorCode, JSONRPC,
} = require('json-rpc-2.0');

const server = new JSONRPCServer();
server.applyMiddleware(async (next, request, serverParams) => {
  try {
    console.log('MIDDLEWARE');
    const res = await next(request, serverParams);
    console.log('MIDDLEWARE RESULT', res);
    return res;
  } catch (error) {
    console.log('MIDDLEWARE EXCEPT', error);
    return createJSONRPCErrorResponse(
      request.id,
      error.code || JSONRPCErrorCode.InternalError,
      error.message,
    );
  }
});

server.addMethod('myMethod', async () => {
  throw new Error('TEST ERROR');
});

const main = async () => {
  const result = await server
    .receive({
      jsonrpc: JSONRPC,
      id: 0,
      method: 'myMethod',
      params: {},
    });

  console.log('RESULT', result);
};

main();

The output is:

MIDDLEWARE
JSON-RPC method myMethod responded an error Error: TEST ERROR
   ... <stacktrace> ...
MIDDLEWARE RESULT { jsonrpc: '2.0', id: 0, error: { code: 0, message: 'TEST ERROR' } }
RESULT { jsonrpc: '2.0', id: 0, error: { code: 0, message: 'TEST ERROR' } }

Moreover, in such case the error is posted in console with console.warn which makes it impossible to control logging of errors by lib's user.

extended example

Hi can you provide more extended describe how to use your library with curl queries and result response examples

Cannot catch "Method not found" errors.

It would be handy to be able to intercept Method not found errors, I have tried the following and they are not triggered when calling a method that is not defined:

  • Defining errorListener in the server constructor.
  • Adding middleware.

For context what I want to achieve from this is to have a listener that catches all methods that are not defined using addMethod - perhaps a setDefaultMethod or similar could be added to handle this case?

Compatibility with React Native / other backend languages?

First off, awesome library!

  • Documentation ๐Ÿ’ฏ
  • Examples ๐Ÿ’ฏ
  • All TypeScript ๐Ÿ’ฏ ๐Ÿ’ฏ ๐Ÿ’ฏ

So, on to my question, do you foresee anything in the implementation that would prevent this from working in React Native? It works on the web, so I would assume so. If you don't know I could just give it a try and report back ๐Ÿ˜„

In terms of other backend languages, I suppose the client should also still work for backend frameworks other than Node (ex. .NET or Go) as long as we write a JSON RPC 2.0 compatible handler?

Thanks!

Allow generic typing of server & client methods

It would be nice to be able to specify the types of server & client methods, to make sure that you call them correctly and haven't forgotten to implement any. Based on post-me, I tried the following:

import { JSONRPCClient } from 'json-rpc-2.0';

type InnerType<T extends any | Promise<any>> = T extends Promise<
  infer U
>
  ? U
  : T;

type MethodsType = Record<string, (...args: any) => any>;

class MyJSONRPCClient<Methods extends MethodsType = any, ClientParams = void> extends JSONRPCClient<ClientParams> {
  request<K extends Extract<keyof Methods, string>>(
    method: K,
    params: Parameters<Methods[K]>,
    clientParams?: ClientParams
  ): PromiseLike<InnerType<ReturnType<Methods[K]>>> {
    return super.request(method, params, clientParams);
  }
}

type Foo = {
  foo(bla: number): number;
  bar(arg: string): string;
};

const x: MyJSONRPCClient<Foo>;
x.request('foo', [123]);

const y: MyJSONRPCClient;
y.request('bla', []);

Which works, except that params is now required, making it conditionally optional, is kind of difficult, I think it requires some hack with rest parameters and conditional types.

It would be nice to have this as a builtin feature of the library for both the server and client. Of course, this might be a breaking change, due to generic parameters order and should we not be able to make the default case type check as of today. Also we need to support an object as params, which might require more typescript trickery, such as an overload or more conditional types.

feature: Encapsulating APIs using Proxy

Hello! Thank you for creating this library!

I have some ideas, I don't know if it will be merged after I pull requests.

I want to use proxy to encapsulate the existing API so that users can call it as if they were really calling an ordinary method, like:

const client = makeProxy<{
subtract: (a: number, b: number) => number;
}>(new JSONRPCClient(...));

await client.subtract(2, 2);

I wonder what you think of my idea?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.