Skip to content

Commit 331fbab

Browse files
authored
Terminate conn on invalid response id & don't send a response for it (#276)
Changes: - Terminate bidi conn on invalid response `id`. - Avoid sending an "invalid request" response on invalid response `id`. It should only be done on ambiguous messages before terminating.
1 parent 857d482 commit 331fbab

4 files changed

Lines changed: 86 additions & 89 deletions

File tree

json_rpc/client.nim

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,23 +133,24 @@ proc callOnProcessMessage*(
133133

134134
const defaultRouter = default(RpcRouter)
135135

136-
proc singleResponseId(resp: ResponseRx2): int {.raises: [JsonRpcError].} =
136+
proc singleResponseId(resp: ResponseRx2): int {.raises: [InvalidResponse].} =
137+
# Note this client only ever sends number IDs, and so it expects number IDs back
137138
case resp.id.kind
138139
of riNumber:
139140
resp.id.num
140141
of riString:
141-
int.low
142+
raise (ref InvalidResponse)(msg: "Expected response with int `id`, but got `id` = \"" & resp.id.str & "\"")
142143
of riNull:
143144
case resp.kind
144145
of rkResult:
145-
int.low
146+
raise (ref InvalidResponse)(msg: "Expected response with int `id`, but got `id` = null")
146147
of rkError:
147148
# likely an invalid request error
148-
raise (ref JsonRpcError)(msg: JrpcSys.encode(resp.error))
149+
raise (ref InvalidResponse)(msg: JrpcSys.encode(resp.error))
149150

150151
proc processMessageResponse(
151152
client: RpcConnection, batch: ResponseBatchRx
152-
) {.raises: [JsonRpcError].} =
153+
) {.raises: [InvalidResponse].} =
153154
let id = case batch.kind
154155
of rbkMany:
155156
var curr = int.low

json_rpc/clients/socketclient.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ proc processMessages(client: RpcSocketClient) {.async: (raises: []).} =
238238

239239
let resp = try:
240240
await client.processMessage(data)
241+
except InvalidResponse as exc:
242+
raise exc
241243
except JsonRpcError as exc:
242244
try:
243245
await client.framing.sendMsg(client.transport, wrapError(router.INVALID_REQUEST, exc.msg))

json_rpc/clients/websocketclient.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ proc processMessages(client: RpcWebSocketClient) {.async: (raises: []).} =
109109

110110
let resp = try:
111111
await client.processMessage(data)
112+
except InvalidResponse as exc:
113+
raise exc
112114
except JsonRpcError as exc:
113115
try:
114116
await client.transport.send(wrapError(router.INVALID_REQUEST, exc.msg), Opcode.Binary)

tests/test_bidirectional.nim

Lines changed: 76 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
# This file may not be copied, modified, or distributed except according to
88
# those terms.
99

10+
{.push raises: [], gcsafe.}
11+
1012
import
1113
chronos/unittest2/asynctests,
1214
stew/byteutils,
@@ -26,6 +28,25 @@ createRpcSigsFromNim(RpcClient, JrpcFlavor):
2628
proc rets(s: string): string
2729
proc invalid(s: int): string
2830

31+
template checkInvalidRequest(client: untyped, req, expectedErr: string): untyped =
32+
# check sending `req` terminates the connection with `expectedErr` error
33+
var disconnFut = newFuture[void]()
34+
client.onDisconnect = proc () {.gcsafe, raises: [].} =
35+
disconnFut.complete()
36+
let fut1 = client.send(req.toBytes)
37+
let fut2 = client.rets("foobar")
38+
waitFor fut1
39+
waitFor disconnFut
40+
try:
41+
discard waitFor fut2
42+
doAssert false
43+
except RpcTransportError as err:
44+
doAssert err.parent != nil
45+
check err.parent.msg == expectedErr
46+
# following requests won't work
47+
expect RpcTransportError:
48+
discard waitFor client.rets("foobar")
49+
2950
template allTests(client: untyped) =
3051
test "Successful RPC call":
3152
let r1 = waitFor client.rets("foobar")
@@ -60,109 +81,80 @@ template allTests(client: untyped) =
6081
res.get()[1].error.isNone
6182
res.get()[2].error.isSome
6283

