fix: keep serving after malformed input instead of dying on decode errors - #2
Merged
Merged
Conversation
RunWithIO decoded stdin with json.Decoder and returned a fatal "decode
error" on any line that could not be decoded into JSONRPCRequest —
including well-formed JSON whose field types do not match, e.g.
{"jsonrpc":1,"id":2,"method":"ping"}. A single crafted or buggy line
could therefore kill the server mid-session: the offending request went
unanswered, and pipe-based hosts could wedge forever on a blocked
writer with no further responses of any kind.
The dispatch loop is now newline-delimited (the MCP stdio framing, one
message per line) over a reusable read buffer:
- broken JSON gets -32700 (Parse error); the loop keeps serving
- well-formed JSON that is not a Request object gets -32600 (Invalid
Request) with id null; the loop keeps serving
- CRLF endings, blank separator lines, and a final unterminated line
are tolerated; there is still no message-size limit
- RunWithIO returns only on clean EOF or a read/write failure
Note on framing strictness: multiple JSON values concatenated on one
line (or pretty-printed multi-line messages) were previously accepted
as separate decoder values; they are protocol violations under the
MCP stdio transport and are now rejected with -32700 per line.
TestDecodeError asserted the old kill-the-server contract; it is
replaced by keep-serving regression tests covering the wedge scenario,
non-object JSON, multiple bad lines, CRLF handling, and final-line EOF.
Latency is unchanged; allocations drop slightly (+2 KB buffered I/O
for the reusable reader).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
RunWithIOdecoded stdin withjson.Decoderand returned a fataldecode erroron any line it could not decode intoJSONRPCRequest. That includes well-formed JSON whose field types do not match the struct:A single such line — crafted by untrusted content or produced by a buggy host — kills the dispatch loop mid-session. Downstream, a host that frames requests through an
io.Pipe(as odek-style adapters do) then wedges permanently: the offending request is never answered, every subsequent valid request is stranded behind a blocked writer, and the server never observes EOF.Reproduced against v1.2.0: after the bad line, no response of any kind is emitted and
RunWithIOnever returns even after stdin closes.Per JSON-RPC 2.0 this class of input must be answered in-band (
-32700/-32600) and the connection kept alive.Fix
The dispatch loop now reads newline-delimited messages directly (the MCP stdio framing: one message per line) over a reusable buffer:
-32700 Parse error, idnull; loop keeps serving-32600 Invalid Request, idnull; loop keeps servingRunWithIOreturns only on clean EOF (nil) or a read/write failureFraming note: previously multiple JSON values concatenated on one line were processed as separate decoder values. That is a protocol violation under the stdio transport ("messages MUST NOT contain embedded newlines", one per line); such lines are now rejected with
-32700. All mainstream clients (Claude Desktop, Cursor, odek) already send strict NDJSON.Testing
TestDecodeErrorasserted the old kill-the-server contract; replaced with five keep-serving regression tests: the wedge repro (type-mismatched line then valid request), non-object JSON, multiple bad lines + CRLF + blank lines, and final-line-without-newline EOF handling.-race; E2E subprocess test passes.Verified end-to-end with a downstream MCP adapter that previously deadlocked on the crafted line: it now receives
-32600for the bad line, a normal response for the follow-up request, and shuts down cleanly on EOF.