From e0b077fea4445ac86399033731c319974a30a31f Mon Sep 17 00:00:00 2001 From: andri lim Date: Wed, 3 Jan 2024 20:06:53 +0700 Subject: [PATCH] Upgrade rpc router internals (#178) * Upgrade rpc router internals * use new chronos asyncraises * Fix style mismatch * Fix nim v2 compilation error * Addresing review * Remove unnecessary custom serializer and let the library do the work * fix error message * Update readme.md --- README.md | 103 +++--- json_rpc.nimble | 4 +- json_rpc/client.nim | 266 ++++++--------- json_rpc/clients/httpclient.nim | 30 +- json_rpc/clients/socketclient.nim | 24 +- json_rpc/clients/websocketclientimpl.nim | 20 +- json_rpc/jsonmarshal.nim | 242 -------------- json_rpc/private/client_handler_wrapper.nim | 109 +++++++ json_rpc/{ => private}/errors.nim | 7 + json_rpc/private/jrpc_conv.nim | 23 ++ json_rpc/private/jrpc_sys.nim | 306 ++++++++++++++++++ json_rpc/private/server_handler_wrapper.nim | 301 +++++++++++++++++ json_rpc/private/shared_wrapper.nim | 70 ++++ json_rpc/router.nim | 269 ++++++++------- json_rpc/rpcproxy.nim | 19 +- json_rpc/server.nim | 47 ++- json_rpc/servers/httpserver.nim | 63 ++-- json_rpc/servers/socketserver.nim | 40 +-- tests/all.nim | 14 +- tests/helpers.nim | 11 - tests/{ => private}/ethcallsigs.nim | 9 - tests/{ => private}/ethhexstrings.nim | 45 +-- tests/{ => private}/ethprocs.nim | 30 +- tests/{ => private}/ethtypes.nim | 0 .../private/file_callsigs.nim | 6 +- tests/private/helpers.nim | 19 ++ tests/{ => private}/stintjson.nim | 20 +- tests/test_callsigs.nim | 26 ++ tests/test_jrpc_sys.nim | 246 ++++++++++++++ tests/test_router_rpc.nim | 97 ++++++ tests/testethcalls.nim | 21 +- tests/testhook.nim | 18 +- tests/testhttp.nim | 25 +- tests/testhttps.nim | 32 +- tests/testproxy.nim | 19 +- tests/testrpcmacro.nim | 98 ++++-- tests/testserverclient.nim | 28 +- 37 files changed, 1909 insertions(+), 798 deletions(-) delete mode 100644 json_rpc/jsonmarshal.nim create mode 100644 json_rpc/private/client_handler_wrapper.nim rename json_rpc/{ => private}/errors.nim (85%) create mode 100644 json_rpc/private/jrpc_conv.nim create mode 100644 json_rpc/private/jrpc_sys.nim create mode 100644 json_rpc/private/server_handler_wrapper.nim create mode 100644 json_rpc/private/shared_wrapper.nim delete mode 100644 tests/helpers.nim rename tests/{ => private}/ethcallsigs.nim (84%) rename tests/{ => private}/ethhexstrings.nim (74%) rename tests/{ => private}/ethprocs.nim (96%) rename tests/{ => private}/ethtypes.nim (100%) rename json_rpc/rpcsecureserver.nim => tests/private/file_callsigs.nim (62%) create mode 100644 tests/private/helpers.nim rename tests/{ => private}/stintjson.nim (60%) create mode 100644 tests/test_callsigs.nim create mode 100644 tests/test_jrpc_sys.nim create mode 100644 tests/test_router_rpc.nim diff --git a/README.md b/README.md index a55824d8..d1ff4ce9 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ router.rpc("hello") do() -> string: ``` As no return type was specified in this example, `result` defaults to the `JsonNode` type. -A JSON string is returned by passing a string though the `%` operator, which converts simple types to `JsonNode`. +A JSON string is returned by passing a string though the `JrpcConv` converter powered by [nim-json-serialization](https://github.com/status-im/nim-json-serialization). The `body` parameters can be defined by using [do notation](https://nim-lang.org/docs/manual.html#procedures-do-notation). This allows full Nim types to be used as RPC parameters. @@ -89,7 +89,7 @@ router.rpc("updateData") do(myObj: MyObject, newData: DataBlob) -> DataBlob: myObj.data = newData ``` -Behind the scenes, all RPC calls take a single json parameter `param` that must be of kind `JArray`. +Behind the scenes, all RPC calls take parameters through `RequestParamsRx` structure. At runtime, the json is checked to ensure that it contains the correct number and type of your parameters to match the `rpc` definition. Compiling with `-d:nimDumpRpcs` will show the output code for the RPC call. To see the output of the `async` generation, add `-d:nimDumpAsync`. @@ -129,83 +129,71 @@ type Note that `array` parameters are explicitly checked for length, and will return an error node if the length differs from their declaration size. -If you wish to support custom types in a particular way, you can provide matching `fromJson` and `%` procedures. +If you wish to support custom types in a particular way, you can provide matching `readValue` and `writeValue` procedures. +The custom serializer you write must be using `JrpcConv` flavor. -### `fromJson` +### `readValue` This takes a Json type and returns the Nim type. #### Parameters -`n: JsonNode`: The current node being processed +`r: var JsonReader[JrpcConv]`: The current JsonReader with JrpcConv flavor. -`argName: string`: The name of the field in `n` - -`result`: The type of this must be `var X` where `X` is the Nim type you wish to handle +`val: var MyInt`: Deserialized value. #### Example ```nim -proc fromJson[T](n: JsonNode, argName: string, result: var seq[T]) = - n.kind.expect(JArray, argName) - result = newSeq[T](n.len) - for i in 0 ..< n.len: - fromJson(n[i], argName, result[i]) +proc readValue*(r: var JsonReader[JrpcConv], val: var MyInt) + {.gcsafe, raises: [IOError, JsonReaderError].} = + let intVal = r.parseInt(int) + val = MyInt(intVal) ``` -### `%` +### `writeValue` -This is the standard way to provide translations from a Nim type to a `JsonNode`. +This is the standard way to provide translations from a Nim type to Json. #### Parameters -`n`: The type you wish to convert - -#### Returns - -`JsonNode`: The newly encoded `JsonNode` type from the parameter type. +`w: var JsonWriter[JrpcConv]`: The current JsonWriter with JrpcConv flavor. -### `expect` - -This is a simple procedure to state your expected type. - -If the actual type doesn't match the expected type, an exception is thrown mentioning which field caused the failure. - -#### Parameters - -`actual: JsonNodeKind`: The actual type of the `JsonNode`. - -`expected: JsonNodeKind`: The desired type. - -`argName: string`: The current field name. +`val: MyInt`: The value you want to convert into Json. #### Example ```nim -myNode.kind.expect(JArray, argName) +proc writeValue*(w: var JsonWriter[JrpcConv], val: MyInt) + {.gcsafe, raises: [IOError].} = + w.writeValue val.int ``` ## JSON Format -The router expects either a string or `JsonNode` with the following structure: +The router expects either a Json document with the following structure: ```json { - "id": JInt, + "id": Int or String, "jsonrpc": "2.0", - "method": JString, - "params": JArray + "method": String, + "params": Array or Object } + ``` +If params is an Array, it is a positional parameters. If it is an Object then the rpc method will be called using named parameters. + + Return values use the following node structure: ```json { - "id": JInt, + "id": Int Or String, "jsonrpc": "2.0", - "result": JsonNode, - "error": JsonNode + "result": Json document, + "error": Json document } ``` @@ -215,35 +203,35 @@ To call and RPC through the router, use the `route` procedure. There are three variants of `route`. -Note that once invoked all RPC calls are error trapped and any exceptions raised are passed back with the error message encoded as a `JsonNode`. +Note that once invoked all RPC calls are error trapped and any exceptions raised are passed back with the error message encoded as a `Json document`. ### `route` by string -This `route` variant will handle all the conversion of `string` to `JsonNode` and check the format and type of the input data. +This `route` variant will handle all the conversion of `string` to `Json document` and check the format and type of the input data. #### Parameters `router: RpcRouter`: The router object that contains the RPCs. -`data: string`: A string ready to be processed into a `JsonNode`. +`data: string`: A string ready to be processed into a `Json document`. #### Returns `Future[string]`: This will be the stringified JSON response, which can be the JSON RPC result or a JSON wrapped error. -### `route` by `JsonNode` +### `route` by `Json document` -This variant allows simplified processing if you already have a `JsonNode`. However if the required fields are not present within `node`, exceptions will be raised. +This variant allows simplified processing if you already have a `Json document`. However if the required fields are not present within `data`, exceptions will be raised. #### Parameters `router: RpcRouter`: The router object that contains the RPCs. -`node: JsonNode`: A pre-processed `JsonNode` that matches the expected format as defined above. +`req: RequestTx`: A pre-processed `Json document` that matches the expected format as defined above. #### Returns -`Future[JsonNode]`: The JSON RPC result or a JSON wrapped error. +`Future[ResponseTx]`: The JSON RPC result or a JSON wrapped error. ### `tryRoute` @@ -253,13 +241,13 @@ This `route` variant allows you to invoke a call if possible, without raising an `router: RpcRouter`: The router object that contains the RPCs. -`node: JsonNode`: A pre-processed `JsonNode` that matches the expected format as defined above. +`data: StringOfJson`: A raw `Json document` that matches the expected format as defined above. -`fut: var Future[JsonNode]`: The JSON RPC result or a JSON wrapped error. +`fut: var Future[StringOfJson]`: The stringified JSON RPC result or a JSON wrapped error. #### Returns -`bool`: `true` if the `method` field provided in `node` matches an available route. Returns `false` when the `method` cannot be found, or if `method` or `params` field cannot be found within `node`. +`Result[void, string]` `isOk` if the `method` field provided in `data` matches an available route. Returns `isErr` when the `method` cannot be found, or if `method` or `params` field cannot be found within `data`. To see the result of a call, we need to provide Json in the expected format. @@ -326,7 +314,7 @@ Below is the most basic way to use a remote call on the client. Here we manually supply the name and json parameters for the call. The `call` procedure takes care of the basic format of the JSON to send to the server. -However you still need to provide `params` as a `JsonNode`, which must exactly match the parameters defined in the equivalent `rpc` definition. +However you still need to provide `params` as a `JsonNode` or `RequestParamsTx`, which must exactly match the parameters defined in the equivalent `rpc` definition. ```nim import json_rpc/[rpcclient, rpcserver], chronos, json @@ -362,6 +350,11 @@ Because the signatures are parsed at compile time, the file will be error checke `path`: The path to the Nim module that contains the RPC header signatures. +#### Variants of createRpcSigs + - `createRpcSigsFromString`, generate rpc wrapper from string instead load it from file. + - `createSingleRpcSig`, generate rpc wrapper from single Nim proc signature, with alias. e.g. calling same rpc method using different return type. + - `createRpcSigsFromNim`, generate rpc wrapper from a list Nim proc signature, without loading any file. + #### Example For example, to support this remote call: @@ -404,7 +397,7 @@ Additionally, the following two procedures are useful: `name: string`: the method to be called `params: JsonNode`: The parameters to the RPC call Returning - `Future[Response]`: A wrapper for the result `JsonNode` and a flag to indicate if this contains an error. + `Future[StringOfJson]`: A wrapper for the result `Json document` and a flag to indicate if this contains an error. Note: Although `call` isn't necessary for a client to function, it allows RPC signatures to be used by the `createRpcSigs`. @@ -416,9 +409,9 @@ Note: Although `call` isn't necessary for a client to function, it allows RPC si ### `processMessage` -To simplify and unify processing within the client, the `processMessage` procedure can be used to perform conversion and error checking from the received string originating from the transport to the `JsonNode` representation that is passed to the RPC. +To simplify and unify processing within the client, the `processMessage` procedure can be used to perform conversion and error checking from the received string originating from the transport to the `Json document` representation that is passed to the RPC. -After a RPC returns, this procedure then completes the futures set by `call` invocations using the `id` field of the processed `JsonNode` from `line`. +After a RPC returns, this procedure then completes the futures set by `call` invocations using the `id` field of the processed `Json document` from `line`. #### Parameters diff --git a/json_rpc.nimble b/json_rpc.nimble index 8709d61d..9a34d07c 100644 --- a/json_rpc.nimble +++ b/json_rpc.nimble @@ -21,8 +21,8 @@ requires "nim >= 1.6.0", "stew", "nimcrypto", "stint", - "chronos", - "httputils", + "chronos#head", + "httputils#head", "chronicles", "websock", "json_serialization", diff --git a/json_rpc/client.nim b/json_rpc/client.nim index 804a228c..75d361cd 100644 --- a/json_rpc/client.nim +++ b/json_rpc/client.nim @@ -1,5 +1,5 @@ # json-rpc -# Copyright (c) 2019-2023 Status Research & Development GmbH +# Copyright (c) 2019-2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) @@ -8,195 +8,129 @@ # those terms. import - std/[tables, macros], + std/[json, tables, macros], chronos, - ./jsonmarshal + results, + ./private/jrpc_conv, + ./private/jrpc_sys, + ./private/client_handler_wrapper, + ./private/shared_wrapper, + ./private/errors -from strutils import toLowerAscii, replace +from strutils import replace export - chronos, jsonmarshal, tables + chronos, + tables, + jrpc_conv, + RequestParamsTx, + results type - ClientId* = int64 - MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [Defect, CatchableError].} RpcClient* = ref object of RootRef - awaiting*: Table[ClientId, Future[Response]] - lastId: ClientId - methodHandlers: Table[string, MethodHandler] - onDisconnect*: proc() {.gcsafe, raises: [Defect].} + awaiting*: Table[RequestId, Future[StringOfJson]] + lastId: int + onDisconnect*: proc() {.gcsafe, raises: [].} + + GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [].} + +{.push gcsafe, raises: [].} + +# ------------------------------------------------------------------------------ +# Public helpers +# ------------------------------------------------------------------------------ - Response* = JsonNode +func requestTxEncode*(name: string, params: RequestParamsTx, id: RequestId): string = + let req = requestTx(name, params, id) + JrpcSys.encode(req) - GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [Defect].} +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ -proc getNextId*(client: RpcClient): ClientId = +proc getNextId*(client: RpcClient): RequestId = client.lastId += 1 - client.lastId + RequestId(kind: riNumber, num: client.lastId) -proc rpcCallNode*(path: string, params: JsonNode, id: ClientId): JsonNode = - %{"jsonrpc": %"2.0", "method": %path, "params": params, "id": %id} +method call*(client: RpcClient, name: string, + params: RequestParamsTx): Future[StringOfJson] + {.base, gcsafe, async.} = + doAssert(false, "`RpcClient.call` not implemented") method call*(client: RpcClient, name: string, - params: JsonNode): Future[Response] {.base, async.} = - discard + params: JsonNode): Future[StringOfJson] + {.base, gcsafe, async.} = -method close*(client: RpcClient): Future[void] {.base, async.} = - discard + await client.call(name, params.paramsTx) -template `or`(a: JsonNode, b: typed): JsonNode = - if a.isNil: b else: a +method close*(client: RpcClient): Future[void] {.base, gcsafe, async.} = + doAssert(false, "`RpcClient.close` not implemented") -proc processMessage*(self: RpcClient, line: string) = +proc processMessage*(client: RpcClient, line: string): Result[void, string] = # Note: this doesn't use any transport code so doesn't need to be # differentiated. - let node = try: parseJson(line) - except CatchableError as exc: raise exc - # TODO https://github.com/status-im/nimbus-eth2/issues/2430 - except Exception as exc: raise (ref ValueError)(msg: exc.msg, parent: exc) - - if "id" in node: - let id = node{"id"} or newJNull() - - var requestFut: Future[Response] - if not self.awaiting.pop(id.getInt(-1), requestFut): - raise newException(ValueError, "Cannot find message id \"" & $id & "\"") - - let version = node{"jsonrpc"}.getStr() - if version != "2.0": - requestFut.fail(newException(ValueError, - "Unsupported version of JSON, expected 2.0, received \"" & version & "\"")) - else: - let result = node{"result"} - if result.isNil: - let error = node{"error"} or newJNull() - requestFut.fail(newException(ValueError, $error)) - else: - requestFut.complete(result) - elif "method" in node: - # This could be subscription notification - let name = node["method"].getStr() - let handler = self.methodHandlers.getOrDefault(name) - if not handler.isNil: - handler(node{"params"} or newJArray()) - else: - raise newException(ValueError, "Invalid jsonrpc message: " & $node) + try: + let response = JrpcSys.decode(line, ResponseRx) -# Signature processing + if response.jsonrpc.isNone: + return err("missing or invalid `jsonrpc`") + + if response.id.isNone: + return err("missing or invalid response id") -proc createRpcProc(procName, parameters, callBody: NimNode): NimNode = - # parameters come as a tree - var paramList = newSeq[NimNode]() - for p in parameters: paramList.add(p) - - let body = quote do: - {.gcsafe.}: - `callBody` - - # build proc - result = newProc(procName, paramList, body) - - # make proc async - result.addPragma ident"async" - # export this proc - result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName)) - -proc toJsonArray(parameters: NimNode): NimNode = - # outputs an array of jsonified parameters - # ie; %[%a, %b, %c] - parameters.expectKind nnkFormalParams - var items = newNimNode(nnkBracket) - for i in 2 ..< parameters.len: - let curParam = parameters[i][0] - if curParam.kind != nnkEmpty: - items.add(nnkPrefix.newTree(ident"%", curParam)) - result = nnkPrefix.newTree(bindSym("%", brForceOpen), items) - -proc createRpcFromSig*(clientType, rpcDecl: NimNode): NimNode = - # Each input parameter in the rpc signature is converted - # to json with `%`. - # Return types are then converted back to native Nim types. - let iJsonNode = newIdentNode("JsonNode") - - var parameters = rpcDecl.findChild(it.kind == nnkFormalParams).copy - # ensure we have at least space for a return parameter - if parameters.isNil or parameters.kind == nnkEmpty or parameters.len == 0: - parameters = nnkFormalParams.newTree(iJsonNode) - - let - procName = rpcDecl.name - pathStr = $procName - returnType = - # if no return type specified, defaults to JsonNode - if parameters[0].kind == nnkEmpty: iJsonNode - else: parameters[0] - customReturnType = returnType != iJsonNode - - # insert rpc client as first parameter - parameters.insert(1, nnkIdentDefs.newTree(ident"client", ident($clientType), - newEmptyNode())) - - let - # variable used to send json to the server - jsonParamIdent = genSym(nskVar, "jsonParam") - # json array of marshalled parameters - jsonParamArray = parameters.toJsonArray() - var - # populate json params - even rpcs with no parameters have an empty json - # array node sent - callBody = newStmtList().add(quote do: - var `jsonParamIdent` = `jsonParamArray` - ) - - # convert return type to Future - parameters[0] = nnkBracketExpr.newTree(ident"Future", returnType) - - let - # temporary variable to hold `Response` from rpc call - rpcResult = genSym(nskLet, "res") - clientIdent = newIdentNode("client") - # proc return variable - procRes = ident"result" - - # perform rpc call - callBody.add(quote do: - # `rpcResult` is of type `Response` - let `rpcResult` = await `clientIdent`.call(`pathStr`, `jsonParamIdent`) - ) - - if customReturnType: - # marshal json to native Nim type - callBody.add(jsonToNim(procRes, returnType, rpcResult, "result")) - else: - # native json expected so no work - callBody.add quote do: - `procRes` = if `rpcResult`.isNil: - newJNull() - else: - `rpcResult` - - # create rpc proc - result = createRpcProc(procName, parameters, callBody) - when defined(nimDumpRpcs): - echo pathStr, ":\n", result.repr - -proc processRpcSigs(clientType, parsedCode: NimNode): NimNode = - result = newStmtList() - - for line in parsedCode: - if line.kind == nnkProcDef: - var procDef = createRpcFromSig(clientType, line) - result.add(procDef) - -proc setMethodHandler*(cl: RpcClient, name: string, callback: MethodHandler) = - cl.methodHandlers[name] = callback - -proc delMethodHandler*(cl: RpcClient, name: string) = - cl.methodHandlers.del(name) + var requestFut: Future[StringOfJson] + let id = response.id.get + if not client.awaiting.pop(id, requestFut): + return err("Cannot find message id \"" & $id & "\"") + + if response.error.isSome: + let error = JrpcSys.encode(response.error.get) + requestFut.fail(newException(JsonRpcError, error)) + return ok() + + if response.result.isNone: + return err("missing or invalid response result") + + requestFut.complete(response.result.get) + return ok() + + except CatchableError as exc: + return err(exc.msg) + +# ------------------------------------------------------------------------------ +# Signature processing +# ------------------------------------------------------------------------------ macro createRpcSigs*(clientType: untyped, filePath: static[string]): untyped = ## Takes a file of forward declarations in Nim and builds them into RPC ## calls, based on their parameters. ## Inputs are marshalled to json, and results are put into the signature's ## Nim type. - result = processRpcSigs(clientType, staticRead($filePath.replace('\\', '/')).parseStmt()) + cresteSignaturesFromString(clientType, staticRead($filePath.replace('\\', '/'))) + +macro createRpcSigsFromString*(clientType: untyped, sigString: static[string]): untyped = + ## Takes a string of forward declarations in Nim and builds them into RPC + ## calls, based on their parameters. + ## Inputs are marshalled to json, and results are put into the signature's + ## Nim type. + cresteSignaturesFromString(clientType, sigString) + +macro createSingleRpcSig*(clientType: untyped, alias: static[string], procDecl: typed): untyped = + ## Takes a single forward declarations in Nim and builds them into RPC + ## calls, based on their parameters. + ## Inputs are marshalled to json, and results are put into the signature's + ## Nim type. + doAssert procDecl.len == 1, "Only accept single proc definition" + let procDecl = procDecl[0] + procDecl.expectKind nnkProcDef + result = createRpcFromSig(clientType, procDecl, ident(alias)) + +macro createRpcSigsFromNim*(clientType: untyped, procList: typed): untyped = + ## Takes a list of forward declarations in Nim and builds them into RPC + ## calls, based on their parameters. + ## Inputs are marshalled to json, and results are put into the signature's + ## Nim type. + processRpcSigs(clientType, procList) + +{.pop.} + diff --git a/json_rpc/clients/httpclient.nim b/json_rpc/clients/httpclient.nim index be9f8102..4ca646fb 100644 --- a/json_rpc/clients/httpclient.nim +++ b/json_rpc/clients/httpclient.nim @@ -9,16 +9,17 @@ import std/[tables, uri], - stew/[byteutils, results], + stew/byteutils, + results, chronos/apps/http/httpclient as chronosHttpClient, chronicles, httputils, json_serialization/std/net, - ".."/[client, errors] + ../client, + ../private/errors, + ../private/jrpc_sys export client, HttpClientFlag, HttpClientFlags -{.push raises: [Defect].} - logScope: topics = "JSONRPC-HTTP-CLIENT" @@ -35,6 +36,8 @@ type const MaxHttpRequestSize = 128 * 1024 * 1024 # maximum size of HTTP body in octets +{.push gcsafe, raises: [].} + proc new( T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false, getHeaders: GetJsonRpcRequestHeaders = nil, flags: HttpClientFlags = {}): T = @@ -51,7 +54,7 @@ proc newRpcHttpClient*( RpcHttpClient.new(maxBodySize, secure, getHeaders, flags) method call*(client: RpcHttpClient, name: string, - params: JsonNode): Future[Response] + params: RequestParamsTx): Future[StringOfJson] {.async, gcsafe.} = doAssert client.httpSession != nil if client.httpAddress.isErr: @@ -66,7 +69,7 @@ method call*(client: RpcHttpClient, name: string, let id = client.getNextId() - reqBody = $rpcCallNode(name, params, id) + reqBody = requestTxEncode(name, params, id) var req: HttpClientRequestRef var res: HttpClientResponseRef @@ -128,19 +131,18 @@ method call*(client: RpcHttpClient, name: string, # completed by processMessage - the flow is quite weird here to accomodate # socket and ws clients, but could use a more thorough refactoring - var newFut = newFuture[Response]() + var newFut = newFuture[StringOfJson]() # add to awaiting responses client.awaiting[id] = newFut - try: - # Might raise for all kinds of reasons - client.processMessage(resText) - except CatchableError as e: + # Might error for all kinds of reasons + let msgRes = client.processMessage(resText) + if msgRes.isErr: # Need to clean up in case the answer was invalid - debug "Failed to process POST Response for JSON-RPC", e = e.msg + debug "Failed to process POST Response for JSON-RPC", msg = msgRes.error client.awaiting.del(id) closeRefs() - raise e + raise newException(JsonRpcError, msgRes.error) client.awaiting.del(id) @@ -175,3 +177,5 @@ proc connect*(client: RpcHttpClient, address: string, port: Port, secure: bool) method close*(client: RpcHttpClient) {.async.} = if not client.httpSession.isNil: await client.httpSession.closeWait() + +{.pop.} diff --git a/json_rpc/clients/socketclient.nim b/json_rpc/clients/socketclient.nim index 1ad5c7e2..97d85bee 100644 --- a/json_rpc/clients/socketclient.nim +++ b/json_rpc/clients/socketclient.nim @@ -9,10 +9,12 @@ import std/tables, + chronicles, + results, chronos, - ../client - -{.push raises: [Defect].} + ../client, + ../private/errors, + ../private/jrpc_sys export client @@ -24,6 +26,8 @@ type const defaultMaxRequestLength* = 1024 * 128 +{.push gcsafe, raises: [].} + proc new*(T: type RpcSocketClient): T = T() @@ -32,16 +36,16 @@ proc newRpcSocketClient*: RpcSocketClient = RpcSocketClient.new() method call*(self: RpcSocketClient, name: string, - params: JsonNode): Future[Response] {.async, gcsafe.} = + params: RequestParamsTx): Future[StringOfJson] {.async, gcsafe.} = ## Remotely calls the specified RPC method. let id = self.getNextId() - var value = $rpcCallNode(name, params, id) & "\r\n" + var value = requestTxEncode(name, params, id) & "\r\n" if self.transport.isNil: - raise newException(ValueError, + raise newException(JsonRpcError, "Transport is not initialised (missing a call to connect?)") # completed by processMessage. - var newFut = newFuture[Response]() + var newFut = newFuture[StringOfJson]() # add to awaiting responses self.awaiting[id] = newFut @@ -60,8 +64,10 @@ proc processData(client: RpcSocketClient) {.async.} = await client.transport.closeWait() break - # TODO handle exceptions - client.processMessage(value) + let res = client.processMessage(value) + if res.isErr: + error "error when processing message", msg=res.error + raise newException(JsonRpcError, res.error) # async loop reconnection and waiting client.transport = await connect(client.address) diff --git a/json_rpc/clients/websocketclientimpl.nim b/json_rpc/clients/websocketclientimpl.nim index 3d61d29b..13b3d195 100644 --- a/json_rpc/clients/websocketclientimpl.nim +++ b/json_rpc/clients/websocketclientimpl.nim @@ -11,13 +11,12 @@ import std/[uri, strutils], pkg/websock/[websock, extensions/compression/deflate], pkg/[chronos, chronos/apps/http/httptable, chronicles], - stew/byteutils + stew/byteutils, + ../private/errors # avoid clash between Json.encode and Base64Pad.encode import ../client except encode -{.push raises: [Defect].} - logScope: topics = "JSONRPC-WS-CLIENT" @@ -28,6 +27,8 @@ type loop*: Future[void] getHeaders*: GetJsonRpcRequestHeaders +{.push gcsafe, raises: [].} + proc new*( T: type RpcWebSocketClient, getHeaders: GetJsonRpcRequestHeaders = nil): T = T(getHeaders: getHeaders) @@ -38,16 +39,16 @@ proc newRpcWebSocketClient*( RpcWebSocketClient.new(getHeaders) method call*(self: RpcWebSocketClient, name: string, - params: JsonNode): Future[Response] {.async, gcsafe.} = + params: RequestParamsTx): Future[StringOfJson] {.async, gcsafe.} = ## Remotely calls the specified RPC method. let id = self.getNextId() - var value = $rpcCallNode(name, params, id) & "\r\n" + var value = requestTxEncode(name, params, id) & "\r\n" if self.transport.isNil: - raise newException(ValueError, + raise newException(JsonRpcError, "Transport is not initialised (missing a call to connect?)") # completed by processMessage. - var newFut = newFuture[Response]() + var newFut = newFuture[StringOfJson]() # add to awaiting responses self.awaiting[id] = newFut @@ -66,7 +67,10 @@ proc processData(client: RpcWebSocketClient) {.async.} = # transmission ends break - client.processMessage(string.fromBytes(value)) + let res = client.processMessage(string.fromBytes(value)) + if res.isErr: + raise newException(JsonRpcError, res.error) + except CatchableError as e: error = e diff --git a/json_rpc/jsonmarshal.nim b/json_rpc/jsonmarshal.nim deleted file mode 100644 index f30ec335..00000000 --- a/json_rpc/jsonmarshal.nim +++ /dev/null @@ -1,242 +0,0 @@ -# json-rpc -# Copyright (c) 2019-2023 Status Research & Development GmbH -# Licensed under either of -# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) -# * MIT license ([LICENSE-MIT](LICENSE-MIT)) -# at your option. -# This file may not be copied, modified, or distributed except according to -# those terms. - -import - std/[macros, json, typetraits], - stew/[byteutils, objects], - json_serialization, - json_serialization/lexer, - json_serialization/std/[options, sets, tables] - -export json, options, json_serialization - -Json.createFlavor JsonRpc - -# Avoid templates duplicating the string in the executable. -const errDeserializePrefix = "Error deserializing stream for type '" - -template wrapErrors(reader, value, actions: untyped): untyped = - ## Convert read errors to `UnexpectedValue` for the purpose of marshalling. - try: - actions - except Exception as err: - reader.raiseUnexpectedValue(errDeserializePrefix & $type(value) & "': " & err.msg) - -# Bytes. - -proc readValue*(r: var JsonReader[JsonRpc], value: var byte) = - ## Implement separate read serialization for `byte` to avoid - ## 'can raise Exception' for `readValue(value, uint8)`. - wrapErrors r, value: - case r.lexer.tok - of tkInt: - if r.lexer.absIntVal in 0'u32 .. byte.high: - value = byte(r.lexer.absIntVal) - else: - r.raiseIntOverflow r.lexer.absIntVal, true - of tkNegativeInt: - r.raiseIntOverflow r.lexer.absIntVal, true - else: - r.raiseUnexpectedToken etInt - r.lexer.next() - -proc writeValue*(w: var JsonWriter[JsonRpc], value: byte) = - json_serialization.writeValue(w, uint8(value)) - -# Enums. - -proc readValue*(r: var JsonReader[JsonRpc], value: var (enum)) = - wrapErrors r, value: - value = type(value) json_serialization.readValue(r, uint64) - -proc writeValue*(w: var JsonWriter[JsonRpc], value: (enum)) = - json_serialization.writeValue(w, uint64(value)) - -# Other base types. - -macro genDistinctSerializers(types: varargs[untyped]): untyped = - ## Implements distinct serialization pass-throughs for `types`. - result = newStmtList() - for ty in types: - result.add(quote do: - - proc readValue*(r: var JsonReader[JsonRpc], value: var `ty`) = - wrapErrors r, value: - json_serialization.readValue(r, value) - - proc writeValue*(w: var JsonWriter[JsonRpc], value: `ty`) {.raises: [IOError].} = - json_serialization.writeValue(w, value) - ) - -genDistinctSerializers bool, int, float, string, int64, uint64, uint32, ref int64, ref int - -# Sequences and arrays. - -proc readValue*[T](r: var JsonReader[JsonRpc], value: var seq[T]) = - wrapErrors r, value: - json_serialization.readValue(r, value) - -proc writeValue*[T](w: var JsonWriter[JsonRpc], value: seq[T]) = - json_serialization.writeValue(w, value) - -proc readValue*[N: static[int]](r: var JsonReader[JsonRpc], value: var array[N, byte]) = - ## Read an array while allowing partial data. - wrapErrors r, value: - r.skipToken tkBracketLe - if r.lexer.tok != tkBracketRi: - for i in low(value) .. high(value): - readValue(r, value[i]) - if r.lexer.tok == tkBracketRi: - break - else: - r.skipToken tkComma - r.skipToken tkBracketRi - -# High level generic unpacking. - -proc unpackArg[T](args: JsonNode, argName: string, argType: typedesc[T]): T {.raises: [ValueError].} = - if args.isNil: - raise newException(ValueError, argName & ": unexpected null value") - try: - result = JsonRpc.decode($args, argType) - except CatchableError as err: - raise newException(ValueError, - "Parameter [" & argName & "] of type '" & $argType & "' could not be decoded: " & err.msg) - -proc expect*(actual, expected: JsonNodeKind, argName: string) = - if actual != expected: - raise newException( - ValueError, "Parameter [" & argName & "] expected " & $expected & " but got " & $actual) - -proc expectArrayLen(node, jsonIdent: NimNode, length: int) = - let - identStr = jsonIdent.repr - expectedStr = "Expected " & $length & " Json parameter(s) but got " - node.add(quote do: - `jsonIdent`.kind.expect(JArray, `identStr`) - if `jsonIdent`.len != `length`: - raise newException(ValueError, `expectedStr` & $`jsonIdent`.len) - ) - -iterator paramsIter(params: NimNode): tuple[name, ntype: NimNode] = - for i in 1 ..< params.len: - let arg = params[i] - let argType = arg[^2] - for j in 0 ..< arg.len-2: - yield (arg[j], argType) - -iterator paramsRevIter(params: NimNode): tuple[name, ntype: NimNode] = - for i in countdown(params.len-1,1): - let arg = params[i] - let argType = arg[^2] - for j in 0 ..< arg.len-2: - yield (arg[j], argType) - -proc isOptionalArg(typeNode: NimNode): bool = - typeNode.kind == nnkBracketExpr and - typeNode[0].kind == nnkIdent and - typeNode[0].strVal == "Option" - -proc expectOptionalArrayLen(node, parameters, jsonIdent: NimNode, maxLength: int): int = - var minLength = maxLength - - for arg, typ in paramsRevIter(parameters): - if not typ.isOptionalArg: break - dec minLength - - let - identStr = jsonIdent.repr - expectedStr = "Expected at least " & $minLength & " and maximum " & $maxLength & " Json parameter(s) but got " - - node.add(quote do: - `jsonIdent`.kind.expect(JArray, `identStr`) - if `jsonIdent`.len < `minLength`: - raise newException(ValueError, `expectedStr` & $`jsonIdent`.len) - ) - - minLength - -proc containsOptionalArg(params: NimNode): bool = - for n, t in paramsIter(params): - if t.isOptionalArg: - return true - -proc jsonToNim*(assignIdent, paramType, jsonIdent: NimNode, paramNameStr: string, optional = false): NimNode = - # verify input and load a Nim type from json data - # note: does not create `assignIdent`, so can be used for `result` variables - result = newStmtList() - # unpack each parameter and provide assignments - let unpackNode = quote do: - `unpackArg`(`jsonIdent`, `paramNameStr`, type(`paramType`)) - - if optional: - result.add(quote do: `assignIdent` = some(`unpackNode`)) - else: - result.add(quote do: `assignIdent` = `unpackNode`) - -proc calcActualParamCount(params: NimNode): int = - # this proc is needed to calculate the actual parameter count - # not matter what is the declaration form - # e.g. (a: U, b: V) vs. (a, b: T) - for n, t in paramsIter(params): - inc result - -proc jsonToNim*(params, jsonIdent: NimNode): NimNode = - # Add code to verify input and load params into Nim types - result = newStmtList() - if not params.isNil: - var minLength = 0 - if params.containsOptionalArg(): - # more elaborate parameters array check - minLength = result.expectOptionalArrayLen(params, jsonIdent, - calcActualParamCount(params)) - else: - # simple parameters array length check - result.expectArrayLen(jsonIdent, calcActualParamCount(params)) - - # unpack each parameter and provide assignments - var pos = 0 - for paramIdent, paramType in paramsIter(params): - # processing multiple variables of one type - # e.g. (a, b: T), including common (a: U, b: V) form - let - paramName = $paramIdent - jsonElement = quote do: - `jsonIdent`.elems[`pos`] - - # declare variable before assignment - result.add(quote do: - var `paramIdent`: `paramType` - ) - - # e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string]) - if paramType.isOptionalArg: - let - innerType = paramType[1] - innerNode = jsonToNim(paramIdent, innerType, jsonElement, paramName, true) - - if pos >= minLength: - # allow both empty and null after mandatory args - # D & E fall into this category - result.add(quote do: - if `jsonIdent`.len > `pos` and `jsonElement`.kind != JNull: `innerNode` - ) - else: - # allow null param for optional args between/before mandatory args - # B fall into this category - result.add(quote do: - if `jsonElement`.kind != JNull: `innerNode` - ) - else: - # mandatory args - # A and C fall into this category - # unpack Nim type and assign from json - result.add jsonToNim(paramIdent, paramType, jsonElement, paramName) - - inc pos diff --git a/json_rpc/private/client_handler_wrapper.nim b/json_rpc/private/client_handler_wrapper.nim new file mode 100644 index 00000000..caddbd62 --- /dev/null +++ b/json_rpc/private/client_handler_wrapper.nim @@ -0,0 +1,109 @@ +# json-rpc +# Copyright (c) 2024 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + macros, + ./shared_wrapper + +{.push gcsafe, raises: [].} + +proc createRpcProc(procName, parameters, callBody: NimNode): NimNode = + # parameters come as a tree + var paramList = newSeq[NimNode]() + for p in parameters: paramList.add(p) + + let body = quote do: + {.gcsafe.}: + `callBody` + + # build proc + result = newProc(procName, paramList, body) + + # make proc async + result.addPragma ident"async" + result.addPragma ident"gcsafe" + # export this proc + result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName)) + +proc setupConversion(reqParams, params: NimNode): NimNode = + # populate json params + # even rpcs with no parameters have an empty json array node sent + + params.expectKind nnkFormalParams + result = newStmtList() + result.add quote do: + var `reqParams` = RequestParamsTx(kind: rpPositional) + + for parName, parType in paramsIter(params): + result.add quote do: + `reqParams`.positional.add encode(JrpcConv, `parName`).StringOfJson + +proc createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimNode = + # Each input parameter in the rpc signature is converted + # to json using JrpcConv.encode. + # Return types are then converted back to native Nim types. + + let + params = rpcDecl.findChild(it.kind == nnkFormalParams).ensureReturnType + procName = if alias.isNil: rpcDecl.name else: alias + pathStr = $rpcDecl.name + returnType = params[0] + reqParams = genSym(nskVar, "reqParams") + setup = setupConversion(reqParams, params) + clientIdent = ident"client" + # temporary variable to hold `Response` from rpc call + rpcResult = genSym(nskLet, "res") + # proc return variable + procRes = ident"result" + doDecode = quote do: + `procRes` = decode(JrpcConv, `rpcResult`.string, typeof `returnType`) + maybeWrap = + if returnType.noWrap: quote do: + `procRes` = `rpcResult` + else: doDecode + + # insert rpc client as first parameter + params.insert(1, nnkIdentDefs.newTree( + clientIdent, + ident($clientType), + newEmptyNode() + )) + + # convert return type to Future + params[0] = nnkBracketExpr.newTree(ident"Future", returnType) + + # perform rpc call + let callBody = quote do: + # populate request params + `setup` + + # `rpcResult` is of type `StringOfJson` + let `rpcResult` = await `clientIdent`.call(`pathStr`, `reqParams`) + `maybeWrap` + + # create rpc proc + result = createRpcProc(procName, params, callBody) + when defined(nimDumpRpcs): + echo pathStr, ":\n", result.repr + +proc processRpcSigs*(clientType, parsedCode: NimNode): NimNode = + result = newStmtList() + + for line in parsedCode: + if line.kind == nnkProcDef: + var procDef = createRpcFromSig(clientType, line) + result.add(procDef) + +proc cresteSignaturesFromString*(clientType: NimNode, sigStrings: string): NimNode = + try: + result = processRpcSigs(clientType, sigStrings.parseStmt()) + except ValueError as exc: + doAssert(false, exc.msg) + +{.pop.} diff --git a/json_rpc/errors.nim b/json_rpc/private/errors.nim similarity index 85% rename from json_rpc/errors.nim rename to json_rpc/private/errors.nim index 63eb0ee6..0b9aaca4 100644 --- a/json_rpc/errors.nim +++ b/json_rpc/private/errors.nim @@ -31,3 +31,10 @@ type ## This could be raised by request handlers when the server ## needs to respond with a custom error code. code*: int + + RequestDecodeError* = object of JsonRpcError + ## raised when fail to decode RequestRx + + ParamsEncodeError* = object of JsonRpcError + ## raised when fail to encode RequestParamsTx + diff --git a/json_rpc/private/jrpc_conv.nim b/json_rpc/private/jrpc_conv.nim new file mode 100644 index 00000000..dda589ce --- /dev/null +++ b/json_rpc/private/jrpc_conv.nim @@ -0,0 +1,23 @@ +# json-rpc +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + json_serialization + +export + json_serialization + +type + StringOfJson* = JsonString + +createJsonFlavor JrpcConv, + requireAllFields = false + +# JrpcConv is a namespace/flavor for encoding and decoding +# parameters and return value of a rpc method. diff --git a/json_rpc/private/jrpc_sys.nim b/json_rpc/private/jrpc_sys.nim new file mode 100644 index 00000000..c118611d --- /dev/null +++ b/json_rpc/private/jrpc_sys.nim @@ -0,0 +1,306 @@ +# json-rpc +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + std/hashes, + results, + json_serialization, + json_serialization/stew/results as jser_results + +export + results, + json_serialization + +# This module implements JSON-RPC 2.0 Specification +# https://www.jsonrpc.org/specification + +type + # Special object of Json-RPC 2.0 + JsonRPC2* = object + + RequestParamKind* = enum + rpPositional + rpNamed + + ParamDescRx* = object + kind* : JsonValueKind + param*: JsonString + + ParamDescNamed* = object + name*: string + value*: JsonString + + # Request params received by server + RequestParamsRx* = object + case kind*: RequestParamKind + of rpPositional: + positional*: seq[ParamDescRx] + of rpNamed: + named*: seq[ParamDescNamed] + + # Request params sent by client + RequestParamsTx* = object + case kind*: RequestParamKind + of rpPositional: + positional*: seq[JsonString] + of rpNamed: + named*: seq[ParamDescNamed] + + RequestIdKind* = enum + riNull + riNumber + riString + + RequestId* = object + case kind*: RequestIdKind + of riNumber: + num*: int + of riString: + str*: string + of riNull: + discard + + # Request received by server + RequestRx* = object + jsonrpc* : results.Opt[JsonRPC2] + id* : RequestId + `method`*: results.Opt[string] + params* : RequestParamsRx + + # Request sent by client + RequestTx* = object + jsonrpc* : JsonRPC2 + id* : results.Opt[RequestId] + `method`*: string + params* : RequestParamsTx + + ResponseError* = object + code* : int + message*: string + data* : results.Opt[JsonString] + + ResponseKind* = enum + rkResult + rkError + + # Response sent by server + ResponseTx* = object + jsonrpc* : JsonRPC2 + id* : RequestId + case kind*: ResponseKind + of rkResult: + result* : JsonString + of rkError: + error* : ResponseError + + # Response received by client + ResponseRx* = object + jsonrpc*: results.Opt[JsonRPC2] + id* : results.Opt[RequestId] + result* : results.Opt[JsonString] + error* : results.Opt[ResponseError] + + ReBatchKind* = enum + rbkSingle + rbkMany + + RequestBatchRx* = object + case kind*: ReBatchKind + of rbkMany: + many* : seq[RequestRx] + of rbkSingle: + single*: RequestRx + + RequestBatchTx* = object + case kind*: ReBatchKind + of rbkMany: + many* : seq[RequestTx] + of rbkSingle: + single*: RequestTx + + ResponseBatchRx* = object + case kind*: ReBatchKind + of rbkMany: + many* : seq[ResponseRx] + of rbkSingle: + single*: ResponseRx + + ResponseBatchTx* = object + case kind*: ReBatchKind + of rbkMany: + many* : seq[ResponseTx] + of rbkSingle: + single*: ResponseTx + +# don't mix the json-rpc system encoding with the +# actual response/params encoding +createJsonFlavor JrpcSys, + requireAllFields = false + +ResponseError.useDefaultSerializationIn JrpcSys +RequestTx.useDefaultWriterIn JrpcSys +ResponseRx.useDefaultReaderIn JrpcSys +RequestRx.useDefaultReaderIn JrpcSys + +const + JsonRPC2Literal = JsonString("\"2.0\"") + +{.push gcsafe, raises: [].} + +func hash*(x: RequestId): hashes.Hash = + var h = 0.Hash + case x.kind: + of riNumber: h = h !& hash(x.num) + of riString: h = h !& hash(x.str) + of riNull: h = h !& hash("null") + result = !$(h) + +func `$`*(x: RequestId): string = + case x.kind: + of riNumber: $x.num + of riString: x.str + of riNull: "null" + +func `==`*(a, b: RequestId): bool = + if a.kind != b.kind: + return false + case a.kind + of riNumber: a.num == b.num + of riString: a.str == b.str + of riNull: true + +func meth*(rx: RequestRx): Opt[string] = + rx.`method` + +proc readValue*(r: var JsonReader[JrpcSys], val: var JsonRPC2) + {.gcsafe, raises: [IOError, JsonReaderError].} = + let version = r.parseAsString() + if version != JsonRPC2Literal: + r.raiseUnexpectedValue("Invalid JSON-RPC version, want=" & + JsonRPC2Literal.string & " got=" & version.string) + +proc writeValue*(w: var JsonWriter[JrpcSys], val: JsonRPC2) + {.gcsafe, raises: [IOError].} = + w.writeValue JsonRPC2Literal + +proc readValue*(r: var JsonReader[JrpcSys], val: var RequestId) + {.gcsafe, raises: [IOError, JsonReaderError].} = + let tok = r.tokKind + case tok + of JsonValueKind.Number: + val = RequestId(kind: riNumber, num: r.parseInt(int)) + of JsonValueKind.String: + val = RequestId(kind: riString, str: r.parseString()) + of JsonValueKind.Null: + val = RequestId(kind: riNull) + r.parseNull() + else: + r.raiseUnexpectedValue("Invalid RequestId, must be Number, String, or Null, got=" & $tok) + +proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestId) + {.gcsafe, raises: [IOError].} = + case val.kind + of riNumber: w.writeValue val.num + of riString: w.writeValue val.str + of riNull: w.writeValue JsonString("null") + +proc readValue*(r: var JsonReader[JrpcSys], val: var RequestParamsRx) + {.gcsafe, raises: [IOError, SerializationError].} = + let tok = r.tokKind + case tok + of JsonValueKind.Array: + val = RequestParamsRx(kind: rpPositional) + r.parseArray: + val.positional.add ParamDescRx( + kind: r.tokKind(), + param: r.parseAsString(), + ) + of JsonValueKind.Object: + val = RequestParamsRx(kind: rpNamed) + for key in r.readObjectFields(): + val.named.add ParamDescNamed( + name: key, + value: r.parseAsString(), + ) + else: + r.raiseUnexpectedValue("RequestParam must be either array or object, got=" & $tok) + +proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestParamsTx) + {.gcsafe, raises: [IOError].} = + case val.kind + of rpPositional: + w.writeArray val.positional + of rpNamed: + w.beginRecord RequestParamsTx + for x in val.named: + w.writeField(x.name, x.value) + w.endRecord() + +proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseTx) + {.gcsafe, raises: [IOError].} = + w.beginRecord ResponseTx + w.writeField("jsonrpc", val.jsonrpc) + w.writeField("id", val.id) + if val.kind == rkResult: + w.writeField("result", val.result) + else: + w.writeField("error", val.error) + w.endRecord() + +proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestBatchTx) + {.gcsafe, raises: [IOError].} = + if val.kind == rbkMany: + w.writeArray(val.many) + else: + w.writeValue(val.single) + +proc readValue*(r: var JsonReader[JrpcSys], val: var RequestBatchRx) + {.gcsafe, raises: [IOError, SerializationError].} = + let tok = r.tokKind + case tok + of JsonValueKind.Array: + val = RequestBatchRx(kind: rbkMany) + r.readValue(val.many) + of JsonValueKind.Object: + val = RequestBatchRx(kind: rbkSingle) + r.readValue(val.single) + else: + r.raiseUnexpectedValue("RequestBatch must be either array or object, got=" & $tok) + +proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseBatchTx) + {.gcsafe, raises: [IOError].} = + if val.kind == rbkMany: + w.writeArray(val.many) + else: + w.writeValue(val.single) + +proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseBatchRx) + {.gcsafe, raises: [IOError, SerializationError].} = + let tok = r.tokKind + case tok + of JsonValueKind.Array: + val = ResponseBatchRx(kind: rbkMany) + r.readValue(val.many) + of JsonValueKind.Object: + val = ResponseBatchRx(kind: rbkSingle) + r.readValue(val.single) + else: + r.raiseUnexpectedValue("ResponseBatch must be either array or object, got=" & $tok) + +proc toTx*(params: RequestParamsRx): RequestParamsTx = + case params.kind: + of rpPositional: + result = RequestParamsTx(kind: rpPositional) + for x in params.positional: + result.positional.add x.param + of rpNamed: + result = RequestParamsTx(kind: rpNamed) + result.named = params.named + +{.pop.} diff --git a/json_rpc/private/server_handler_wrapper.nim b/json_rpc/private/server_handler_wrapper.nim new file mode 100644 index 00000000..d7116be9 --- /dev/null +++ b/json_rpc/private/server_handler_wrapper.nim @@ -0,0 +1,301 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + std/[macros, typetraits], + stew/[byteutils, objects], + json_serialization, + json_serialization/std/[options], + ./errors, + ./jrpc_sys, + ./jrpc_conv, + ./shared_wrapper + +export + jrpc_conv + +{.push gcsafe, raises: [].} + +proc unpackArg(args: JsonString, argName: string, argType: type): argType + {.gcsafe, raises: [JsonRpcError].} = + ## This where input parameters are decoded from JSON into + ## Nim data types + try: + result = JrpcConv.decode(args.string, argType) + except CatchableError as err: + raise newException(RequestDecodeError, + "Parameter [" & argName & "] of type '" & + $argType & "' could not be decoded: " & err.msg) + +proc expectArrayLen(node, paramsIdent: NimNode, length: int) = + ## Make sure positional params meets the handler expectation + let + expected = "Expected " & $length & " Json parameter(s) but got " + node.add quote do: + if `paramsIdent`.positional.len != `length`: + raise newException(RequestDecodeError, `expected` & + $`paramsIdent`.positional.len) + +iterator paramsRevIter(params: NimNode): tuple[name, ntype: NimNode] = + ## Bacward iterator of handler parameters + for i in countdown(params.len-1,1): + let arg = params[i] + let argType = arg[^2] + for j in 0 ..< arg.len-2: + yield (arg[j], argType) + +proc isOptionalArg(typeNode: NimNode): bool = + typeNode.kind == nnkBracketExpr and + typeNode[0].kind == nnkIdent and + typeNode[0].strVal == "Option" + +proc expectOptionalArrayLen(node: NimNode, + parameters: NimNode, + paramsIdent: NimNode, + maxLength: int): int = + ## Validate if parameters sent by client meets + ## minimum expectation of server + var minLength = maxLength + + for arg, typ in paramsRevIter(parameters): + if not typ.isOptionalArg: break + dec minLength + + let + expected = "Expected at least " & $minLength & " and maximum " & + $maxLength & " Json parameter(s) but got " + + node.add quote do: + if `paramsIdent`.positional.len < `minLength`: + raise newException(RequestDecodeError, `expected` & + $`paramsIdent`.positional.len) + + minLength + +proc containsOptionalArg(params: NimNode): bool = + ## Is one of handler parameters an optional? + for n, t in paramsIter(params): + if t.isOptionalArg: + return true + +proc jsonToNim(paramVar: NimNode, + paramType: NimNode, + paramVal: NimNode, + paramName: string): NimNode = + ## Convert a positional parameter from Json into Nim + result = quote do: + `paramVar` = `unpackArg`(`paramVal`, `paramName`, `paramType`) + +proc calcActualParamCount(params: NimNode): int = + ## this proc is needed to calculate the actual parameter count + ## not matter what is the declaration form + ## e.g. (a: U, b: V) vs. (a, b: T) + for n, t in paramsIter(params): + inc result + +proc makeType(typeName, params: NimNode): NimNode = + ## Generate type section contains an object definition + ## with fields of handler params + let typeSec = quote do: + type `typeName` = object + + let obj = typeSec[0][2] + let recList = newNimNode(nnkRecList) + if params.len > 1: + for i in 1..= minLength: + # allow both empty and null after mandatory args + # D & E fall into this category + code.add quote do: + if `paramsIdent`.positional.len > `pos` and + `paramKind` != JsonValueKind.Null: + `innerNode` + else: + # allow null param for optional args between/before mandatory args + # B fall into this category + code.add quote do: + if `paramKind` != JsonValueKind.Null: + `innerNode` + else: + # mandatory args + # A and C fall into this category + # unpack Nim type and assign from json + code.add jsonToNim(paramVar, paramType, paramVal, paramName) + +proc makeParams(retType: NimNode, params: NimNode): seq[NimNode] = + ## Convert rpc params into handler params + result.add retType + if params.len > 1: + for i in 1.. 1 # not including return type + (posSetup, minLength) = setupPositional(params, paramsIdent) + handler = makeHandler(handlerName, params, procBody, returnType) + named = setupNamed(paramsObj, paramsIdent, params) + + if hasParams: + setup.add makeType(typeName, params) + setup.add quote do: + var `paramsObj`: `typeName` + + # unpack each parameter and provide assignments + var + pos = 0 + positional = newStmtList() + executeParams: seq[NimNode] + + for paramIdent, paramType in paramsIter(params): + positional.setupPositional(paramsObj, paramsIdent, + paramIdent, paramType, pos, minLength) + executeParams.add quote do: + `paramsObj`.`paramIdent` + inc pos + + if hasParams: + setup.add quote do: + if `paramsIdent`.kind == rpPositional: + `posSetup` + `positional` + else: + `named` + else: + setup.add quote do: + if `paramsIdent`.kind == rpPositional: + `posSetup` + + let + awaitedResult = ident "awaitedResult" + doEncode = quote do: encode(JrpcConv, `awaitedResult`) + maybeWrap = + if returnType.noWrap: awaitedResult + else: ident"StringOfJson".newCall doEncode + executeCall = newCall(handlerName, executeParams) + + result = newStmtList() + result.add handler + result.add quote do: + proc `procWrapper`(`paramsIdent`: RequestParamsRx): Future[StringOfJson] {.async, gcsafe.} = + # Avoid 'yield in expr not lowered' with an intermediate variable. + # See: https://github.com/nim-lang/Nim/issues/17849 + `setup` + let `awaitedResult` = await `executeCall` + return `maybeWrap` diff --git a/json_rpc/private/shared_wrapper.nim b/json_rpc/private/shared_wrapper.nim new file mode 100644 index 00000000..43ad61a4 --- /dev/null +++ b/json_rpc/private/shared_wrapper.nim @@ -0,0 +1,70 @@ +# json-rpc +# Copyright (c) 2024 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + std/[json, macros], + ./jrpc_sys, + ./jrpc_conv + +iterator paramsIter*(params: NimNode): tuple[name, ntype: NimNode] = + ## Forward iterator of handler parameters + for i in 1 ..< params.len: + let arg = params[i] + let argType = arg[^2] + for j in 0 ..< arg.len-2: + yield (arg[j], argType) + +func ensureReturnType*(params: NimNode): NimNode = + let retType = ident"JsonNode" + if params.isNil or params.kind == nnkEmpty or params.len == 0: + return nnkFormalParams.newTree(retType) + + if params.len >= 1 and params[0].kind == nnkEmpty: + params[0] = retType + + params + +func noWrap*(returnType: NimNode): bool = + ## Condition when return type should not be encoded + ## to Json + returnType.repr == "StringOfJson" or + returnType.repr == "JsonString" + +func paramsTx*(params: JsonNode): RequestParamsTx = + if params.kind == JArray: + var args: seq[JsonString] + for x in params: + args.add JrpcConv.encode(x).JsonString + RequestParamsTx( + kind: rpPositional, + positional: system.move(args), + ) + elif params.kind == JObject: + var args: seq[ParamDescNamed] + for k, v in params: + args.add ParamDescNamed( + name: k, + value: JrpcConv.encode(v).JsonString, + ) + RequestParamsTx( + kind: rpNamed, + named: system.move(args), + ) + else: + RequestParamsTx( + kind: rpPositional, + positional: @[JrpcConv.encode(params).JsonString], + ) + +func requestTx*(name: string, params: RequestParamsTx, id: RequestId): RequestTx = + RequestTx( + id: Opt.some(id), + `method`: name, + params: params, + ) diff --git a/json_rpc/router.nim b/json_rpc/router.nim index f6ebf599..2c24fafd 100644 --- a/json_rpc/router.nim +++ b/json_rpc/router.nim @@ -8,135 +8,204 @@ # those terms. import - std/[macros, strutils, tables], - chronicles, chronos, json_serialization/writer, - ./jsonmarshal, ./errors + std/[macros, tables, json], + chronicles, + chronos, + ./private/server_handler_wrapper, + ./private/errors, + ./private/jrpc_sys export - chronos, jsonmarshal + chronos, + jrpc_conv, + json type - StringOfJson* = JsonString - # Procedure signature accepted as an RPC call by server - RpcProc* = proc(input: JsonNode): Future[StringOfJson] {.gcsafe, raises: [Defect].} + RpcProc* = proc(params: RequestParamsRx): Future[StringOfJson] + {.gcsafe, raises: [CatchableError].} RpcRouter* = object procs*: Table[string, RpcProc] const - methodField = "method" - paramsField = "params" - JSON_PARSE_ERROR* = -32700 INVALID_REQUEST* = -32600 METHOD_NOT_FOUND* = -32601 INVALID_PARAMS* = -32602 INTERNAL_ERROR* = -32603 SERVER_ERROR* = -32000 + JSON_ENCODE_ERROR* = -32001 defaultMaxRequestLength* = 1024 * 128 -proc init*(T: type RpcRouter): T = discard +{.push gcsafe, raises: [].} -proc newRpcRouter*: RpcRouter {.deprecated.} = - RpcRouter.init() - -proc register*(router: var RpcRouter, path: string, call: RpcProc) = - router.procs[path] = call +# ------------------------------------------------------------------------------ +# Private helpers +# ------------------------------------------------------------------------------ -proc clear*(router: var RpcRouter) = - router.procs.clear +func invalidRequest(msg: string): ResponseError = + ResponseError(code: INVALID_REQUEST, message: msg) -proc hasMethod*(router: RpcRouter, methodName: string): bool = router.procs.hasKey(methodName) +func methodNotFound(msg: string): ResponseError = + ResponseError(code: METHOD_NOT_FOUND, message: msg) -func isEmpty(node: JsonNode): bool = node.isNil or node.kind == JNull +func serverError(msg: string, data: StringOfJson): ResponseError = + ResponseError(code: SERVER_ERROR, message: msg, data: Opt.some(data)) -# Json reply wrappers +func somethingError(code: int, msg: string): ResponseError = + ResponseError(code: code, message: msg) -# https://www.jsonrpc.org/specification#response_object -proc wrapReply*(id: JsonNode, value: StringOfJson): StringOfJson = - # Success response carries version, id and result fields only - StringOfJson( - """{"jsonrpc":"2.0","id":$1,"result":$2}""" % [$id, string(value)] & "\r\n") +proc validateRequest(router: RpcRouter, req: RequestRx): + Result[RpcProc, ResponseError] = + if req.jsonrpc.isNone: + return invalidRequest("'jsonrpc' missing or invalid").err -proc wrapError*(code: int, msg: string, id: JsonNode = newJNull(), - data: JsonNode = newJNull()): StringOfJson = - # Error reply that carries version, id and error object only - StringOfJson( - """{"jsonrpc":"2.0","id":$1,"error":{"code":$2,"message":$3,"data":$4}}""" % [ - $id, $code, escapeJson(msg), $data - ] & "\r\n") + if req.id.kind == riNull: + return invalidRequest("'id' missing or invalid").err -proc route*(router: RpcRouter, node: JsonNode): Future[StringOfJson] {.async, gcsafe.} = - if node{"jsonrpc"}.getStr() != "2.0": - return wrapError(INVALID_REQUEST, "'jsonrpc' missing or invalid") + if req.meth.isNone: + return invalidRequest("'method' missing or invalid").err - let id = node{"id"} - if id == nil: - return wrapError(INVALID_REQUEST, "'id' missing or invalid") + let + methodName = req.meth.get + rpcProc = router.procs.getOrDefault(methodName) + + if rpcProc.isNil: + return methodNotFound("'" & methodName & + "' is not a registered RPC method").err + + ok(rpcProc) + +proc wrapError(err: ResponseError, id: RequestId): ResponseTx = + ResponseTx( + id: id, + kind: rkError, + error: err, + ) + +proc wrapError(code: int, msg: string, id: RequestId): ResponseTx = + ResponseTx( + id: id, + kind: rkError, + error: somethingError(code, msg), + ) + +proc wrapReply(res: StringOfJson, id: RequestId): ResponseTx = + ResponseTx( + id: id, + kind: rkResult, + result: res, + ) + +proc wrapError(code: int, msg: string): string = + """{"jsonrpc":"2.0","id":null,"error":{"code":""" & $code & + ""","message":""" & escapeJson(msg) & "}}" + +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ - let methodName = node{"method"}.getStr() - if methodName.len == 0: - return wrapError(INVALID_REQUEST, "'method' missing or invalid") +proc init*(T: type RpcRouter): T = discard - let rpcProc = router.procs.getOrDefault(methodName) - let params = node.getOrDefault("params") +proc register*(router: var RpcRouter, path: string, call: RpcProc) + {.gcsafe, raises: [CatchableError].} = + router.procs[path] = call - if rpcProc == nil: - return wrapError(METHOD_NOT_FOUND, "'" & methodName & "' is not a registered RPC method", id) - else: - try: - let res = await rpcProc(if params == nil: newJArray() else: params) - return wrapReply(id, res) - except InvalidRequest as err: - return wrapError(err.code, err.msg, id) - except CatchableError as err: - debug "Error occurred within RPC", methodName = methodName, err = err.msg - return wrapError( - SERVER_ERROR, methodName & " raised an exception", id, newJString(err.msg)) +proc clear*(router: var RpcRouter) = + router.procs.clear -proc route*(router: RpcRouter, data: string): Future[string] {.async, gcsafe.} = - ## Route to RPC from string data. Data is expected to be able to be converted to Json. +proc hasMethod*(router: RpcRouter, methodName: string): bool = + router.procs.hasKey(methodName) + +proc route*(router: RpcRouter, req: RequestRx): + Future[ResponseTx] {.gcsafe, async: (raises: []).} = + let rpcProc = router.validateRequest(req).valueOr: + return wrapError(error, req.id) + + try: + let res = await rpcProc(req.params) + return wrapReply(res, req.id) + except InvalidRequest as err: + return wrapError(err.code, err.msg, req.id) + except CatchableError as err: + let methodName = req.meth.get # this Opt already validated + debug "Error occurred within RPC", + methodName = methodName, err = err.msg + return serverError(methodName & " raised an exception", + escapeJson(err.msg).StringOfJson). + wrapError(req.id) + +proc wrapErrorAsync*(code: int, msg: string): + Future[StringOfJson] {.gcsafe, async: (raises: []).} = + return wrapError(code, msg).StringOfJson + +proc route*(router: RpcRouter, data: string): + Future[string] {.gcsafe, async: (raises: []).} = + ## Route to RPC from string data. Data is expected to be able to be + ## converted to Json. ## Returns string of Json from RPC result/error node when defined(nimHasWarnBareExcept): {.warning[BareExcept]:off.} - let node = - try: parseJson(data) + let request = + try: + JrpcSys.decode(data, RequestRx) except CatchableError as err: - return string(wrapError(JSON_PARSE_ERROR, err.msg)) + return wrapError(JSON_PARSE_ERROR, err.msg) except Exception as err: # TODO https://github.com/status-im/nimbus-eth2/issues/2430 - return string(wrapError(JSON_PARSE_ERROR, err.msg)) + return wrapError(JSON_PARSE_ERROR, err.msg) + + let reply = + try: + let response = await router.route(request) + JrpcSys.encode(response) + except CatchableError as err: + return wrapError(JSON_ENCODE_ERROR, err.msg) + except Exception as err: + return wrapError(JSON_ENCODE_ERROR, err.msg) when defined(nimHasWarnBareExcept): {.warning[BareExcept]:on.} - return string(await router.route(node)) + return reply -proc tryRoute*(router: RpcRouter, data: JsonNode, fut: var Future[StringOfJson]): bool = +proc tryRoute*(router: RpcRouter, data: StringOfJson, + fut: var Future[StringOfJson]): Result[void, string] = ## Route to RPC, returns false if the method or params cannot be found. ## Expects json input and returns json output. - let - jPath = data.getOrDefault(methodField) - jParams = data.getOrDefault(paramsField) - if jPath.isEmpty or jParams.isEmpty: - return false + when defined(nimHasWarnBareExcept): + {.warning[BareExcept]:off.} + {.warning[UnreachableCode]:off.} - let - path = jPath.getStr - rpc = router.procs.getOrDefault(path) - if rpc != nil: - fut = rpc(jParams) - return true - -proc hasReturnType(params: NimNode): bool = - if params != nil and params.len > 0 and params[0] != nil and - params[0].kind != nnkEmpty: - result = true - -macro rpc*(server: RpcRouter, path: string, body: untyped): untyped = + try: + let req = JrpcSys.decode(data.string, RequestRx) + + if req.jsonrpc.isNone: + return err("`jsonrpc` missing or invalid") + + if req.meth.isNone: + return err("`method` missing or invalid") + + let rpc = router.procs.getOrDefault(req.meth.get) + if rpc.isNil: + return err("rpc method not found: " & req.meth.get) + + fut = rpc(req.params) + return ok() + + except CatchableError as ex: + return err(ex.msg) + except Exception as ex: + return err(ex.msg) + + when defined(nimHasWarnBareExcept): + {.warning[BareExcept]:on.} + {.warning[UnreachableCode]:on.} + +macro rpc*(server: RpcRouter, path: static[string], body: untyped): untyped = ## Define a remote procedure call. ## Input and return parameters are defined using the ``do`` notation. ## For example: @@ -146,41 +215,17 @@ macro rpc*(server: RpcRouter, path: string, body: untyped): untyped = ## ``` ## Input parameters are automatically marshalled from json to Nim types, ## and output parameters are automatically marshalled to json for transport. - result = newStmtList() let - parameters = body.findChild(it.kind == nnkFormalParams) - # all remote calls have a single parameter: `params: JsonNode` - paramsIdent = newIdentNode"params" - rpcProcImpl = genSym(nskProc) - rpcProcWrapper = genSym(nskProc) - var - setup = jsonToNim(parameters, paramsIdent) + params = body.findChild(it.kind == nnkFormalParams) procBody = if body.kind == nnkStmtList: body else: body.body + procWrapper = genSym(nskProc, $path & "_rpcWrapper") - let ReturnType = if parameters.hasReturnType: parameters[0] - else: ident "JsonNode" + result = wrapServerHandler($path, params, procBody, procWrapper) - # delegate async proc allows return and setting of result as native type result.add quote do: - proc `rpcProcImpl`(`paramsIdent`: JsonNode): Future[`ReturnType`] {.async.} = - `setup` - `procBody` - - let - awaitedResult = ident "awaitedResult" - doEncode = quote do: encode(JsonRpc, `awaitedResult`) - maybeWrap = - if ReturnType == ident"StringOfJson": doEncode - else: ident"StringOfJson".newCall doEncode - - result.add quote do: - proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} = - # Avoid 'yield in expr not lowered' with an intermediate variable. - # See: https://github.com/nim-lang/Nim/issues/17849 - let `awaitedResult` = await `rpcProcImpl`(`paramsIdent`) - return `maybeWrap` - - `server`.register(`path`, `rpcProcWrapper`) + `server`.register(`path`, `procWrapper`) when defined(nimDumpRpcs): echo "\n", path, ": ", result.repr + +{.pop.} diff --git a/json_rpc/rpcproxy.nim b/json_rpc/rpcproxy.nim index 5ceeb2ee..47f4e109 100644 --- a/json_rpc/rpcproxy.nim +++ b/json_rpc/rpcproxy.nim @@ -7,12 +7,11 @@ # This file may not be copied, modified, or distributed except according to # those terms. -{.push raises: [Defect].} - import pkg/websock/websock, ./servers/[httpserver], - ./clients/[httpclient, websocketclient] + ./clients/[httpclient, websocketclient], + ./private/jrpc_sys type ClientKind* = enum @@ -40,6 +39,8 @@ type compression*: bool flags*: set[TLSFlags] +{.push gcsafe, raises: [].} + # TODO Add validations that provided uri-s are correct https/wss uri and retrun # Result[string, ClientConfig] proc getHttpClientConfig*(uri: string): ClientConfig = @@ -53,9 +54,9 @@ proc getWebSocketClientConfig*( ClientConfig(kind: WebSocket, wsUri: uri, compression: compression, flags: flags) proc proxyCall(client: RpcClient, name: string): RpcProc = - return proc (params: JsonNode): Future[StringOfJson] {.async.} = - let res = await client.call(name, params) - return StringOfJson($res) + return proc (params: RequestParamsRx): Future[StringOfJson] {.gcsafe, async.} = + let res = await client.call(name, params.toTx) + return res proc getClient*(proxy: RpcProxy): RpcClient = case proxy.kind @@ -85,14 +86,14 @@ proc new*( listenAddresses: openArray[TransportAddress], cfg: ClientConfig, authHooks: seq[HttpAuthHook] = @[] -): T {.raises: [Defect, CatchableError].} = +): T {.raises: [CatchableError].} = RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg) proc new*( T: type RpcProxy, listenAddresses: openArray[string], cfg: ClientConfig, - authHooks: seq[HttpAuthHook] = @[]): T {.raises: [Defect, CatchableError].} = + authHooks: seq[HttpAuthHook] = @[]): T {.raises: [CatchableError].} = RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg) proc connectToProxy(proxy: RpcProxy): Future[void] = @@ -125,3 +126,5 @@ proc stop*(proxy: RpcProxy) {.async.} = proc closeWait*(proxy: RpcProxy) {.async.} = await proxy.rpcHttpServer.closeWait() + +{.pop.} diff --git a/json_rpc/server.nim b/json_rpc/server.nim index 22b739d1..35c0ef28 100644 --- a/json_rpc/server.nim +++ b/json_rpc/server.nim @@ -8,21 +8,35 @@ # those terms. import - std/tables, + std/json, chronos, ./router, - ./jsonmarshal + ./private/jrpc_conv, + ./private/jrpc_sys, + ./private/shared_wrapper, + ./private/errors -export chronos, jsonmarshal, router +export + chronos, + jrpc_conv, + router type RpcServer* = ref object of RootRef router*: RpcRouter -proc new(T: type RpcServer): T = +{.push gcsafe, raises: [].} + +# ------------------------------------------------------------------------------ +# Constructors +# ------------------------------------------------------------------------------ + +proc new*(T: type RpcServer): T = T(router: RpcRouter.init()) -proc newRpcServer*(): RpcServer {.deprecated.} = RpcServer.new() +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ template rpc*(server: RpcServer, path: string, body: untyped): untyped = server.router.rpc(path, body) @@ -32,8 +46,23 @@ template hasMethod*(server: RpcServer, methodName: string): bool = proc executeMethod*(server: RpcServer, methodName: string, - args: JsonNode): Future[StringOfJson] = - server.router.procs[methodName](args) + params: RequestParamsTx): Future[StringOfJson] + {.gcsafe, raises: [JsonRpcError].} = + + let + req = requestTx(methodName, params, RequestId(kind: riNumber, num: 0)) + reqData = JrpcSys.encode(req).JsonString + + server.router.tryRoute(reqData, result).isOkOr: + raise newException(JsonRpcError, error) + +proc executeMethod*(server: RpcServer, + methodName: string, + args: JsonNode): Future[StringOfJson] + {.gcsafe, raises: [JsonRpcError].} = + + let params = paramsTx(args) + server.executeMethod(methodName, params) # Wrapper for message processing @@ -42,10 +71,12 @@ proc route*(server: RpcServer, line: string): Future[string] {.gcsafe.} = # Server registration -proc register*(server: RpcServer, name: string, rpc: RpcProc) = +proc register*(server: RpcServer, name: string, rpc: RpcProc) {.gcsafe, raises: [CatchableError].} = ## Add a name/code pair to the RPC server. server.router.register(name, rpc) proc unRegisterAll*(server: RpcServer) = # Remove all remote procedure calls from this server. server.router.clear + +{.pop.} diff --git a/json_rpc/servers/httpserver.nim b/json_rpc/servers/httpserver.nim index f5dd3b00..548cb270 100644 --- a/json_rpc/servers/httpserver.nim +++ b/json_rpc/servers/httpserver.nim @@ -11,9 +11,11 @@ import stew/byteutils, chronicles, httputils, chronos, chronos/apps/http/[httpserver, shttpserver], - ".."/[errors, server] + ../private/errors, + ../server -export server, shttpserver +export + server, shttpserver logScope: topics = "JSONRPC-HTTP-SERVER" @@ -36,41 +38,52 @@ type httpServers: seq[HttpServerRef] authHooks: seq[HttpAuthHook] -proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback = - return proc (req: RequestFence): Future[HttpResponseRef] {.async.} = - if req.isOk(): - let request = req.get() +proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback2 = + return proc (req: RequestFence): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = + if not req.isOk(): + return defaultResponse() - # if hook result is not nil, - # it means we should return immediately + let request = req.get() + # if hook result is not nil, + # it means we should return immediately + try: for hook in rpcServer.authHooks: let res = await hook(request) if not res.isNil: return res - + except CatchableError as exc: + error "Internal error while processing JSON-RPC hook", msg=exc.msg + try: + return await request.respond( + Http503, + "Internal error while processing JSON-RPC hook: " & exc.msg) + except HttpWriteError as exc: + error "Something error", msg=exc.msg + return defaultResponse() + + let + headers = HttpTable.init([("Content-Type", + "application/json; charset=utf-8")]) + try: let body = await request.getBody() - headers = HttpTable.init([("Content-Type", - "application/json; charset=utf-8")]) - - data = - try: - await rpcServer.route(string.fromBytes(body)) - except CancelledError as exc: - raise exc - except CatchableError as exc: - debug "Internal error while processing JSON-RPC call" - return await request.respond( - Http503, - "Internal error while processing JSON-RPC call: " & exc.msg, - headers) + data = await rpcServer.route(string.fromBytes(body)) res = await request.respond(Http200, data, headers) trace "JSON-RPC result has been sent" return res - else: - return dumbResponse() + except CancelledError as exc: + raise exc + except CatchableError as exc: + debug "Internal error while processing JSON-RPC call" + try: + return await request.respond( + Http503, + "Internal error while processing JSON-RPC call: " & exc.msg) + except HttpWriteError as exc: + error "Something error", msg=exc.msg + return defaultResponse() proc addHttpServer*( rpcServer: RpcHttpServer, diff --git a/json_rpc/servers/socketserver.nim b/json_rpc/servers/socketserver.nim index 4b80c899..676a7656 100644 --- a/json_rpc/servers/socketserver.nim +++ b/json_rpc/servers/socketserver.nim @@ -10,7 +10,8 @@ import chronicles, json_serialization/std/net, - ".."/[errors, server] + ../private/errors, + ../server export errors, server @@ -18,26 +19,25 @@ type RpcSocketServer* = ref object of RpcServer servers: seq[StreamServer] -proc sendError*[T](transport: T, code: int, msg: string, id: JsonNode, - data: JsonNode = newJNull()) {.async.} = - ## Send error message to client - let error = wrapError(code, msg, id, data) - result = transport.write(string wrapReply(id, StringOfJson("null"), error)) - -proc processClient(server: StreamServer, transport: StreamTransport) {.async, gcsafe.} = +proc processClient(server: StreamServer, transport: StreamTransport) {.async: (raises: []), gcsafe.} = ## Process transport data to the RPC server - var rpc = getUserData[RpcSocketServer](server) - while true: - var - value = await transport.readLine(defaultMaxRequestLength) - if value == "": - await transport.closeWait() - break - - debug "Processing message", address = transport.remoteAddress(), line = value - - let res = await rpc.route(value) - discard await transport.write(res) + try: + var rpc = getUserData[RpcSocketServer](server) + while true: + var + value = await transport.readLine(defaultMaxRequestLength) + if value == "": + await transport.closeWait() + break + + debug "Processing message", address = transport.remoteAddress(), line = value + + let res = await rpc.route(value) + discard await transport.write(res & "\r\n") + except TransportError as ex: + error "Transport closed during processing client", msg=ex.msg + except CatchableError as ex: + error "Error occured during processing client", msg=ex.msg # Utility functions for setting up servers using stream transport addresses diff --git a/tests/all.nim b/tests/all.nim index eb70851f..b669d870 100644 --- a/tests/all.nim +++ b/tests/all.nim @@ -1,3 +1,12 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + {. warning[UnusedImport]:off .} import @@ -6,4 +15,7 @@ import testhttp, testserverclient, testproxy, - testhook + testhook, + test_jrpc_sys, + test_router_rpc, + test_callsigs diff --git a/tests/helpers.nim b/tests/helpers.nim deleted file mode 100644 index a4c65338..00000000 --- a/tests/helpers.nim +++ /dev/null @@ -1,11 +0,0 @@ -import - ../json_rpc/router - -converter toStr*(value: distinct (string|StringOfJson)): string = string(value) - -template `==`*(a: StringOfJson, b: JsonNode): bool = - parseJson(string a) == b - -template `==`*(a: JsonNode, b: StringOfJson): bool = - a == parseJson(string b) - diff --git a/tests/ethcallsigs.nim b/tests/private/ethcallsigs.nim similarity index 84% rename from tests/ethcallsigs.nim rename to tests/private/ethcallsigs.nim index f72d5072..5d798f01 100644 --- a/tests/ethcallsigs.nim +++ b/tests/private/ethcallsigs.nim @@ -41,23 +41,14 @@ proc eth_getCompilers(): seq[string] proc eth_compileLLL(): seq[byte] proc eth_compileSolidity(): seq[byte] proc eth_compileSerpent(): seq[byte] -proc eth_newFilter(filterOptions: FilterOptions): int proc eth_newBlockFilter(): int proc eth_newPendingTransactionFilter(): int proc eth_uninstallFilter(filterId: int): bool -proc eth_getFilterChanges(filterId: int): seq[LogObject] -proc eth_getFilterLogs(filterId: int): seq[LogObject] -proc eth_getLogs(filterOptions: FilterOptions): seq[LogObject] proc eth_getWork(): seq[UInt256] proc eth_submitWork(nonce: int64, powHash: Uint256, mixDigest: Uint256): bool proc eth_submitHashrate(hashRate: UInt256, id: Uint256): bool proc shh_post(): string -proc shh_version(message: WhisperPost): bool proc shh_newIdentity(): array[60, byte] proc shh_hasIdentity(identity: array[60, byte]): bool proc shh_newGroup(): array[60, byte] -proc shh_addToGroup(identity: array[60, byte]): bool -proc shh_newFilter(filterOptions: FilterOptions, to: array[60, byte], topics: seq[UInt256]): int proc shh_uninstallFilter(id: int): bool -proc shh_getFilterChanges(id: int): seq[WhisperMessage] -proc shh_getMessages(id: int): seq[WhisperMessage] diff --git a/tests/ethhexstrings.nim b/tests/private/ethhexstrings.nim similarity index 74% rename from tests/ethhexstrings.nim rename to tests/private/ethhexstrings.nim index 9c6c82cb..9d6a15f5 100644 --- a/tests/ethhexstrings.nim +++ b/tests/private/ethhexstrings.nim @@ -1,9 +1,14 @@ +import + ../../json_rpc/private/errors + type HexQuantityStr* = distinct string HexDataStr* = distinct string # Hex validation +{.push gcsafe, raises: [].} + template stripLeadingZeros(value: string): string = var cidx = 0 # ignore the last character so we retain '0' on zero value @@ -61,53 +66,55 @@ template hexQuantityStr*(value: string): HexQuantityStr = value.HexQuantityStr # Converters import json -import ../json_rpc/jsonmarshal +import ../../json_rpc/private/jrpc_conv -proc `%`*(value: HexDataStr): JsonNode = +proc `%`*(value: HexDataStr): JsonNode {.gcsafe, raises: [JsonRpcError].} = if not value.validate: - raise newException(ValueError, "HexDataStr: Invalid hex for Ethereum: " & value.string) + raise newException(JsonRpcError, "HexDataStr: Invalid hex for Ethereum: " & value.string) else: result = %(value.string) -proc `%`*(value: HexQuantityStr): JsonNode = +proc `%`*(value: HexQuantityStr): JsonNode {.gcsafe, raises: [JsonRpcError].} = if not value.validate: - raise newException(ValueError, "HexQuantityStr: Invalid hex for Ethereum: " & value.string) + raise newException(JsonRpcError, "HexQuantityStr: Invalid hex for Ethereum: " & value.string) else: result = %(value.string) -proc writeValue*(w: var JsonWriter[JsonRpc], val: HexDataStr) {.raises: [IOError].} = +proc writeValue*(w: var JsonWriter[JrpcConv], val: HexDataStr) {.raises: [IOError].} = writeValue(w, val.string) -proc writeValue*(w: var JsonWriter[JsonRpc], val: HexQuantityStr) {.raises: [IOError].} = +proc writeValue*(w: var JsonWriter[JrpcConv], val: HexQuantityStr) {.raises: [IOError].} = writeValue(w, $val.string) -proc readValue*(r: var JsonReader[JsonRpc], v: var HexDataStr) = +proc readValue*(r: var JsonReader[JrpcConv], v: var HexDataStr) {.gcsafe, raises: [JsonReaderError].} = # Note that '0x' is stripped after validation try: let hexStr = readValue(r, string) if not hexStr.hexDataStr.validate: - raise newException(ValueError, "Value for '" & $v.type & "' is not valid as a Ethereum data \"" & hexStr & "\"") + raise newException(JsonRpcError, "Value for '" & $v.type & "' is not valid as a Ethereum data \"" & hexStr & "\"") v = hexStr[2..hexStr.high].hexDataStr except Exception as err: r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) -proc readValue*(r: var JsonReader[JsonRpc], v: var HexQuantityStr) = +proc readValue*(r: var JsonReader[JrpcConv], v: var HexQuantityStr) {.gcsafe, raises: [JsonReaderError].} = # Note that '0x' is stripped after validation try: let hexStr = readValue(r, string) if not hexStr.hexQuantityStr.validate: - raise newException(ValueError, "Value for '" & $v.type & "' is not valid as a Ethereum data \"" & hexStr & "\"") + raise newException(JsonRpcError, "Value for '" & $v.type & "' is not valid as a Ethereum data \"" & hexStr & "\"") v = hexStr[2..hexStr.high].hexQuantityStr except Exception as err: r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) +{.pop.} + # testing when isMainModule: import unittest suite "Hex quantity": test "Empty string": - expect ValueError: + expect JsonRpcError: let source = "" x = hexQuantityStr source @@ -123,17 +130,17 @@ when isMainModule: x = hexQuantityStr"0x123" check %x == %source test "Missing header": - expect ValueError: + expect JsonRpcError: let source = "1234" x = hexQuantityStr source check %x != %source - expect ValueError: + expect JsonRpcError: let source = "01234" x = hexQuantityStr source check %x != %source - expect ValueError: + expect JsonRpcError: let source = "x1234" x = hexQuantityStr source @@ -146,23 +153,23 @@ when isMainModule: x = hexDataStr source check %x == %source test "Odd length": - expect ValueError: + expect JsonRpcError: let source = "0x123" x = hexDataStr source check %x != %source test "Missing header": - expect ValueError: + expect JsonRpcError: let source = "1234" x = hexDataStr source check %x != %source - expect ValueError: + expect JsonRpcError: let source = "01234" x = hexDataStr source check %x != %source - expect ValueError: + expect JsonRpcError: let source = "x1234" x = hexDataStr source diff --git a/tests/ethprocs.nim b/tests/private/ethprocs.nim similarity index 96% rename from tests/ethprocs.nim rename to tests/private/ethprocs.nim index 5f5a1ee8..471ec53e 100644 --- a/tests/ethprocs.nim +++ b/tests/private/ethprocs.nim @@ -1,6 +1,9 @@ import nimcrypto, stint, - ethtypes, ethhexstrings, stintjson, ../json_rpc/rpcserver + ./ethtypes, + ./ethhexstrings, + ./stintjson, + ../../json_rpc/rpcserver #[ For details on available RPC calls, see: https://github.com/ethereum/wiki/wiki/JSON-RPC @@ -28,6 +31,22 @@ import specified once without invoking `reset`. ]# +EthSend.useDefaultSerializationIn JrpcConv +EthCall.useDefaultSerializationIn JrpcConv +TransactionObject.useDefaultSerializationIn JrpcConv +ReceiptObject.useDefaultSerializationIn JrpcConv +FilterOptions.useDefaultSerializationIn JrpcConv +FilterData.useDefaultSerializationIn JrpcConv +LogObject.useDefaultSerializationIn JrpcConv +WhisperPost.useDefaultSerializationIn JrpcConv +WhisperMessage.useDefaultSerializationIn JrpcConv + +template derefType(): untyped = + var x: BlockObject + typeof(x[]) + +useDefaultSerializationIn(derefType(), JrpcConv) + proc addEthRpcs*(server: RpcServer) = server.rpc("web3_clientVersion") do() -> string: ## Returns the current client version. @@ -51,10 +70,10 @@ proc addEthRpcs*(server: RpcServer) = ## "3": Ropsten Testnet ## "4": Rinkeby Testnet ## "42": Kovan Testnet - #[ Note, See: - https://github.com/ethereum/interfaces/issues/6 - https://github.com/ethereum/EIPs/issues/611 - ]# + ## Note, See: + ## https://github.com/ethereum/interfaces/issues/6 + ## https://github.com/ethereum/EIPs/issues/611 + result = "" server.rpc("net_listening") do() -> bool: @@ -449,4 +468,3 @@ proc addEthRpcs*(server: RpcServer) = ## id: the filter id. ## Returns a list of messages received since last poll. discard - diff --git a/tests/ethtypes.nim b/tests/private/ethtypes.nim similarity index 100% rename from tests/ethtypes.nim rename to tests/private/ethtypes.nim diff --git a/json_rpc/rpcsecureserver.nim b/tests/private/file_callsigs.nim similarity index 62% rename from json_rpc/rpcsecureserver.nim rename to tests/private/file_callsigs.nim index 0be17b75..4e1e1eb5 100644 --- a/json_rpc/rpcsecureserver.nim +++ b/tests/private/file_callsigs.nim @@ -1,5 +1,5 @@ # json-rpc -# Copyright (c) 2019-2023 Status Research & Development GmbH +# Copyright (c) 2024 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) @@ -7,6 +7,4 @@ # This file may not be copied, modified, or distributed except according to # those terms. -import server -import servers/[socketserver, shttpserver] -export server, socketserver, shttpserver \ No newline at end of file +proc shh_uninstallFilter(id: int): bool diff --git a/tests/private/helpers.nim b/tests/private/helpers.nim new file mode 100644 index 00000000..9a612255 --- /dev/null +++ b/tests/private/helpers.nim @@ -0,0 +1,19 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + ../../json_rpc/router + +converter toStr*(value: distinct (string|StringOfJson)): string = string(value) + +template `==`*(a: StringOfJson, b: JsonNode): bool = + parseJson(string a) == b + +template `==`*(a: JsonNode, b: StringOfJson): bool = + a == parseJson(string b) diff --git a/tests/stintjson.nim b/tests/private/stintjson.nim similarity index 60% rename from tests/stintjson.nim rename to tests/private/stintjson.nim index 28e77f65..06832aee 100644 --- a/tests/stintjson.nim +++ b/tests/private/stintjson.nim @@ -1,4 +1,9 @@ -import stint, ../json_rpc/jsonmarshal +import + std/json, + stint, + ../../json_rpc/private/jrpc_conv + +{.push gcsafe, raises: [].} template stintStr(n: UInt256|Int256): JsonNode = var s = n.toHex @@ -10,13 +15,16 @@ proc `%`*(n: UInt256): JsonNode = n.stintStr proc `%`*(n: Int256): JsonNode = n.stintStr -proc writeValue*(w: var JsonWriter[JsonRpc], val: UInt256) = +proc writeValue*(w: var JsonWriter[JrpcConv], val: UInt256) + {.gcsafe, raises: [IOError].} = writeValue(w, val.stintStr) -proc writeValue*(w: var JsonWriter[JsonRpc], val: ref UInt256) = +proc writeValue*(w: var JsonWriter[JrpcConv], val: ref UInt256) + {.gcsafe, raises: [IOError].} = writeValue(w, val[].stintStr) -proc readValue*(r: var JsonReader[JsonRpc], v: var UInt256) = +proc readValue*(r: var JsonReader[JrpcConv], v: var UInt256) + {.gcsafe, raises: [JsonReaderError].} = ## Allows UInt256 to be passed as a json string. ## Expects base 16 string, starting with "0x". try: @@ -27,8 +35,10 @@ proc readValue*(r: var JsonReader[JsonRpc], v: var UInt256) = except Exception as err: r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) -proc readValue*(r: var JsonReader[JsonRpc], v: var ref UInt256) = +proc readValue*(r: var JsonReader[JrpcConv], v: var ref UInt256) + {.gcsafe, raises: [JsonReaderError].} = ## Allows ref UInt256 to be passed as a json string. ## Expects base 16 string, starting with "0x". readValue(r, v[]) +{.pop.} diff --git a/tests/test_callsigs.nim b/tests/test_callsigs.nim new file mode 100644 index 00000000..e5162045 --- /dev/null +++ b/tests/test_callsigs.nim @@ -0,0 +1,26 @@ +# json-rpc +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + ../json_rpc/client + +from os import getCurrentDir, DirSep +from strutils import rsplit +template sourceDir: string = currentSourcePath.rsplit(DirSep, 1)[0] + +createRpcSigs(RpcClient, sourceDir & "/private/file_callsigs.nim") + +createSingleRpcSig(RpcClient, "bottle"): + proc get_Bottle(id: int): bool + +createRpcSigsFromNim(RpcClient): + proc get_Banana(id: int): bool + proc get_Combo(id, index: int, name: string): bool + proc get_Name(id: int): string + proc getJsonString(name: string): JsonString diff --git a/tests/test_jrpc_sys.nim b/tests/test_jrpc_sys.nim new file mode 100644 index 00000000..44e76ad5 --- /dev/null +++ b/tests/test_jrpc_sys.nim @@ -0,0 +1,246 @@ +# json-rpc +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + unittest2, + ../json_rpc/private/jrpc_sys + +func id(): RequestId = + RequestId(kind: riNull) + +func id(x: string): RequestId = + RequestId(kind: riString, str: x) + +func id(x: int): RequestId = + RequestId(kind: riNumber, num: x) + +func req(id: int or string, meth: string, params: RequestParamsTx): RequestTx = + RequestTx( + id: Opt.some(id(id)), + `method`: meth, + params: params + ) + +func reqNull(meth: string, params: RequestParamsTx): RequestTx = + RequestTx( + id: Opt.some(id()), + `method`: meth, + params: params + ) + +func reqNoId(meth: string, params: RequestParamsTx): RequestTx = + RequestTx( + `method`: meth, + params: params + ) + +func toParams(params: varargs[(string, JsonString)]): seq[ParamDescNamed] = + for x in params: + result.add ParamDescNamed(name:x[0], value:x[1]) + +func namedPar(params: varargs[(string, JsonString)]): RequestParamsTx = + RequestParamsTx( + kind: rpNamed, + named: toParams(params) + ) + +func posPar(params: varargs[JsonString]): RequestParamsTx = + RequestParamsTx( + kind: rpPositional, + positional: @params + ) + +func res(id: int or string, r: JsonString): ResponseTx = + ResponseTx( + id: id(id), + kind: rkResult, + result: r, + ) + +func res(id: int or string, err: ResponseError): ResponseTx = + ResponseTx( + id: id(id), + kind: rkError, + error: err, + ) + +func resErr(code: int, msg: string): ResponseError = + ResponseError( + code: code, + message: msg, + ) + +func resErr(code: int, msg: string, data: JsonString): ResponseError = + ResponseError( + code: code, + message: msg, + data: Opt.some(data) + ) + +func reqBatch(args: varargs[RequestTx]): RequestBatchTx = + if args.len == 1: + RequestBatchTx( + kind: rbkSingle, single: args[0] + ) + else: + RequestBatchTx( + kind: rbkMany, many: @args + ) + +func resBatch(args: varargs[ResponseTx]): ResponseBatchTx = + if args.len == 1: + ResponseBatchTx( + kind: rbkSingle, single: args[0] + ) + else: + ResponseBatchTx( + kind: rbkMany, many: @args + ) + +suite "jrpc_sys conversion": + let np1 = namedPar(("banana", JsonString("true")), ("apple", JsonString("123"))) + let pp1 = posPar(JsonString("123"), JsonString("true"), JsonString("\"hello\"")) + + test "RequestTx -> RequestRx: id(int), positional": + let tx = req(123, "int_positional", pp1) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, RequestRx) + + check: + rx.jsonrpc.isSome + rx.id.kind == riNumber + rx.id.num == 123 + rx.meth.get == "int_positional" + rx.params.kind == rpPositional + rx.params.positional.len == 3 + rx.params.positional[0].kind == JsonValueKind.Number + rx.params.positional[1].kind == JsonValueKind.Bool + rx.params.positional[2].kind == JsonValueKind.String + + test "RequestTx -> RequestRx: id(string), named": + let tx = req("word", "string_named", np1) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, RequestRx) + + check: + rx.jsonrpc.isSome + rx.id.kind == riString + rx.id.str == "word" + rx.meth.get == "string_named" + rx.params.kind == rpNamed + rx.params.named[0].name == "banana" + rx.params.named[0].value.string == "true" + rx.params.named[1].name == "apple" + rx.params.named[1].value.string == "123" + + test "RequestTx -> RequestRx: id(null), named": + let tx = reqNull("null_named", np1) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, RequestRx) + + check: + rx.jsonrpc.isSome + rx.id.kind == riNull + rx.meth.get == "null_named" + rx.params.kind == rpNamed + rx.params.named[0].name == "banana" + rx.params.named[0].value.string == "true" + rx.params.named[1].name == "apple" + rx.params.named[1].value.string == "123" + + test "RequestTx -> RequestRx: none, none": + let tx = reqNoId("none_positional", posPar()) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, RequestRx) + + check: + rx.jsonrpc.isSome + rx.id.kind == riNull + rx.meth.get == "none_positional" + rx.params.kind == rpPositional + rx.params.positional.len == 0 + + test "ResponseTx -> ResponseRx: id(int), res": + let tx = res(777, JsonString("true")) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, ResponseRx) + check: + rx.jsonrpc.isSome + rx.id.isSome + rx.id.get.num == 777 + rx.result.isSome + rx.result.get == JsonString("true") + rx.error.isNone + + test "ResponseTx -> ResponseRx: id(string), err: nodata": + let tx = res("gum", resErr(999, "fatal")) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, ResponseRx) + check: + rx.jsonrpc.isSome + rx.id.isSome + rx.id.get.str == "gum" + rx.result.isNone + rx.error.isSome + rx.error.get.code == 999 + rx.error.get.message == "fatal" + rx.error.get.data.isNone + + test "ResponseTx -> ResponseRx: id(string), err: some data": + let tx = res("gum", resErr(999, "fatal", JsonString("888.999"))) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, ResponseRx) + check: + rx.jsonrpc.isSome + rx.id.isSome + rx.id.get.str == "gum" + rx.result.isNone + rx.error.isSome + rx.error.get.code == 999 + rx.error.get.message == "fatal" + rx.error.get.data.get == JsonString("888.999") + + test "RequestBatchTx -> RequestBatchRx: single": + let tx1 = req(123, "int_positional", pp1) + let tx = reqBatch(tx1) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, RequestBatchRx) + check: + rx.kind == rbkSingle + + test "RequestBatchTx -> RequestBatchRx: many": + let tx1 = req(123, "int_positional", pp1) + let tx2 = req("word", "string_named", np1) + let tx3 = reqNull("null_named", np1) + let tx4 = reqNoId("none_positional", posPar()) + let tx = reqBatch(tx1, tx2, tx3, tx4) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, RequestBatchRx) + check: + rx.kind == rbkMany + rx.many.len == 4 + + test "ResponseBatchTx -> ResponseBatchRx: single": + let tx1 = res(777, JsonString("true")) + let tx = resBatch(tx1) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, ResponseBatchRx) + check: + rx.kind == rbkSingle + + test "ResponseBatchTx -> ResponseBatchRx: many": + let tx1 = res(777, JsonString("true")) + let tx2 = res("gum", resErr(999, "fatal")) + let tx3 = res("gum", resErr(999, "fatal", JsonString("888.999"))) + let tx = resBatch(tx1, tx2, tx3) + let txBytes = JrpcSys.encode(tx) + let rx = JrpcSys.decode(txBytes, ResponseBatchRx) + check: + rx.kind == rbkMany + rx.many.len == 3 diff --git a/tests/test_router_rpc.nim b/tests/test_router_rpc.nim new file mode 100644 index 00000000..71824aad --- /dev/null +++ b/tests/test_router_rpc.nim @@ -0,0 +1,97 @@ +import + unittest2, + ../json_rpc/router, + json_serialization/stew/results, + json_serialization/std/options + +var server = RpcRouter() + +server.rpc("optional") do(A: int, B: Option[int], C: string, D: Option[int], E: Option[string]) -> string: + var res = "A: " & $A + res.add ", B: " & $B.get(99) + res.add ", C: " & C + res.add ", D: " & $D.get(77) + res.add ", E: " & E.get("none") + return res + +server.rpc("noParams") do() -> int: + return 123 + +server.rpc("emptyParams"): + return %777 + +server.rpc("comboParams") do(a, b, c: int) -> int: + return a+b+c + +server.rpc("returnJsonString") do(a, b, c: int) -> JsonString: + return JsonString($(a+b+c)) + +func req(meth: string, params: string): string = + """{"jsonrpc":"2.0", "id":0, "method": """ & + "\"" & meth & "\", \"params\": " & params & "}" + +suite "rpc router": + test "no params": + let n = req("noParams", "[]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":123}""" + + test "no params with params": + let n = req("noParams", "[123]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"noParams raised an exception","data":"Expected 0 Json parameter(s) but got 1"}}""" + + test "optional B E, positional": + let n = req("optional", "[44, null, \"apple\", 33]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 99, C: apple, D: 33, E: none"}""" + + test "optional B D E, positional": + let n = req("optional", "[44, null, \"apple\"]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 99, C: apple, D: 77, E: none"}""" + + test "optional D E, positional": + let n = req("optional", "[44, 567, \"apple\"]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 567, C: apple, D: 77, E: none"}""" + + test "optional D wrong E, positional": + let n = req("optional", "[44, 567, \"apple\", \"banana\"]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"optional raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"}}""" + + test "optional D extra, positional": + let n = req("optional", "[44, 567, \"apple\", 999, \"banana\", true]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 567, C: apple, D: 999, E: banana"}""" + + test "optional B D E, named": + let n = req("optional", """{"A": 33, "C":"banana" }""") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 33, B: 99, C: banana, D: 77, E: none"}""" + + test "optional B E, D front, named": + let n = req("optional", """{"D": 8887, "A": 33, "C":"banana" }""") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 33, B: 99, C: banana, D: 8887, E: none"}""" + + test "optional B E, D front, extra X, named": + let n = req("optional", """{"D": 8887, "X": false , "A": 33, "C":"banana"}""") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":"A: 33, B: 99, C: banana, D: 8887, E: none"}""" + + test "empty params": + let n = req("emptyParams", "[]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":777}""" + + test "combo params": + let n = req("comboParams", "[6,7,8]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":21}""" + + test "return json string": + let n = req("returnJsonString", "[6,7,8]") + let res = waitFor server.route(n) + check res == """{"jsonrpc":"2.0","id":0,"result":21}""" diff --git a/tests/testethcalls.nim b/tests/testethcalls.nim index 14e198b1..f736c65f 100644 --- a/tests/testethcalls.nim +++ b/tests/testethcalls.nim @@ -1,7 +1,20 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2, tables, - stint, ethtypes, ethprocs, stintjson, chronicles, - ../json_rpc/[rpcclient, rpcserver], ./helpers + stint, chronicles, + ../json_rpc/[rpcclient, rpcserver], + ./private/helpers, + ./private/ethtypes, + ./private/ethprocs, + ./private/stintjson from os import getCurrentDir, DirSep from strutils import rsplit @@ -15,7 +28,7 @@ var server.addEthRpcs() ## Generate client convenience marshalling wrappers from forward declarations -createRpcSigs(RpcSocketClient, sourceDir & DirSep & "ethcallsigs.nim") +createRpcSigs(RpcSocketClient, sourceDir & "/private/ethcallsigs.nim") func rpcDynamicName(name: string): string = "rpc." & name @@ -38,7 +51,7 @@ proc testLocalCalls: Future[seq[StringOfJson]] = returnUint256 = server.executeMethod("rpc.testReturnUint256", %[]) return all(uint256Param, returnUint256) -proc testRemoteUInt256: Future[seq[Response]] = +proc testRemoteUInt256: Future[seq[StringOfJson]] = ## Call function remotely on server, testing `stint` types let uint256Param = client.call("rpc.uint256Param", %[%"0x1234567890"]) diff --git a/tests/testhook.nim b/tests/testhook.nim index ddb9d57c..1502ab7b 100644 --- a/tests/testhook.nim +++ b/tests/testhook.nim @@ -1,10 +1,19 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2, websock/websock, ../json_rpc/[rpcclient, rpcserver] const - serverHost = "localhost" + serverHost = "127.0.0.1" serverPort = 8547 serverAddress = serverHost & ":" & $serverPort @@ -31,18 +40,19 @@ suite "HTTP server hook test": waitFor client.connect(serverHost, Port(serverPort), false) expect ErrorResponse: let r = waitFor client.call("testHook", %[%"abc"]) + discard r test "good auth token": let client = newRpcHttpClient(getHeaders = authHeaders) waitFor client.connect(serverHost, Port(serverPort), false) let r = waitFor client.call("testHook", %[%"abc"]) - check r.getStr == "Hello abc" + check r.string == "\"Hello abc\"" waitFor srv.closeWait() proc wsAuthHeaders(ctx: Hook, headers: var HttpTable): Result[void, string] - {.gcsafe, raises: [Defect].} = + {.gcsafe, raises: [].} = headers.add("Auth-Token", "Good Token") return ok() @@ -80,7 +90,7 @@ suite "Websocket server hook test": test "good auth token": waitFor client.connect("ws://127.0.0.1:8545/", hooks = @[hook]) let r = waitFor client.call("testHook", %[%"abc"]) - check r.getStr == "Hello abc" + check r.string == "\"Hello abc\"" srv.stop() waitFor srv.closeWait() diff --git a/tests/testhttp.nim b/tests/testhttp.nim index 973276b3..b91af3e9 100644 --- a/tests/testhttp.nim +++ b/tests/testhttp.nim @@ -1,3 +1,12 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import unittest2 import ../json_rpc/[rpcserver, rpcclient] @@ -7,7 +16,7 @@ proc simpleTest(address: string, port: Port): Future[bool] {.async.} = var client = newRpcHttpClient() await client.connect(address, port, secure = false) var r = await client.call("noParamsProc", %[]) - if r.getStr == "Hello world": + if r.string == "\"Hello world\"": result = true proc continuousTest(address: string, port: Port): Future[int] {.async.} = @@ -16,7 +25,7 @@ proc continuousTest(address: string, port: Port): Future[int] {.async.} = for i in 0.. high(MyEnum).int: + r.raiseUnexpectedValue("invalid enum range " & $intVal) + val = MyEnum(intVal) + let testObj = %*{ "a": %1, @@ -38,7 +64,7 @@ let }, "c": %1.0} -var s = newRpcSocketServer(["localhost:8545"]) +var s = newRpcSocketServer(["127.0.0.1:8545"]) # RPC definitions s.rpc("rpc.simplePath"): @@ -100,6 +126,8 @@ type d: Option[int] e: Option[string] +OptionalFields.useDefaultSerializationIn JrpcConv + s.rpc("rpc.mixedOptionalArg") do(a: int, b: Option[int], c: string, d: Option[int], e: Option[string]) -> OptionalFields: @@ -125,6 +153,8 @@ type o2: Option[bool] o3: Option[bool] +MaybeOptions.useDefaultSerializationIn JrpcConv + s.rpc("rpc.optInObj") do(data: string, options: Option[MaybeOptions]) -> int: if options.isSome: let o = options.get @@ -155,10 +185,10 @@ suite "Server types": test "Enum param paths": block: - let r = waitFor s.executeMethod("rpc.enumParam", %[(int64(Enum1))]) + let r = waitFor s.executeMethod("rpc.enumParam", %[%int64(Enum1)]) check r == "[\"Enum1\"]" - expect(ValueError): + expect(JsonRpcError): discard waitFor s.executeMethod("rpc.enumParam", %[(int64(42))]) test "Different param types": @@ -201,30 +231,30 @@ suite "Server types": inp2 = MyOptional() r1 = waitFor s.executeMethod("rpc.optional", %[%inp1]) r2 = waitFor s.executeMethod("rpc.optional", %[%inp2]) - check r1 == JsonRpc.encode inp1 - check r2 == JsonRpc.encode inp2 + check r1.string == JrpcConv.encode inp1 + check r2.string == JrpcConv.encode inp2 test "Return statement": let r = waitFor s.executeMethod("rpc.testReturns", %[]) - check r == JsonRpc.encode 1234 + check r == JrpcConv.encode 1234 test "Runtime errors": - expect ValueError: + expect JsonRpcError: # root param not array discard waitFor s.executeMethod("rpc.arrayParam", %"test") - expect ValueError: + expect JsonRpcError: # too big for array discard waitFor s.executeMethod("rpc.arrayParam", %[%[0, 1, 2, 3, 4, 5, 6], %"hello"]) - expect ValueError: + expect JsonRpcError: # wrong sub parameter type discard waitFor s.executeMethod("rpc.arrayParam", %[%"test", %"hello"]) - expect ValueError: + expect JsonRpcError: # wrong param type discard waitFor s.executeMethod("rpc.differentParams", %[%"abc", %1]) test "Multiple variables of one type": let r = waitFor s.executeMethod("rpc.multiVarsOfOneType", %[%"hello", %"world"]) - check r == JsonRpc.encode "hello world" + check r == JrpcConv.encode "hello world" test "Optional arg": let @@ -233,37 +263,37 @@ suite "Server types": r1 = waitFor s.executeMethod("rpc.optionalArg", %[%117, %int1]) r2 = waitFor s.executeMethod("rpc.optionalArg", %[%117]) r3 = waitFor s.executeMethod("rpc.optionalArg", %[%117, newJNull()]) - check r1 == JsonRpc.encode int1 - check r2 == JsonRpc.encode int2 - check r3 == JsonRpc.encode int2 + check r1 == JrpcConv.encode int1 + check r2 == JrpcConv.encode int2 + check r3 == JrpcConv.encode int2 test "Optional arg2": let r1 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B"]) - check r1 == JsonRpc.encode "AB" + check r1 == JrpcConv.encode "AB" let r2 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull()]) - check r2 == JsonRpc.encode "AB" + check r2 == JrpcConv.encode "AB" let r3 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), newJNull()]) - check r3 == JsonRpc.encode "AB" + check r3 == JrpcConv.encode "AB" let r4 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), %"D"]) - check r4 == JsonRpc.encode "ABD" + check r4 == JrpcConv.encode "ABD" let r5 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", %"D"]) - check r5 == JsonRpc.encode "ABCD" + check r5 == JrpcConv.encode "ABCD" let r6 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", newJNull()]) - check r6 == JsonRpc.encode "ABC" + check r6 == JrpcConv.encode "ABC" let r7 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C"]) - check r7 == JsonRpc.encode "ABC" + check r7 == JrpcConv.encode "ABC" test "Mixed optional arg": var ax = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, %11, %"hello", %12, %"world"]) - check ax == JsonRpc.encode OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world")) + check ax == JrpcConv.encode OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world")) var bx = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, newJNull(), %"hello"]) - check bx == JsonRpc.encode OptionalFields(a: 10, c: "hello") + check bx == JrpcConv.encode OptionalFields(a: 10, c: "hello") test "Non-built-in optional types": let @@ -271,33 +301,33 @@ suite "Server types": testOpts1 = MyOptionalNotBuiltin(val: some(t2)) testOpts2 = MyOptionalNotBuiltin() var r = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts1]) - check r == JsonRpc.encode t2.y + check r == JrpcConv.encode t2.y var r2 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[]) - check r2 == JsonRpc.encode "Empty1" + check r2 == JrpcConv.encode "Empty1" var r3 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts2]) - check r3 == JsonRpc.encode "Empty2" + check r3 == JrpcConv.encode "Empty2" test "Manually set up JSON for optionals": # Check manual set up json with optionals let opts1 = parseJson("""{"o1": true}""") var r1 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts1]) - check r1 == JsonRpc.encode 1 + check r1 == JrpcConv.encode 1 let opts2 = parseJson("""{"o2": true}""") var r2 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts2]) - check r2 == JsonRpc.encode 2 + check r2 == JrpcConv.encode 2 let opts3 = parseJson("""{"o3": true}""") var r3 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts3]) - check r3 == JsonRpc.encode 4 + check r3 == JrpcConv.encode 4 # Combinations let opts4 = parseJson("""{"o1": true, "o3": true}""") var r4 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts4]) - check r4 == JsonRpc.encode 5 + check r4 == JrpcConv.encode 5 let opts5 = parseJson("""{"o2": true, "o3": true}""") var r5 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts5]) - check r5 == JsonRpc.encode 6 + check r5 == JrpcConv.encode 6 let opts6 = parseJson("""{"o1": true, "o2": true}""") var r6 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts6]) - check r6 == JsonRpc.encode 3 + check r6 == JrpcConv.encode 3 s.stop() waitFor s.closeWait() diff --git a/tests/testserverclient.nim b/tests/testserverclient.nim index 6c17176d..c86337de 100644 --- a/tests/testserverclient.nim +++ b/tests/testserverclient.nim @@ -1,5 +1,14 @@ +# json-rpc +# Copyright (c) 2019-2023 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + import - unittest2, + unittest2, ../json_rpc/[rpcclient, rpcserver] # Create RPC on server @@ -14,16 +23,16 @@ proc setupServer*(srv: RpcServer) = raise (ref InvalidRequest)(code: -32001, msg: "Unknown payload") suite "Socket Server/Client RPC": - var srv = newRpcSocketServer(["localhost:8545"]) + var srv = newRpcSocketServer(["127.0.0.1:8545"]) var client = newRpcSocketClient() srv.setupServer() srv.start() - waitFor client.connect("localhost", Port(8545)) + waitFor client.connect("127.0.0.1", Port(8545)) test "Successful RPC call": let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) - check r.getStr == "Hello abc data: [1, 2, 3, 4]" + check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" test "Missing params": expect(CatchableError): @@ -38,7 +47,7 @@ suite "Socket Server/Client RPC": discard waitFor client.call("invalidRequest", %[]) check false except CatchableError as e: - check e.msg == """{"code":-32001,"message":"Unknown payload","data":null}""" + check e.msg == """{"code":-32001,"message":"Unknown payload"}""" srv.stop() waitFor srv.closeWait() @@ -53,7 +62,7 @@ suite "Websocket Server/Client RPC": test "Successful RPC call": let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) - check r.getStr == "Hello abc data: [1, 2, 3, 4]" + check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" test "Missing params": expect(CatchableError): @@ -68,7 +77,7 @@ suite "Websocket Server/Client RPC": discard waitFor client.call("invalidRequest", %[]) check false except CatchableError as e: - check e.msg == """{"code":-32001,"message":"Unknown payload","data":null}""" + check e.msg == """{"code":-32001,"message":"Unknown payload"}""" srv.stop() waitFor srv.closeWait() @@ -85,7 +94,7 @@ suite "Websocket Server/Client RPC with Compression": test "Successful RPC call": let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]]) - check r.getStr == "Hello abc data: [1, 2, 3, 4]" + check r.string == "\"Hello abc data: [1, 2, 3, 4]\"" test "Missing params": expect(CatchableError): @@ -100,7 +109,8 @@ suite "Websocket Server/Client RPC with Compression": discard waitFor client.call("invalidRequest", %[]) check false except CatchableError as e: - check e.msg == """{"code":-32001,"message":"Unknown payload","data":null}""" + check e.msg == """{"code":-32001,"message":"Unknown payload"}""" srv.stop() waitFor srv.closeWait() +