Echo request id in post-parse Streamable HTTP error responses (#1687)

Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
This commit is contained in:
Tarek Mahmoud Sayed 2026-07-02 11:09:05 -07:00 • committed by GitHub
commit e23be1d75e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 19 deletions

View file

@ -174,7 +174,7 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
// checks) so a conformant client on this revision surfaces the error instead of mistaking the modern server for a
// legacy one and falling back to the initialize handshake.
var body =
@"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" +
@"{""jsonrpc"":""2.0"",""id"":4242,""method"":""server/discover"",""params"":{" +
July2026ProtocolMetaFragment("2025-11-25") + "}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
@ -184,6 +184,33 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal((int)McpErrorCode.HeaderMismatch, json["error"]!["code"]!.GetValue<int>());
// The body parsed successfully, so per the base protocol responses section (and SEP-2243's error
// response format) this error MUST echo the request id rather than emitting id=null (see #1677).
Assert.Equal(4242, json["id"]!.GetValue<long>());
}
[Fact]
public async Task July2026Post_MissingMcpNameHeader_ReturnsHeaderMismatch_EchoesRequestId()
{
await StartAsync();
// A well-formed tools/call whose body parses, but the required Mcp-Name header is absent. The server
// rejects it with -32020 HeaderMismatch, and because the request id was readable the JSON-RPC error
// MUST carry that same id (regression guard for #1677).
var body =
@"{""jsonrpc"":""2.0"",""id"":4242,""method"":""tools/call"",""params"":{""name"":""echo"",""arguments"":{""text"":""hi""}," +
July2026ProtocolMetaFragment() + "}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
request.Headers.Add(ProtocolVersionHeader, McpHttpHeaders.July2026ProtocolVersion);
request.Headers.Add("Mcp-Method", "tools/call");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal((int)McpErrorCode.HeaderMismatch, json["error"]!["code"]!.GetValue<int>());
Assert.Equal(4242, json["id"]!.GetValue<long>());
}
[Fact]

View file

@ -224,7 +224,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
using var request = new HttpRequestMessage(HttpMethod.Post, "")
{
Content = JsonContent(EchoRequest),
Content = JsonContent("""{"jsonrpc":"2.0","id":4242,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hi"}}}"""),
Headers =
{
{ "mcp-session-id", "fakeSession" },
@ -232,6 +232,11 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
};
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
// The request body parsed successfully, so the JSON-RPC error MUST echo its id rather than
// emitting id=null (base protocol responses section; see #1677).
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal(4242, doc.RootElement.GetProperty("id").GetInt64());
}
[Fact]
@ -245,6 +250,31 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
Assert.Contains("Mcp-Session-Id", body);
Assert.Contains("Stateless", body);
// The request body parsed successfully, so the JSON-RPC error MUST echo its id (see #1677).
using var doc = JsonDocument.Parse(body);
Assert.Equal(1, doc.RootElement.GetProperty("id").GetInt64());
}
[Fact]
public async Task StatelessPostWithSessionId_Returns400_EchoesRequestId()
{
await StartAsync(stateless: true);
using var request = new HttpRequestMessage(HttpMethod.Post, "")
{
Content = JsonContent("""{"jsonrpc":"2.0","id":4242,"method":"tools/list","params":{}}"""),
Headers =
{
{ "mcp-session-id", "someSession" },
},
};
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
// The request body parsed successfully, so the stateless-mode rejection MUST echo its id (see #1677).
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
Assert.Equal(4242, doc.RootElement.GetProperty("id").GetInt64());
}
[Fact]