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

fix(client): send POST bodies with Content-Length instead of chunked encoding#1877
ump45nose wants to merge 1 commit into
modelcontextprotocol:mainfrom
ump45nose:fix/mcp-http-content-length

Conversation

@ump45nose

Copy link
Copy Markdown

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transfer-Encoding=Chunked causes issues with locally run Azure Functions

1 participant