Skip to content

Streamable HTTP returns -32600 instead of -32700 for malformed JSON #1918

Open

Description

@mforce

Summary

The Streamable HTTP POST handler answers a body that is not valid JSON with -32600 (Invalid Request). JSON-RPC 2.0 §5.1 assigns -32700 (Parse error) to invalid JSON and keeps -32600 for JSON that parses but is not a valid request object.

Where

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs, lines 80–93 on both v2.2.0 and main:

catch (JsonException)
{
    // The POST body was not a well-formed JSON-RPC message (malformed JSON, or a request whose
    // id was explicitly null, which MCP forbids). ...
    await WriteJsonRpcErrorAsync(context,
        "Bad Request: The POST body did not contain a valid JSON-RPC message.",
        StatusCodes.Status400BadRequest, (int)McpErrorCode.InvalidRequest);
    return;
}

The one catch covers two cases: a syntax error, and a well-formed message the converter rejects (for example "id": null). Both get InvalidRequest.

Repro

With valid auth and the required headers (Accept: application/json, text/event-stream, a supported MCP-Protocol-Version), POST the truncated body {"jsonrpc": to a stateless Streamable HTTP endpoint.

  • Actual: HTTP 400, {"jsonrpc":"2.0","id":null,"error":{"code":-32600,...}}
  • Expected: HTTP 400 with "code": -32700

It happens before protocol-version dispatch, so 2025-11-25 and 2026-07-28 both behave this way.

Suggested fix

Tell the two cases apart. One way: parse the body into a JsonDocument/JsonNode first, return McpErrorCode.ParseError if that throws, then deserialize the JsonRpcMessage and keep InvalidRequest for envelope failures. Two tests would pin it: one for malformed JSON (-32700), one for "id": null (-32600).

#1629 already returns ParseError for the stdio over-nesting case, so this would make HTTP consistent with stdio.

Related: the conformance test PostMalformedJson_Returns400_InvalidRequest_WithNullId currently asserts -32600 for malformed JSON, so it would change with the fix. Open PR #1844 edits the same catch to add parse detail and keeps InvalidRequest; this could land with it or after it.

Found during a conformance audit of a server built on 2.2.0.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions