From 1ddc24494f6b9527094b342c82f3b84a3b57d0be Mon Sep 17 00:00:00 2001 From: themylogin Date: Fri, 10 Jan 2025 17:24:30 +0100 Subject: [PATCH] Support JSON-RPC 2.0 Notifications (messages without the `id` key) (#15370) --- .../api/base/server/ws_handler/rpc.py | 53 +++++++++++-------- .../unit/api/base/server/test_ws_handler.py | 1 - 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/middlewared/middlewared/api/base/server/ws_handler/rpc.py b/src/middlewared/middlewared/api/base/server/ws_handler/rpc.py index f0d1de3326ef8..a8598b73a0717 100644 --- a/src/middlewared/middlewared/api/base/server/ws_handler/rpc.py +++ b/src/middlewared/middlewared/api/base/server/ws_handler/rpc.py @@ -18,6 +18,7 @@ from middlewared.service_exception import (CallException, CallError, ValidationError, ValidationErrors, adapt_exception, get_errname) from middlewared.utils.debug import get_frame_details +from middlewared.utils.lang import undefined from middlewared.utils.limits import MsgSizeError, MsgSizeLimit, parse_message from middlewared.utils.lock import SoftHardSemaphore, SoftHardSemaphoreLimit from middlewared.utils.origin import ConnectionOrigin @@ -268,7 +269,7 @@ async def validate_message(message: Any) -> None: if not isinstance(message["id"], None | int | str): raise ValueError("'id' member must be of type null, string or number") except KeyError: - raise ValueError("Missing 'id' member") + pass try: if not isinstance(message["method"], str) or not message["method"]: @@ -290,21 +291,21 @@ async def process_message(self, app: RpcWebSocketApp, message: Any): # format of the message (i.e. needs to be a dict) app.send_error(None, JSONRPCError.INVALID_REQUEST.value, "Invalid Message Format") except ValueError as e: - app.send_error(message.get("id"), JSONRPCError.INVALID_REQUEST.value, str(e)) + if (id_ := message.get("id", undefined)) != undefined: + app.send_error(id_, JSONRPCError.INVALID_REQUEST.value, str(e)) return + id_ = message.get("id", undefined) + try: method = self.methods[message["method"]] except KeyError: - app.send_error( - message["id"], - JSONRPCError.METHOD_NOT_FOUND.value, - "Method does not exist", - ) + if id_ != undefined: + app.send_error(id_, JSONRPCError.METHOD_NOT_FOUND.value, "Method does not exist") return asyncio.ensure_future( - self.process_method_call(app, message["id"], method, message["params"]) + self.process_method_call(app, id_, method, message["params"]) ) async def process_method_call(self, app: RpcWebSocketApp, id_: Any, method: Method, params: list): @@ -312,18 +313,22 @@ async def process_method_call(self, app: RpcWebSocketApp, id_: Any, method: Meth async with app.softhardsemaphore: result = await method.call(app, params) except SoftHardSemaphoreLimit as e: - app.send_error(id_, JSONRPCError.TRUENAS_TOO_MANY_CONCURRENT_CALLS.value, - f"Maximum number of concurrent calls ({e.args[0]}) has exceeded") + if id_ != undefined: + app.send_error(id_, JSONRPCError.TRUENAS_TOO_MANY_CONCURRENT_CALLS.value, + f"Maximum number of concurrent calls ({e.args[0]}) has exceeded") except ValidationError as e: - app.send_truenas_validation_error(id_, sys.exc_info(), [ - (e.attribute, e.errmsg, e.errno), - ]) + if id_ != undefined: + app.send_truenas_validation_error(id_, sys.exc_info(), [ + (e.attribute, e.errmsg, e.errno), + ]) except ValidationErrors as e: - app.send_truenas_validation_error(id_, sys.exc_info(), list(e)) + if id_ != undefined: + app.send_truenas_validation_error(id_, sys.exc_info(), list(e)) except (CallException, Error) as e: # CallException and subclasses are the way to gracefully send errors to the client - app.send_truenas_error(id_, JSONRPCError.TRUENAS_CALL_ERROR.value, "Method call error", e.errno, str(e), - sys.exc_info(), e.extra) + if id_ != undefined: + app.send_truenas_error(id_, JSONRPCError.TRUENAS_CALL_ERROR.value, "Method call error", e.errno, str(e), + sys.exc_info(), e.extra) except Exception as e: adapted = adapt_exception(e) if adapted: @@ -335,15 +340,17 @@ async def process_method_call(self, app: RpcWebSocketApp, id_: Any, method: Meth error = e extra = None - app.send_truenas_error(id_, JSONRPCError.TRUENAS_CALL_ERROR.value, "Method call error", errno_, - str(error) or repr(error), sys.exc_info(), extra) + if id_ != undefined: + app.send_truenas_error(id_, JSONRPCError.TRUENAS_CALL_ERROR.value, "Method call error", errno_, + str(error) or repr(error), sys.exc_info(), extra) if not adapted and not app.py_exceptions: self.middleware.logger.warning(f"Exception while calling {method.name}(*{method.dump_args(params)!r})", exc_info=True) else: - app.send({ - "jsonrpc": "2.0", - "result": result, - "id": id_, - }) + if id_ != undefined: + app.send({ + "jsonrpc": "2.0", + "result": result, + "id": id_, + }) diff --git a/src/middlewared/middlewared/pytest/unit/api/base/server/test_ws_handler.py b/src/middlewared/middlewared/pytest/unit/api/base/server/test_ws_handler.py index dda559819c5d4..ab640cac8ff65 100644 --- a/src/middlewared/middlewared/pytest/unit/api/base/server/test_ws_handler.py +++ b/src/middlewared/middlewared/pytest/unit/api/base/server/test_ws_handler.py @@ -38,7 +38,6 @@ }, ValueError, ), - ({"method": "test.method", "jsonrpc": "2.0", "params": ["a", "b"]}, ValueError), # test "method" member ({"id": 1, "method": [], "jsonrpc": "2.0", "params": ["a", "b"]}, ValueError), ({"id": 1, "method": "", "jsonrpc": "2.0", "params": ["a", "b"]}, ValueError),