Skip to content

Commit 0923355

Browse files
committed
Surface JSON parse detail in streamable-HTTP 400 responses
When the POST body cannot be parsed as a JSON-RPC message, the 400 InvalidRequest response was a fixed one-liner that discarded the JsonException's reason and position. A body truncated by an intermediary (proxy, gateway, or transport) is therefore indistinguishable from any other malformed body without server-side logs (#1842). The catch now includes the parser message and line/byte position in the error message, so the failure is diagnosable from the response alone. The message prefix and the conformant id=null envelope are unchanged.
1 parent 609499b commit 0923355

2 files changed

Lines changed: 35 additions & 3 deletions

File tree

‎src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,18 @@ await WriteJsonRpcErrorAsync(context,
8181
{
8282
message = await ReadJsonRpcMessageAsync(context);
8383
}
84-
catch (JsonException)
84+
catch (JsonException ex)
8585
{
8686
// The POST body was not a well-formed JSON-RPC message (malformed JSON, or a request whose
8787
// id was explicitly null, which MCP forbids). Surface a conformant JSON-RPC error response
88-
// with a null id rather than letting the exception bubble up as an opaque 500.
88+
// with a null id rather than letting the exception bubble up as an opaque 500. The parser's
89+
// position detail is included so a truncated or corrupted body (e.g. one mangled by an
90+
// intermediary) is diagnosable from the response alone (#1842).
91+
var position = ex.BytePositionInLine is null
92+
? $"line {ex.LineNumber?.ToString() ?? "unknown"}"
93+
: $"line {ex.LineNumber?.ToString() ?? "unknown"}, byte position {ex.BytePositionInLine}";
8994
await WriteJsonRpcErrorAsync(context,
90-
"Bad Request: The POST body did not contain a valid JSON-RPC message.",
95+
$"Bad Request: The POST body did not contain a valid JSON-RPC message: {ex.Message} ({position}).",
9196
StatusCodes.Status400BadRequest, (int)McpErrorCode.InvalidRequest);
9297
return;
9398
}

‎tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,33 @@ public async Task PostMalformedJson_Returns400_InvalidRequest_WithNullId()
316316
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
317317
Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("id").ValueKind);
318318
Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());
319+
320+
// The parse failure itself must be diagnosable from the response alone: the message carries the
321+
// parser's reason and position instead of an opaque one-liner (#1842).
322+
var message = doc.RootElement.GetProperty("error").GetProperty("message").GetString();
323+
Assert.Contains("did not contain a valid JSON-RPC message", message);
324+
Assert.Contains("line 0, byte position", message);
325+
}
326+
327+
[Theory]
328+
[InlineData("""{"jsonrpc":"2.0","id":1,"method":"tools/list","para""")]
329+
[InlineData("""{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"a":1""")]
330+
public async Task PostTruncatedJson_Returns400_WithParserDetail(string body)
331+
{
332+
await StartAsync();
333+
334+
// A body cut in half by an intermediary (proxy, gateway, or the transport itself) is the exact
335+
// shape reported in #1842. The 400 response must say where parsing stopped so the truncation
336+
// is identifiable from the response without server-side logs.
337+
using var response = await HttpClient.PostAsync("", JsonContent(body), TestContext.Current.CancellationToken);
338+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
339+
340+
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
341+
Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());
342+
343+
var message = doc.RootElement.GetProperty("error").GetProperty("message").GetString();
344+
Assert.Contains("did not contain a valid JSON-RPC message", message);
345+
Assert.Contains("line 0, byte position", message);
319346
}
320347

321348
[Fact]

0 commit comments

Comments
 (0)