Skip to content

JSON-RPC read fails with large payloads (>64KB) due to pipe short reads #29

@ewilderj

Description

@ewilderj

Summary

The JSON-RPC client in copilot/jsonrpc.py fails to read large responses (>64KB) because _read_message() assumes read(n) always returns exactly n bytes. On Unix pipes, this is not guaranteed—the kernel may return fewer bytes ("short read"), especially when data exceeds the pipe buffer size.

Reproduction

Send a prompt with ~35KB+ of context. The response (~72KB with context echo) exceeds the 64KB pipe buffer:

import asyncio
from copilot import CopilotClient

async def test():
    client = CopilotClient()
    await client.start()
    
    session = await client.create_session({
        "model": "claude-sonnet-4.5",
        "streaming": False,
        "available_tools": [],
    })
    
    # 35KB context triggers the bug
    context = "Status update: work in progress. " * 1000  # ~35KB
    await session.send({"prompt": f"Summarize: {context}"})
    # ... wait for response
    
asyncio.run(test())

Error:

JSON-RPC read loop error: Unterminated string starting at: line 1 column 36026 (char 36025)

Root Cause

In copilot/jsonrpc.py, the _read_message() method:

content_length = int(header.split(":")[1].strip())
# ...
content_bytes = self.process.stdout.read(content_length)  # BUG: assumes full read
content = content_bytes.decode("utf-8")
return json.loads(content)

When content_length exceeds the pipe buffer (64KB on macOS, varies on Linux), read() returns only what is currently buffered. The truncated bytes fail JSON parsing.

Evidence

Diagnostic testing shows the exact behavior:

Context Size Expected Response Received Missing
35KB 72,094 bytes 65,513 bytes 9.1%
40KB 82,258 bytes 65,514 bytes 20.4%
45KB 92,554 bytes 65,512 bytes 29.2%

Note: Received bytes are consistently ~65,512 (≈64KB), the macOS pipe buffer limit.

Suggested Fix

Read in a loop until all expected bytes are received:

def _read_exact(self, num_bytes: int) -> bytes:
    """Read exactly num_bytes, handling partial/short reads from pipes."""
    chunks = []
    remaining = num_bytes
    while remaining > 0:
        chunk = self.process.stdout.read(remaining)
        if not chunk:
            raise EOFError("Unexpected end of stream while reading JSON-RPC message")
        chunks.append(chunk)
        remaining -= len(chunk)
    return b"".join(chunks)

def _read_message(self) -> Optional[dict]:
    # ... existing header parsing ...
    content_bytes = self._read_exact(content_length)  # Use loop-based read
    # ... rest unchanged ...

Environment

  • OS: macOS 14.x (also affects Linux with different buffer sizes)
  • Python: 3.11
  • SDK version: Latest from pip

Workaround

Limit context size to ~30KB to keep responses under 64KB.

References

  • POSIX read() specification: reads from pipes may return fewer bytes than requested
  • macOS pipe buffer: 64KB (PIPE_SIZE in XNU kernel)
  • Linux pipe buffer: typically 64KB (configurable via fcntl(F_SETPIPE_SZ))

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions