{"record":{"id":"a65ed1067ac95b4d","repo":"can1357/oh-my-pi","slug":"failed-to-decode-reassembled-rpc-frame","errorCode":null,"errorMessage":"Failed to decode reassembled RPC frame","messagePattern":"Failed to decode reassembled RPC frame","errorType":"exception","errorClass":"RpcError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/client.py","lineNumber":213,"sourceCode":"            or pending.next_index != index\n        ):\n            raise RpcError(\"RPC chunk sequence mismatch\")\n        pending.chunks.append(chunk)\n        pending.received_bytes += len(chunk)\n        pending.next_index += 1\n        if pending.received_bytes > pending.byte_length:\n            raise RpcError(\"RPC chunk sequence exceeds its declared length\")\n        if pending.next_index < pending.count:\n            return None\n        if pending.received_bytes != pending.byte_length:\n            raise RpcError(\"RPC chunk sequence length mismatch\")\n\n        self._pending = None\n        try:\n            decoded = b\"\".join(pending.chunks).decode(\"utf-8\")\n            frame = json.loads(decoded)\n        except (UnicodeDecodeError, json.JSONDecodeError) as exc:\n            raise RpcError(\"Failed to decode reassembled RPC frame\") from exc\n        if not isinstance(frame, dict):\n            raise RpcError(\"RPC frame must be a JSON object\")\n        return cast(JsonObject, frame)\n\n\ndef _process_group_id(process: subprocess.Popen[Any]) -> int | None:\n    \"\"\"Process-group id of `process`, or `None` when groups are unavailable.\n\n    Captured right after spawn so teardown can signal the whole group even\n    after the leader is reaped — POSIX `os.getpgid` fails on a reaped pid.\n    \"\"\"\n    getpgid = getattr(os, \"getpgid\", None)\n    if getpgid is None:\n        return None\n    try:\n        return getpgid(process.pid)\n    except OSError:\n        return None","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/client.py#L195-L231","documentation":"The chunk sequence completed and the byte counts matched, but the reassembled bytes could not be decoded as UTF-8 or parsed as JSON. Chunk-level validation only guarantees base64 integrity and length consistency — it cannot guarantee the sender encoded valid UTF-8 JSON. The library wraps the underlying UnicodeDecodeError/JSONDecodeError in a domain RpcError.","triggerScenarios":"push() completing a chunk sequence whose concatenated bytes are not valid UTF-8, or valid UTF-8 that is not parseable JSON (truncated JSON, concatenated objects, binary data, wrong character encoding like latin-1).","commonSituations":"Sender serialized with a non-UTF-8 codec; payload was sliced mid-multi-byte-character because chunks were cut by character count; the 'frame' was actually binary (e.g. protobuf/msgpack) sent through the JSON chunking path; sender wrote multiple JSON documents concatenated instead of one.","solutions":["On the sender, chunk the payload after encoding: payload_bytes = json.dumps(frame).encode('utf-8'), and split those bytes so multi-byte chars are never severed.","Ensure exactly one JSON object is serialized per sequence (json.dumps of one dict).","If the payload is binary, use the appropriate binary transport instead of the JSON chunking path.","Inspect the reassembled bytes by decoding leniently on the sender side (errors='replace') to find the corruption point.","Verify sender locale/encoding (PYTHONIOENCODING, open() encoding) forces UTF-8."],"exampleFix":"// before\ntext = json.dumps(frame)\nchunks = [text[i:i+n] for i in range(0, len(text), n)]  # may split multibyte chars\n\n// after\nencoded = json.dumps(frame).encode(\"utf-8\")\nchunks = [encoded[i:i+n] for i in range(0, len(encoded), n)]  # byte-safe\nsend_chunks(chunks, byteLength=len(encoded))","handlingStrategy":"try-catch","validationCode":"import json\ndef payload_is_valid_json(payload: bytes) -> bool:\n    try:\n        json.loads(payload.decode(\"utf-8\"))\n        return True\n    except (UnicodeDecodeError, json.JSONDecodeError):\n        return False\n# run on the sender's full payload before chunking","typeGuard":"def is_utf8_json_dict(payload: bytes) -> TypeGuard[bytes]:\n    try:\n        import json\n        return isinstance(json.loads(payload.decode(\"utf-8\")), dict)\n    except Exception:\n        return False","tryCatchPattern":"try:\n    frame = client.push(last_chunk)\nexcept RpcError as exc:\n    if str(exc) == \"Failed to decode reassembled RPC frame\":\n        logger.error(\"reassembled payload is not UTF-8 JSON; dumping hex for diagnosis\")\n        dump_reassembled_hex_for_debug()\n        request_resend()\n    else:\n        raise","preventionTips":["Serialize with json.dumps(...).encode('utf-8') and chunk the bytes, never the str.","Validate the full payload parses as one JSON object before chunking it on the sender.","Force UTF-8 everywhere (file opens, subprocess env, network codecs).","Route binary payloads through a binary path, not the JSON chunking protocol."],"tags":["rpc","encoding","json","chunking"],"backgroundTag":"reassembled-frame-decode-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}