{"record":{"id":"8fc5409af5ee087b","repo":"can1357/oh-my-pi","slug":"rpc-frame-must-be-a-json-object","errorCode":null,"errorMessage":"RPC frame must be a JSON object","messagePattern":"RPC frame must be a JSON object","errorType":"exception","errorClass":"RpcError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/client.py","lineNumber":146,"sourceCode":"class _PendingRpcChunks:\n    chunk_id: str\n    count: int\n    byte_length: int\n    next_index: int = 0\n    chunks: list[bytes] = field(default_factory=list)\n    received_bytes: int = 0\n\n\nclass _RpcFrameDecoder:\n    def __init__(self) -> None:\n        self._pending: _PendingRpcChunks | None = None\n\n    def push(self, value: object) -> JsonObject | None:\n        if not isinstance(value, dict) or value.get(\"type\") != \"rpc_chunk\":\n            if self._pending is not None:\n                raise RpcError(\"RPC chunk sequence was interrupted\")\n            if not isinstance(value, dict):\n                raise RpcError(\"RPC frame must be a JSON object\")\n            return cast(JsonObject, value)\n\n        chunk_id = value.get(\"chunkId\")\n        index = value.get(\"index\")\n        count = value.get(\"count\")\n        byte_length = value.get(\"byteLength\")\n        data = value.get(\"data\")\n        max_chunk_count = (\n            _MAX_RPC_REASSEMBLED_BYTES + _RPC_CHUNK_PAYLOAD_BYTES - 1\n        ) // _RPC_CHUNK_PAYLOAD_BYTES\n        if (\n            not isinstance(chunk_id, str)\n            or not chunk_id\n            or len(chunk_id) > 128\n            or not isinstance(index, int)\n            or isinstance(index, bool)\n            or not isinstance(count, int)\n            or isinstance(count, bool)","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/client.py#L128-L164","documentation":"push() is the frame feeder for the RPC client's reassembly pipeline. After handling chunked rpc_chunk frames, any leftover frame must be a JSON object (dict); a non-dict value (list, string, number, bool, null) is rejected because RPC frames are protocol messages keyed by fields like \"type\", \"id\", and \"method\". The library throws this to enforce the wire contract at the client boundary rather than letting a malformed frame flow into dispatch.","triggerScenarios":"Calling client.push(value) with a top-level JSON value that is not a dict (e.g. a JSON array, string, int, bool, or None) and is not an rpc_chunk dict. Also raised at the end of a chunk reassembly when the fully reassembled frame parses to a non-dict JSON value.","commonSituations":"Feeding raw lines from a non-omp peer process or socket that emits bare JSON arrays or scalars; piping stdout of a script that logs plain strings; a peer library version that speaks a different (non-object) framing; accidentally passing a parsed JSON document root instead of a single frame.","solutions":["Verify the sender emits one JSON object per frame (e.g. {\"type\": \"request\", ...}), not arrays or scalars.","Inspect what you feed push(): if you parse lines yourself, ensure each line is json.loads'ed to a dict before pushing.","If the peer legitimately streams a top-level array, iterate its elements and push each object individually.","Check peer/library version compatibility with the omp-rpc framing protocol.","Wrap push() in try/except RpcError to detect and drop/inspect malformed frames instead of crashing the pump."],"exampleFix":"// before\nfor line in proc.stdout:\n    frame = json.loads(line)\n    client.push(frame)  # crashes if line is '[1,2,3]' or '\"hello\"'\n\n// after\nfor line in proc.stdout:\n    frame = json.loads(line)\n    if isinstance(frame, dict):\n        client.push(frame)\n    else:\n        logger.warning(\"skipping non-object frame: %r\", frame)","handlingStrategy":"type-guard","validationCode":"def is_valid_frame(value: object) -> bool:\n    return isinstance(value, dict) and \"type\" in value","typeGuard":"def is_json_object(value: object) -> TypeGuard[dict]:\n    return isinstance(value, dict)","tryCatchPattern":"try:\n    frame = client.push(value)\nexcept RpcError as exc:\n    if str(exc) == \"RPC frame must be a JSON object\":\n        logger.warning(\"discarding non-object frame: %r\", value)\n        frame = None\n    else:\n        raise","preventionTips":["Type-check frames with isinstance(value, dict) before push().","Only feed push() with frames produced by json.loads of trusted single-object lines.","Unit-test your frame pump against arrays/scalars/null inputs."],"tags":["rpc","protocol","validation","python"],"backgroundTag":"rpc-frame-not-object","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}