Skip to content

fix(client): send POST bodies with Content-Length instead of chunked encoding - #1877

Open
ump45nose wants to merge 1 commit into
modelcontextprotocol:mainfrom
ump45nose:fix/mcp-http-content-length
Open

ump45nose wants to merge 1 commit into
modelcontextprotocol:mainfrom
ump45nose:fix/mcp-http-content-length

Conversation

@ump45nose

Copy link
Copy Markdown
Contributor

Summary

Buffer the Streamable HTTP POST body so it goes out with a Content-Length instead of Transfer-Encoding: chunked.

McpHttpClient.CreatePostBodyContent built the body with JsonContent.Create(...) on modern .NET. JsonContent serializes lazily and cannot report a length, so HttpClient had no length to send and fell back to chunked encoding. Hosts that reject chunked request bodies then fail the POST — the local Azure Functions Python worker answers 400 "'Transfer-Encoding: chunked' header can not be used when content object is not specified" — which is the failure tracked in #816 and consolidated as #932.

The netstandard2.0 branch already buffered the payload into a ByteArrayContent, which does set Content-Length, so the wire format also depended on the target framework. This PR uses the buffered path on every target framework, which removes the TFM split entirely.

Changes

  • src/ModelContextProtocol.Core/Client/McpHttpClient.cs — drop the #if NET / #else split; serialize with JsonSerializer.SerializeToUtf8Bytes and wrap the bytes in a ByteArrayContent for all target frameworks. No Content-Type change: it is still the bare application/json value from s_applicationJsonContentType.
  • tests/ModelContextProtocol.Tests/Transport/HttpClientTransportTests.cs — regression test that drives a real Streamable HTTP POST through HttpClientTransport and asserts the request declares a Content-Length matching the bytes actually sent, and is not sent with chunked encoding.

Tradeoff: one serialized message is held in memory per request instead of being streamed. The netstandard2.0 path already paid that cost, and request bodies here are single JSON-RPC messages.

Testing

Build — all target frameworks, warnings as errors:

dotnet build --configuration Debug
    0 个警告 / 0 个错误   (0 warnings, 0 errors)

New test, red on unfixed source and green with the fix. With McpHttpClient.cs reverted to upstream/main but the test kept:

###### RED (unfixed source) ######
  错误消息:
   the POST body must declare a Content-Length; got null
失败!  - 失败: 1,通过: 0,总计: 1

With the fix:

###### GREEN (with fix) ######
已通过! - 失败: 0,通过: 1,已跳过: 0,总计: 1

Observed request properties from the message handler, before and after:

source Content.Headers.ContentLength content type
upstream/main null (→ chunked on the wire) JsonContent
this branch 54 ByteArrayContent

The values are captured inside the handler because McpHttpClient.SendAsync disposes the content once it returns; reading them afterwards throws ObjectDisposedException.

Full test project on net10.0:

dotnet test tests/ModelContextProtocol.Tests/ --framework net10.0
已通过! - 失败: 0,通过: 2388,已跳过: 5,总计: 2393,持续时间: 6 m

The 5 skips are the pre-existing Docker/external-service tests.

Other target frameworks (environment limit, please note). Only the .NET 10 runtime is installed here, so dotnet test cannot launch the net8.0/net9.0 test hosts. I ran those TFMs with the xUnit v3 in-process runner instead and both pass 17/17 for HttpClientTransportTests:

net8.0, .NET 8.0.30 runtime:   Total: 17, Errors: 0, Failed: 0
net9.0, via DOTNET_ROLL_FORWARD=Major onto .NET 10.0.11:  Total: 17, Errors: 0, Failed: 0

The net9.0 run executed on the .NET 10 runtime rather than a real .NET 9 runtime, so treat genuine net9.0 execution as covered by CI. netstandard2.0 compiles clean in the build above but has no test host.

Not verified: no end-to-end run against a locally hosted Azure Functions MCP server; the wire-format property is asserted at the request level instead.

Notes on the issue state

#932 still carries needs confirmation. The change follows the root cause and the preferred default described in the maintainer comment on the issue — send a Content-Length body by default rather than making it configurable. If the outcome should instead be opt-in via a HttpClientTransportOptions flag, say so and I will rework it.

Fixes #932

Note

This pull request description was AI-generated.

…encoding

On modern .NET, `McpHttpClient.CreatePostBodyContent` built the request body
with `JsonContent.Create(...)`. `JsonContent` serializes lazily and cannot
report a length, so `HttpClient` had no `Content-Length` to send and fell back
to `Transfer-Encoding: chunked`. Hosts that reject chunked request bodies then
fail the POST: the local Azure Functions Python worker answers with
`400 "'Transfer-Encoding: chunked' header can not be used when content object
is not specified"`, so an MCP server that works when deployed does not work
when run locally.

The netstandard2.0 branch already buffered the payload into a
`ByteArrayContent`, which does set `Content-Length`, so the wire format also
depended on the target framework. Use the buffered path on every target
framework: serialize to UTF-8 bytes and wrap them in a `ByteArrayContent`.
That removes the TFM split and makes the encoding uniform, at the cost of
holding one serialized message in memory, which the netstandard path already
did.

Add a regression test that drives a real Streamable HTTP POST through
`HttpClientTransport` and asserts, from the message handler, that the request
declares a `Content-Length` matching the bytes actually sent and is not sent
with chunked encoding. The values are captured in the handler because
`McpHttpClient` disposes the content once `SendAsync` returns.

Fixes modelcontextprotocol#932
@ump45nose
ump45nose force-pushed the fix/mcp-http-content-length branch from f3e240e to b82cb25 Compare September 21, 2026 06:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant