fix(telemetry): Reject a JSON reply or a weights argument that is not an object

Both workload scripts decoded JSON and went straight to .get() or .items().
An array or a scalar decodes fine and then raises AttributeError, which is not a
ValueError, so the handler around the weights parsing could not catch it and the
operator saw a traceback naming neither the command nor what arrived.

Four sites across the two files, all four guarded, and the weights handler now
also catches TypeError for a non-numeric weight value.

Negative weights are left alone: they do not raise, the type is dropped from the
mix silently, and changing that changes accepted input.
This commit is contained in:
Pratik Mankawde
2026-09-22 20:30:53 +01:00
parent 331a453c9d
commit 68a805563f
2 changed files with 45 additions and 2 deletions

View File

@@ -411,6 +411,14 @@ async def _recv_matching_reply(
raise asyncio.TimeoutError(f"{command} (id {request_id}) got no reply")
raw = await asyncio.wait_for(conn.ws.recv(), timeout=remaining)
reply = json.loads(raw)
# A reply that decodes to an array or a scalar has no .get(), and the
# AttributeError that follows names neither the command nor what
# arrived. Reject it here so the message carries both.
if not isinstance(reply, dict):
raise TypeError(
f"{command} (id {request_id}) got a JSON "
f"{type(reply).__name__} reply, expected an object"
)
# A reply with no id counts as this request's: a few xrpld error paths
# answer before the id is parsed, and treating those as stale would
# turn a reported error into a timeout.
@@ -769,6 +777,19 @@ def main() -> None:
if args.weights:
try:
custom = json.loads(args.weights)
# A JSON array or scalar decodes fine and then has no .items().
# AttributeError is not a ValueError, so the handler below would not
# catch it: report the type that arrived instead of a traceback.
if not isinstance(custom, dict):
logger.error(
"Invalid --weights: expected a JSON object of command to "
"weight, got a JSON %s: %s",
type(custom).__name__,
args.weights,
)
sys.exit(1)
# TypeError covers a non-numeric weight value (null, a list, an
# object), which int() rejects with TypeError rather than ValueError.
weights = {k: int(v) for k, v in custom.items()}
if not weights or sum(weights.values()) <= 0:
logger.error(
@@ -777,7 +798,7 @@ def main() -> None:
)
sys.exit(1)
logger.info("Using custom weights: %s", weights)
except (json.JSONDecodeError, ValueError) as exc:
except (json.JSONDecodeError, TypeError, ValueError) as exc:
logger.error("Invalid --weights JSON: %s", exc)
sys.exit(1)

View File

@@ -281,6 +281,7 @@ async def ws_request(
Raises:
TimeoutError: If no reply carrying this request's ``id`` arrived within
RECV_TIMEOUT_S.
TypeError: If a reply decodes to something other than a JSON object.
"""
request_id = next(_request_ids)
request: dict[str, Any] = {"command": command}
@@ -311,6 +312,14 @@ async def ws_request(
raise TimeoutError(f"{command} (id {request_id}) got no reply")
raw = await asyncio.wait_for(ws.recv(), timeout=remaining)
resp = json.loads(raw)
# A reply that decodes to an array or a scalar has no .get(), and the
# AttributeError that follows names neither the command nor what
# arrived. Reject it here so the message carries both.
if not isinstance(resp, dict):
raise TypeError(
f"{command} (id {request_id}) got a JSON "
f"{type(resp).__name__} reply, expected an object"
)
reply_id = resp.get("id")
if reply_id is None or reply_id == request_id:
break
@@ -1211,6 +1220,19 @@ def main() -> None:
if args.weights:
try:
custom = json.loads(args.weights)
# A JSON array or scalar decodes fine and then has no .items().
# AttributeError is not a ValueError, so the handler below would not
# catch it: report the type that arrived instead of a traceback.
if not isinstance(custom, dict):
logger.error(
"Invalid --weights: expected a JSON object of transaction "
"type to weight, got a JSON %s: %s",
type(custom).__name__,
args.weights,
)
sys.exit(1)
# TypeError covers a non-numeric weight value (null, a list, an
# object), which int() rejects with TypeError rather than ValueError.
weights = {k: int(v) for k, v in custom.items()}
if not weights or sum(weights.values()) <= 0:
logger.error(
@@ -1219,7 +1241,7 @@ def main() -> None:
)
sys.exit(1)
logger.info("Using custom weights: %s", weights)
except (json.JSONDecodeError, ValueError) as exc:
except (json.JSONDecodeError, TypeError, ValueError) as exc:
logger.error("Invalid --weights JSON: %s", exc)
sys.exit(1)