Improve both client and server resilience against fields and elements with null value (#195)

* Improve resilience against null fields

* Fix client processMessage when handling error

* Improve both client and server resilience against fields and elements with null value
This commit is contained in:
andri lim 2024-01-17 14:10:05 +07:00 committed by GitHub
parent b6d068f489
commit 8d79d52841
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 78 additions and 14 deletions

View File

@ -89,16 +89,20 @@ proc processMessage*(client: RpcClient, line: string): Result[void, string] =
var requestFut: Future[JsonString] var requestFut: Future[JsonString]
let id = response.id.get let id = response.id.get
if not client.awaiting.pop(id, requestFut): if not client.awaiting.pop(id, requestFut):
return err("Cannot find message id \"" & $id & "\"") let msg = "Cannot find message id \"" & $id & "\":"
requestFut.fail(newException(JsonRpcError, msg))
return ok()
if response.error.isSome: if response.error.isSome:
let error = JrpcSys.encode(response.error.get) let error = JrpcSys.encode(response.error.get)
requestFut.fail(newException(JsonRpcError, error)) requestFut.fail(newException(JsonRpcError, error))
return ok() return ok()
# Up to this point, the result should contains something # Up to this point, the result should contains something
if response.result.string.len == 0: if response.result.string.len == 0:
return err("missing or invalid response result") let msg = "missing or invalid response result"
requestFut.fail(newException(JsonRpcError, msg))
return ok()
requestFut.complete(response.result) requestFut.complete(response.result)
return ok() return ok()

View File

@ -14,7 +14,11 @@ export
json_serialization json_serialization
createJsonFlavor JrpcConv, createJsonFlavor JrpcConv,
requireAllFields = false automaticObjectSerialization = false,
requireAllFields = false,
omitOptionalFields = true, # Skip optional fields==none in Writer
allowUnknownFields = true,
skipNullFields = true # Skip optional fields==null in Reader
# JrpcConv is a namespace/flavor for encoding and decoding # JrpcConv is a namespace/flavor for encoding and decoding
# parameters and return value of a rpc method. # parameters and return value of a rpc method.

View File

@ -141,11 +141,14 @@ type
# don't mix the json-rpc system encoding with the # don't mix the json-rpc system encoding with the
# actual response/params encoding # actual response/params encoding
createJsonFlavor JrpcSys, createJsonFlavor JrpcSys,
requireAllFields = false automaticObjectSerialization = false,
requireAllFields = false,
omitOptionalFields = true, # Skip optional fields==none in Writer
allowUnknownFields = true,
skipNullFields = true # Skip optional fields==null in Reader
ResponseError.useDefaultSerializationIn JrpcSys ResponseError.useDefaultSerializationIn JrpcSys
RequestTx.useDefaultWriterIn JrpcSys RequestTx.useDefaultWriterIn JrpcSys
ResponseRx.useDefaultReaderIn JrpcSys
RequestRx.useDefaultReaderIn JrpcSys RequestRx.useDefaultReaderIn JrpcSys
const const
@ -253,6 +256,18 @@ proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseTx)
w.writeField("error", val.error) w.writeField("error", val.error)
w.endRecord() w.endRecord()
proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseRx)
{.gcsafe, raises: [IOError, SerializationError].} =
# We need to overload ResponseRx reader because
# we don't want to skip null fields
r.parseObjectWithoutSkip(key):
case key
of "jsonrpc": r.readValue(val.jsonrpc)
of "id" : r.readValue(val.id)
of "result" : val.result = r.parseAsString()
of "error" : r.readValue(val.error)
else: discard
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestBatchTx) proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestBatchTx)
{.gcsafe, raises: [IOError].} = {.gcsafe, raises: [IOError].} =
if val.kind == rbkMany: if val.kind == rbkMany:

View File