63-
test "Request with null id terminates the connection":
64-
# the response will contain a null id
65-
var disconnFut = newFuture[void]()
66-
client.onDisconnect = proc () {.gcsafe, raises: [].} =
67-
disconnFut.complete()
68-
let fut1 = client.send("""{"jsonrpc": "2.0", "method": "foobar", "id": null}""".toBytes)
69-
let fut2 = client.rets("foobar")
70-
waitFor fut1
71-
waitFor disconnFut
72-
try:
73-
discard waitFor fut2
74-
doAssert false
75-
except RpcTransportError as err:
76-
# check it fails with method not found; id=null response
77-
check err.parent.msg == """{"code":-32601,"message":"'foobar' is not a registered RPC method"}"""
78-
# following requests won't work
79-
expect RpcTransportError:
80-
discard waitFor client.rets("foobar")
81-
82-
test "Sending an ambiguous message terminates the connection":
83-
var disconnFut = newFuture[void]()
84-
client.onDisconnect = proc () {.gcsafe, raises: [].} =
85-
disconnFut.complete()
86-
let fut1 = client.send("""{"foo": "boo"}""".toBytes)
87-
let fut2 = client.rets("foobar")
88-
waitFor fut1
89-
waitFor disconnFut
90-
try:
91-
discard waitFor fut2
92-
doAssert false
93-
except RpcTransportError as err:
94-
# check it fails with parse error; id=null response
95-
check err.parent.msg == """{"code":-32600,"message":"',' expected"}"""
96-
# following requests won't work
97-
expect RpcTransportError:
98-
discard waitFor client.rets("foobar")
99-
100-
test "Sending an ambiguous batch message terminates the connection":
101-
var disconnFut = newFuture[void]()
102-
client.onDisconnect = proc () {.gcsafe, raises: [].} =
103-
disconnFut.complete()
104-
let fut1 = client.send("""[{"foo": "boo"}]""".toBytes)
105-
let fut2 = client.rets("foobar")
106-
waitFor fut1
107-
waitFor disconnFut
108-
try:
109-
discard waitFor fut2
110-
doAssert false
111-
except RpcTransportError as err:
112-
# check it fails with parse error; id=null response
113-
check err.parent.msg == """{"code":-32600,"message":"',' expected"}"""
114-
# following requests won't work
115-
expect RpcTransportError:
116-
discard waitFor client.rets("foobar")
117-
118-
test "Sending a response with id null terminates the connection":
119-
var disconnFut = newFuture[void]()
120-
client.onDisconnect = proc () {.gcsafe, raises: [].} =
121-
disconnFut.complete()
122-
const resp = """{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}"""
123-
waitFor client.send(resp.toBytes)
124-
waitFor disconnFut
125-
126-
test "Sending a batch response with id null terminates the connection":
127-
var disconnFut = newFuture[void]()
128-
client.onDisconnect = proc () {.gcsafe, raises: [].} =
129-
disconnFut.complete()
130-
const resp = """[{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}]"""
131-
waitFor client.send(resp.toBytes)
132-
waitFor disconnFut
133-
13484
test "Sending an unknown id is ignored":
135-
const resp = """{"jsonrpc": "2.0", "result": 7, "id": 123123}"""
136-
waitFor client.send(resp.toBytes)
85+
const req = """{"jsonrpc": "2.0", "method": "foo", "id": 123123}"""
86+
waitFor client.send(req.toBytes)
13787
# following requests still work
13888
let r1 = waitFor client.rets("foobar")
13989
check r1 == "ret foobar"
14090

141-
test "Sending a batch with an unknown id is ignored":
142-
const resp = """[{"jsonrpc": "2.0", "result": 7, "id": 123123}]"""
143-
waitFor client.send(resp.toBytes)
91+
test "Sending an unknown id within a batch is ignored":
92+
const req = """[{"jsonrpc": "2.0", "method": "foo", "id": 123123}]"""
93+
waitFor client.send(req.toBytes)
14494
# following requests still work
14595
let r1 = waitFor client.rets("foobar")
14696
check r1 == "ret foobar"
14797

