github/copilot-sdk · error · EndOfStreamException

Stream ended while reading JSON-RPC headers.

Error message

Stream ended while reading JSON-RPC headers.

What it means

JsonRpc.ReadMessageAsync throws EndOfStreamException when the underlying stream returns zero bytes after at least part of a JSON-RPC header has been read — the peer did not send a clean EOF between frames but truncated mid-header. ReadLoopAsync surfaces this to the connection handler.

Solutions

  1. Fix the peer to write complete frames atomically (full header+body per write) before closing.
  2. Treat mid-header truncation as a connection failure: log, close, and reconnect.
  3. Check the peer's stderr/exit code for a crash that explains the truncated write.
  4. Ensure no proxy/transport is clipping writes; verify framing uses \r\n\r\n between header and body.

Example fix

// before
stream.Write(headerBytes); stream.Close(); // header cut off
// after
using var ms = new MemoryStream();
ms.Write(headerBytes); ms.Write(body);
ms.CopyTo(stream); // single atomic write, then flush/close
Defensive patterns

Strategy: try-catch

Try / catch

try { await reader.ReadLoopAsync(ct); }
catch (EndOfStreamException ex) when (ex.Message.Contains("reading JSON-RPC headers")) {
    logger.LogWarning(ex, "Peer truncated a frame header; connection terminated");
    await ReconnectAsync(ct);
}

Prevention

When it happens

Trigger: Remote process crashes or exits while a frame header is partially written; network/socket dropped mid-header; a writer that closes the stream between a partial header and the rest of the frame.

Common situations: Server or CLI subprocess killed unexpectedly (OOM, ctrl-C); flaky transport dropping mid-message; sending raw bytes that break the Content-Length: ...\r\n framing protocol.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/a232a594eb0fdd2a. Report an issue: GitHub.

Appendix: source

Thrown at dotnet/src/JsonRpc.cs:395

        }

        while (headerEnd < 0)
        {
            if (filled == buffer.Length)
            {
                Array.Resize(ref buffer, buffer.Length * 2);
            }

            int bytesRead = await _receiveStream.ReadAsync(buffer.AsMemory(filled, buffer.Length - filled), cancellationToken).ConfigureAwait(false);
            if (bytesRead == 0)
            {
                // Clean EOF only if we haven't started a frame; otherwise the peer truncated mid-header.
                if (filled == 0)
                {
                    return (-1, buffer, 0);
                }

                throw new EndOfStreamException("Stream ended while reading JSON-RPC headers.");
            }

            filled += bytesRead;

            // Scan for \r\n\r\n starting from where a match could begin
            int scanStart = Math.Max(filled - bytesRead - 3, 0);
            int pos = buffer.AsSpan(scanStart, filled - scanStart).IndexOf("\r\n\r\n"u8);
            if (pos >= 0)
            {
                headerEnd = scanStart + pos + 4;
            }
        }

        // Parse Content-Length. LSP framing puts each header on its own \r\n-terminated
        // line; we walk the lines and require an exact "Content-Length: " prefix at the
        // start of one of them. A substring match anywhere in the header block would
        // false-positive on values like "X-Trace: Content-Length: 5" and desync the stream.
        // A missing or unparsable Content-Length means the framing is broken — there's

View on GitHub (pinned to cd8cf15dc3)