Skip to content
Draft
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
30 changes: 24 additions & 6 deletions src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ internal sealed class StreamableHttpHandler(
/// </summary>
private static readonly string[] s_sessionSupportingProtocolVersions = McpProtocolVersions.InitializeHandshakeProtocolVersions;

private static readonly JsonTypeInfo<JsonElement> s_jsonElementTypeInfo = GetRequiredJsonTypeInfo<JsonElement>();
private static readonly JsonTypeInfo<JsonRpcMessage> s_messageTypeInfo = GetRequiredJsonTypeInfo<JsonRpcMessage>();
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();

Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -663,10 +681,10 @@ internal static string MakeNewSessionId()
}

internal static async Task<JsonRpcMessage?> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down