diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index f26e9ed5c..8c7a53273 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -43,6 +43,7 @@ internal sealed class StreamableHttpHandler( /// private static readonly string[] s_sessionSupportingProtocolVersions = McpProtocolVersions.InitializeHandshakeProtocolVersions; + private static readonly JsonTypeInfo s_jsonElementTypeInfo = GetRequiredJsonTypeInfo(); private static readonly JsonTypeInfo s_messageTypeInfo = GetRequiredJsonTypeInfo(); private static readonly JsonTypeInfo s_errorTypeInfo = GetRequiredJsonTypeInfo(); @@ -76,15 +77,32 @@ await WriteJsonRpcErrorAsync(context, return; } + // Parse the body as JSON first, so that invalid JSON (Parse error, -32700) can be told apart from + // JSON that is not a valid JSON-RPC message (Invalid Request, -32600), per JSON-RPC 2.0 §5.1. + JsonElement body; + try + { + body = await context.Request.ReadFromJsonAsync(s_jsonElementTypeInfo, context.RequestAborted); + } + catch (JsonException) + { + // The POST body was not valid JSON, so no request id can be read: answer with a null id rather + // than letting the exception bubble up as an opaque 500. + await WriteJsonRpcErrorAsync(context, + "Bad Request: The POST body was not valid JSON.", + StatusCodes.Status400BadRequest, (int)McpErrorCode.ParseError); + return; + } + JsonRpcMessage? message; try { - message = await ReadJsonRpcMessageAsync(context); + message = AddRequestContext(context, body.Deserialize(s_messageTypeInfo)); } 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). Surface a conformant JSON-RPC error response + // The POST body was valid JSON but not a well-formed JSON-RPC message (for example 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. await WriteJsonRpcErrorAsync(context, "Bad Request: The POST body did not contain a valid JSON-RPC message.", @@ -663,10 +681,10 @@ internal static string MakeNewSessionId() } internal static async Task ReadJsonRpcMessageAsync(HttpContext context) - { - // Implementation for reading a JSON-RPC message from the request body - var message = await context.Request.ReadFromJsonAsync(s_messageTypeInfo, context.RequestAborted); + => AddRequestContext(context, await context.Request.ReadFromJsonAsync(s_messageTypeInfo, context.RequestAborted)); + private static JsonRpcMessage? AddRequestContext(HttpContext context, JsonRpcMessage? message) + { if (message is not null) { var protocolVersion = context.Request.Headers[McpProtocolVersionHeaderName].ToString(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index dd051e4d3..a718b6854 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -302,20 +302,23 @@ public async Task StatelessPostWithSessionId_Returns400_EchoesRequestId() Assert.Equal(4242, doc.RootElement.GetProperty("id").GetInt64()); } - [Fact] - public async Task PostMalformedJson_Returns400_InvalidRequest_WithNullId() + [Theory] + [InlineData("{ this is not valid json")] + [InlineData("""{"jsonrpc":""")] + public async Task PostMalformedJson_Returns400_ParseError_WithNullId(string body) { await StartAsync(); - using var response = await HttpClient.PostAsync("", JsonContent("{ this is not valid json"), TestContext.Current.CancellationToken); + using var response = await HttpClient.PostAsync("", JsonContent(body), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); // The server must emit a conformant JSON-RPC error envelope (not a raw 500). Because the request // id could not be read, the error carries id=null per JSON-RPC 2.0 §5.1 — and crucially it must - // serialize as JSON null, not "" (regression guard for the RequestId write path). + // serialize as JSON null, not "" (regression guard for the RequestId write path). Invalid JSON is + // a Parse error (-32700); Invalid Request (-32600) is for JSON that is not a valid request object. 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()); + Assert.Equal((int)McpErrorCode.ParseError, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32()); } [Fact]