148-
test "Sending a string id is ignored":
149-
const resp = """{"jsonrpc": "2.0", "result": 7, "id": "123123"}"""
150-
waitFor client.send(resp.toBytes)
98+
test "Sending a notification won't terminate the connection":
99+
const req = """{"jsonrpc": "2.0", "method": "rets", "params": ["foo"]}"""
100+
waitFor client.send(req.toBytes)
151101
# following requests still work
152102
let r1 = waitFor client.rets("foobar")
153103
check r1 == "ret foobar"
154104

155-
test "Sending a batch with a string id is ignored":
156-
const resp = """[{"jsonrpc": "2.0", "result": 7, "id": "123123"}]"""
157-
waitFor client.send(resp.toBytes)
105+
test "Sending an invalid notification won't terminate the connection":
106+
const req = """{"jsonrpc": "2.0", "method": "rets", "params": [123]}"""
107+
waitFor client.send(req.toBytes)
158108
# following requests still work
159109
let r1 = waitFor client.rets("foobar")
160110
check r1 == "ret foobar"
161111

112+
test "Sending an ambiguous message terminates the connection":
113+
# check it fails with parse error; id=null response
114+
const req = """{"foo": "boo"}"""
115+
const expected = """{"code":-32600,"message":"',' expected"}"""
116+
checkInvalidRequest(client, req, expected)
117+
118+
test "Sending an ambiguous batch message terminates the connection":
119+
const req = """[{"foo": "boo"}]"""
120+
const expected = """{"code":-32600,"message":"',' expected"}"""
121+
checkInvalidRequest(client, req, expected)
122+
123+
test "Sending a null id terminates the connection":
124+
# note this terminates the connection when receiving the null id response
125+
const req = """{"jsonrpc": "2.0", "method": "foo", "id": null}"""
126+
const expected = """{"code":-32601,"message":"'foo' is not a registered RPC method"}"""
127+
checkInvalidRequest(client, req, expected)
128+
129+
test "Sending a null id within a batch terminates the connection":
130+
const req = """[{"jsonrpc": "2.0", "method": "foo", "id": null}]"""
131+
const expected = """{"code":-32601,"message":"'foo' is not a registered RPC method"}"""
132+
checkInvalidRequest(client, req, expected)
133+
134+
test "Sending a null id terminates the connection; variant":
135+
const req = """{"jsonrpc": "2.0", "method": "rets", "params": ["foo"], "id": null}"""
136+
const expected = "Expected response with int `id`, but got `id` = null"
137+
checkInvalidRequest(client, req, expected)
138+
139+
test "Sending a null id within a batch terminates the connection; variant":
140+
const req = """[{"jsonrpc": "2.0", "method": "rets", "params": ["foo"], "id": null}]"""
141+
const expected = "Expected response with int `id`, but got `id` = null"
142+
checkInvalidRequest(client, req, expected)
143+
144+
test "Sending a string id terminates the connection":
145+
# note this terminates the connection when receiving the string id response
146+
const req = """{"jsonrpc": "2.0", "method": "foo", "id": "123123"}"""
147+
const expected = """Expected response with int `id`, but got `id` = "123123""""
148+
checkInvalidRequest(client, req, expected)
149+
150+
test "Sending a string id within a batch terminates the connection":
151+
const req = """[{"jsonrpc": "2.0", "method": "foo", "id": "123123"}]"""
152+
const expected = """Expected response with int `id`, but got `id` = "123123""""
153+
checkInvalidRequest(client, req, expected)
154+
162155
suite "Test bidirectional socket server/client":
163156
setup:
164-
# XXX Framing.lengthHeaderBE32()
165-
const framing = Framing.newLine()
157+
const framing = Framing.lengthHeaderBE32()
166158
var srv = newRpcSocketServer(["127.0.0.1:0"], framing = framing)
167159
var client = newRpcSocketClient(framing = framing)
168160

0 commit comments

Comments
 (0)