Skip to content

Return 400 for unreadable strings in Streamable HTTP POST validation - #2014

Open
sk-20231 wants to merge 1 commit into
modelcontextprotocol:mainfrom
sk-20231:fix-invalid-utf8-500
Open

sk-20231 wants to merge 1 commit into
modelcontextprotocol:mainfrom
sk-20231:fix-invalid-utf8-500

Conversation

@sk-20231

Copy link
Copy Markdown

Fixes #1984

Problem

HandlePostRequestAsync reads some request values as strings while it validates the headers against the body. Two kinds of input make those reads throw InvalidOperationException, which the handler does not catch (it only catches JsonException), so the request returns 500:

  • A string that is not valid UTF-8. Utf8JsonReader accepts the bytes inside a string token and only validates them when the token is converted to a .NET string.
  • A value that is not a string where a string is expected, for example "name": 5 on a tools/call request. JsonValue.GetValue<string>() throws for it.

Affected reads:

Value Validation Before After
params.protocolVersion on initialize (with an MCP-Protocol-Version header) protocol version envelope 500 400, HeaderMismatch (-32020)
_meta/io.modelcontextprotocol/protocolVersion protocol version envelope 500 400, InvalidParams (-32602)
Body value compared with Mcp-Name (invalid UTF-8 or not a string) ValidateMcpHeaders 500 400, HeaderMismatch (-32020)
Argument annotated with x-mcp-header ValidateCustomParamHeaders 500 400, HeaderMismatch (-32020)

Change

  • Added a private TryGetStringValue helper that returns false instead of throwing when a node is not a JSON string or the string is not valid UTF-8, and a TryGetJsonNodeStringProperty wrapper that replaces GetJsonNodeStringProperty.
  • Each check now rejects an unreadable value with the error code it already uses for a bad value of that field, instead of adding one generic code. An unreadable _meta protocol version is handled like a missing one, the same as a non-string value today.
  • Every response echoes the request id, as the existing error paths do.
  • No public API changes. McpHeaderEncoder is unchanged. The x-mcp-header check validates the argument before calling ConvertToHeaderValue.

On the question in the issue (-32600 vs -32700): the body is well-formed JSON and is parsed into a valid JSON-RPC request with a readable id, so I used the codes each validation step already returns. Happy to switch to a different code if you prefer.

Tests

Five tests in HttpHeaderConformanceTests. Each one failed with 500 before the change. Each now checks the status, the JSON-RPC error code, and the echoed id:

  • Server_RejectsInvalidUtf8InProtocolVersionMeta_WithBadRequest
  • Server_RejectsInvalidUtf8InInitializeProtocolVersion_WithBadRequest
  • Server_RejectsInvalidUtf8InMcpNameBodyValue_WithBadRequest
  • Server_RejectsInvalidUtf8InCustomParamHeaderArgument_WithBadRequest
  • Server_RejectsNonStringMcpNameBodyValue_WithBadRequest

Local results:

  • Full solution build: 0 errors
  • ModelContextProtocol.AspNetCore.Tests (net10.0): 759 passed, 0 failed, 45 skipped
  • ModelContextProtocol.Tests (net472): 3299 passed, 0 failed, 290 skipped

Note

This PR was written with help from an AI coding assistant (Claude Code). I reviewed the change, reproduced the bug, and ran the build and tests locally.

The JSON reader does not validate UTF-8 inside string tokens. That only
happens when a token is converted to a .NET string, which throws
InvalidOperationException. Converting a non-string value to a string
throws the same exception. HandlePostRequestAsync only catches
JsonException, so these requests returned 500.

Read these values with a helper that returns false instead of throwing,
and reject them with the error each check already uses:
- initialize params.protocolVersion: 400, HeaderMismatch (-32020)
- _meta protocolVersion: treated as missing, 400, InvalidParams (-32602)
- Mcp-Name body value (invalid UTF-8 or not a string): 400, HeaderMismatch
- x-mcp-header argument values: 400, HeaderMismatch

All responses echo the request id.

Fixes modelcontextprotocol#1984

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid UTF-8 in a request string returns 500 instead of 400 (Streamable HTTP)

1 participant