{"record":{"id":"4c266954eee3b46a","repo":"CoplayDev/unity-mcp","slug":"connection-closed-before-reading-expected-bytes","errorCode":null,"errorMessage":"Connection closed before reading expected bytes","messagePattern":"Connection closed before reading expected bytes","errorType":"exception","errorClass":"ConnectionError","httpStatus":null,"severity":"error","filePath":"Server/src/transport/legacy/unity_connection.py","lineNumber":168,"sourceCode":"        except BlockingIOError:\n            pass  # No data pending; socket is alive\n        except Exception:\n            logger.debug(\"Stale socket detected; will reconnect on next send\")\n            try:\n                self.sock.close()\n            except Exception:\n                pass\n            self.sock = None\n        finally:\n            if self.sock and orig_blocking is not None:\n                self.sock.setblocking(orig_blocking)\n\n    def _read_exact(self, sock: socket.socket, count: int) -> bytes:\n        data = bytearray()\n        while len(data) < count:\n            chunk = sock.recv(count - len(data))\n            if not chunk:\n                raise ConnectionError(\n                    \"Connection closed before reading expected bytes\")\n            data.extend(chunk)\n        return bytes(data)\n\n    def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes:\n        \"\"\"Receive a complete response from Unity, handling chunked data.\"\"\"\n        if self.use_framing:\n            # Heartbeat semantics: the Unity editor emits zero-length frames while\n            # a long-running command is still executing. We tolerate a bounded\n            # number of these frames (or a small time window) before surfacing a\n            # timeout to the caller so tools can retry or fail gracefully.\n            heartbeat_limit = getattr(config, 'max_heartbeat_frames', 16)\n            heartbeat_window = getattr(config, 'heartbeat_timeout', 2.0)\n            heartbeat_started = time.monotonic()\n            heartbeat_count = 0\n            try:\n                while True:\n                    header = self._read_exact(sock, 8)","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/CoplayDev/unity-mcp/blob/c21bf496bca87d54e75bad048563c3adb1782081/Server/src/transport/legacy/unity_connection.py#L150-L186","documentation":"Raised by _read_exact (unity_connection.py:168) in the framed receive path. It loops calling sock.recv() to assemble a fixed byte count (the 8-byte length header or a payload); an empty recv() means TCP EOF before the expected bytes arrived, so the peer closed mid-message.","triggerScenarios":"receive_full_response in framing mode reading the 8-byte header or the payload when Unity closes the socket — e.g. a Unity domain reload, an editor crash, or the user quitting Unity while a command is in flight.","commonSituations":"Unity recompiling scripts (domain reload invalidates the socket), the editor being closed mid-operation, or a network device killing the connection during a large framed response.","solutions":["Let send_command's retry loop handle it — it catches the failure, drops the dead socket, and reconnects on the next attempt.","Keep Unity open and avoid forcing recompiles during long tool calls.","If it recurs constantly, inspect the Unity Editor console for crashes or repeated domain reloads."],"exampleFix":"// before\nresp = conn.send_command(cmd, params)  # raises on EOF\n\n// after — rely on retry; only surface to the user after retries are exhausted\nresp = conn.send_command(cmd, params, max_attempts=config.max_retries)","handlingStrategy":"retry","validationCode":"# Validate the socket is live before sending\nimport select\n\ndef socket_alive(sock) -> bool:\n    if sock is None:\n        return False\n    r, _, x = select.select([sock], [], [sock], 0)\n    if x or (r and not sock.recv(1, socket.MSG_PEEK)):\n        return False  # EOF or error pending\n    return True","typeGuard":"def is_mid_stream_eof(e: BaseException) -> bool:\n    return (isinstance(e, ConnectionError)\n            and 'Connection closed before reading expected bytes' in str(e))","tryCatchPattern":"# Rely on send_command's built-in retry; it drops the dead socket and reconnects.\ntry:\n    resp = conn.send_command(cmd, params, max_attempts=config.max_retries)\nexcept ConnectionError as e:\n    if 'Connection closed before reading expected bytes' in str(e):\n        conn.disconnect()\n        resp = conn.send_command(cmd, params)  # one manual reconnect+retry","preventionTips":["Avoid forcing Unity recompiles while long tool calls are in flight.","Keep Unity open for the duration of a command batch.","Use the framed (default) transport, which is more robust to drops."],"tags":["connection","framed","eof","socket","domain-reload"],"backgroundTag":null,"analyzedSha":"c21bf496bca87d54e75bad048563c3adb1782081","analyzedAt":"2026-08-13T17:36:56.095Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}