Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,18 @@ await WriteJsonRpcErrorAsync(context,
{
message = await ReadJsonRpcMessageAsync(context);
}
catch (JsonException)
catch (JsonException ex)
{
// The POST body was not a well-formed JSON-RPC message (malformed JSON, or a request whose
// id was explicitly null, which MCP forbids). Surface a conformant JSON-RPC error response
// with a null id rather than letting the exception bubble up as an opaque 500.
// with a null id rather than letting the exception bubble up as an opaque 500. The parser's
// position detail is included so a truncated or corrupted body (e.g. one mangled by an
// intermediary) is diagnosable from the response alone (#1842).
var position = ex.BytePositionInLine is null
? $"line {ex.LineNumber?.ToString() ?? "unknown"}"
: $"line {ex.LineNumber?.ToString() ?? "unknown"}, byte position {ex.BytePositionInLine}";
await WriteJsonRpcErrorAsync(context,
"Bad Request: The POST body did not contain a valid JSON-RPC message.",
$"Bad Request: The POST body did not contain a valid JSON-RPC message: {ex.Message} ({position}).",
StatusCodes.Status400BadRequest, (int)McpErrorCode.InvalidRequest);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,33 @@ public async Task PostMalformedJson_Returns400_InvalidRequest_WithNullId()
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("id").ValueKind);
Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());

// The parse failure itself must be diagnosable from the response alone: the message carries the
// parser's reason and position instead of an opaque one-liner (#1842).
var message = doc.RootElement.GetProperty("error").GetProperty("message").GetString();
Assert.Contains("did not contain a valid JSON-RPC message", message);
Assert.Contains("line 0, byte position", message);
}

[Theory]
[InlineData("""{"jsonrpc":"2.0","id":1,"method":"tools/list","para""")]
[InlineData("""{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"a":1""")]
public async Task PostTruncatedJson_Returns400_WithParserDetail(string body)
{
await StartAsync();

// A body cut in half by an intermediary (proxy, gateway, or the transport itself) is the exact
// shape reported in #1842. The 400 response must say where parsing stopped so the truncation
// is identifiable from the response without server-side logs.
using var response = await HttpClient.PostAsync("", JsonContent(body), TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());

var message = doc.RootElement.GetProperty("error").GetProperty("message").GetString();
Assert.Contains("did not contain a valid JSON-RPC message", message);
Assert.Contains("line 0, byte position", message);
}

[Fact]
Expand Down