@ -149,11 +149,10 @@ proc setupPositional(code: NimNode;
`paramsIdent`.positional[`pos`].kind `paramsIdent`.positional[`pos`].kind
paramVar = quote do: paramVar = quote do:
`paramsObj`.`paramIdent` `paramsObj`.`paramIdent`
innerNode = jsonToNim(paramVar, paramType, paramVal, paramName)
# e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string]) # e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string])
if paramType.isOptionalArg: if paramType.isOptionalArg:
let
innerNode = jsonToNim(paramVar, paramType, paramVal, paramName)
if pos >= minLength: if pos >= minLength:
# allow both empty and null after mandatory args # allow both empty and null after mandatory args
# D & E fall into this category # D & E fall into this category
@ -171,7 +170,9 @@ proc setupPositional(code: NimNode;
# mandatory args # mandatory args
# A and C fall into this category # A and C fall into this category
# unpack Nim type and assign from json # unpack Nim type and assign from json
code.add jsonToNim(paramVar, paramType, paramVal, paramName) code.add quote do:
if `paramKind` != JsonValueKind.Null:
`innerNode`
proc makeParams(retType: NimNode, params: NimNode): seq[NimNode] = proc makeParams(retType: NimNode, params: NimNode): seq[NimNode] =
## Convert rpc params into handler params ## Convert rpc params into handler params

View File

@ -64,6 +64,19 @@ proc executeMethod*(server: RpcServer,
let params = paramsTx(args) let params = paramsTx(args)
server.executeMethod(methodName, params) server.executeMethod(methodName, params)
proc executeMethod*(server: RpcServer,
methodName: string,
args: JsonString): Future[JsonString]
{.gcsafe, raises: [JsonRpcError].} =
let params = try:
let x = JrpcSys.decode(args.string, RequestParamsRx)
x.toTx
except SerializationError as exc:
raise newException(JsonRpcError, exc.msg)
server.executeMethod(methodName, params)
# Wrapper for message processing # Wrapper for message processing
proc route*(server: RpcServer, line: string): Future[string] {.gcsafe.} = proc route*(server: RpcServer, line: string): Future[string] {.gcsafe.} =

View File

@ -244,3 +244,12 @@ suite "jrpc_sys conversion":
check: check:
rx.kind == rbkMany rx.kind == rbkMany
rx.many.len == 3 rx.many.len == 3
test "skip null value":
let jsonBytes = """{"jsonrpc":null, "id":null, "method":null, "params":null}"""
let x = JrpcSys.decode(jsonBytes, RequestRx)
check:
x.jsonrpc.isNone
x.id.kind == riNull
x.`method`.isNone
x.params.kind == rpPositional

View File

@ -39,11 +39,16 @@ type
Enum0 Enum0
Enum1 Enum1
MuscleCar = object
color: string
wheel: int
MyObject.useDefaultSerializationIn JrpcConv MyObject.useDefaultSerializationIn JrpcConv
Test.useDefaultSerializationIn JrpcConv Test.useDefaultSerializationIn JrpcConv
Test2.useDefaultSerializationIn JrpcConv Test2.useDefaultSerializationIn JrpcConv
MyOptional.useDefaultSerializationIn JrpcConv MyOptional.useDefaultSerializationIn JrpcConv
MyOptionalNotBuiltin.useDefaultSerializationIn JrpcConv MyOptionalNotBuiltin.useDefaultSerializationIn JrpcConv
MuscleCar.useDefaultSerializationIn JrpcConv
proc readValue*(r: var JsonReader[JrpcConv], val: var MyEnum) proc readValue*(r: var JsonReader[JrpcConv], val: var MyEnum)
{.gcsafe, raises: [IOError, SerializationError].} = {.gcsafe, raises: [IOError, SerializationError].} =
@ -118,6 +123,9 @@ s.rpc("rpc.optionalArg2") do(a, b: string, c, d: Option[string]) -> string:
if d.isSome: ret.add d.get() if d.isSome: ret.add d.get()
return ret return ret
s.rpc("echo") do(car: MuscleCar) -> JsonString:
return JrpcConv.encode(car).JsonString
type type
OptionalFields = object OptionalFields = object
a: int a: int
@ -345,12 +353,22 @@ suite "Server types":
r1 = waitFor s.executeMethod("rpc.optionalStringArg", %[%data]) r1 = waitFor s.executeMethod("rpc.optionalStringArg", %[%data])
r2 = waitFor s.executeMethod("rpc.optionalStringArg", %[]) r2 = waitFor s.executeMethod("rpc.optionalStringArg", %[])
r3 = waitFor s.executeMethod("rpc.optionalStringArg", %[newJNull()]) r3 = waitFor s.executeMethod("rpc.optionalStringArg", %[newJNull()])
echo r1
echo r2
echo r3
check r1 == %data.get() check r1 == %data.get()
check r2 == %"nope" check r2 == %"nope"
check r3 == %"nope" check r3 == %"nope"
test "Null object fields":
let r = waitFor s.executeMethod("echo", """{"car":{"color":"red","wheel":null}}""".JsonString)
check r == """{"color":"red","wheel":0}"""
let x = waitFor s.executeMethod("echo", """{"car":{"color":null,"wheel":77}}""".JsonString)
check x == """{"color":"","wheel":77}"""
let y = waitFor s.executeMethod("echo", """{"car":null}""".JsonString)
check y == """{"color":"","wheel":0}"""
let z = waitFor s.executeMethod("echo", "[null]".JsonString)
check z == """{"color":"","wheel":0}"""
s.stop() s.stop()
waitFor s.closeWait() waitFor s.closeWait()