Conversation
added 3 commits
April 19, 2026 15:35
The codec was only publishing the reason phrase in headers["status"] on the client path. That is good for logging but useless for callers that need to branch on the actual HTTP status (e.g. to treat a 2xx as success and a 4xx as a client error). Mirror the :method pseudo-header the server path already writes and populate headers[":status"] with the numeric response code before firing onHeaders. Comes with two unit tests that feed a minimal HTTP/1.1 response through the filter and assert both :status and the original status reason land on the callbacks' header map — guarding against future regressions in onHeadersComplete's client branch.
HttpAsyncClient hosts one outbound TCP connection per send(), installs the existing HttpCodecFilter in client mode as a read-only filter, and formats the request bytes directly so callers keep full control over method, path, and headers. Each request context implements DeferredDeletable; on completion the client hands ownership to Dispatcher::deferredDelete so the connection, codec filter, and callbacks tear down past the current callback frame rather than unwinding from inside their own callbacks. Body handling reflects a quirk of the codec in client mode: each body chunk is emitted twice, once inline and once again with the accumulated body at message-complete. The client takes only the end_stream delivery so the response body matches what arrived on the wire.
Drive the client against a real 127.0.0.1 listener stood up by RealListenerTestBase so the codec, deferred-delete teardown, and connection lifecycle all run through live kernel sockets rather than mocks. Covers: POST round-trip (request formatting and response body delivery), malformed-URL rejection (send returns false and fires no callback), and malformed response bytes (codec error surfaces as an error callback). Callbacks are captured through a shared_ptr so late firings from the dispatcher teardown path cannot hit a destroyed sink on the test stack.
caleb2h
approved these changes
Apr 19, 2026
caleb2h
pushed a commit
that referenced
this pull request
Apr 19, 2026
The codec was only publishing the reason phrase in headers["status"] on the client path. That is good for logging but useless for callers that need to branch on the actual HTTP status (e.g. to treat a 2xx as success and a 4xx as a client error). Mirror the :method pseudo-header the server path already writes and populate headers[":status"] with the numeric response code before firing onHeaders. Comes with two unit tests that feed a minimal HTTP/1.1 response through the filter and assert both :status and the original status reason land on the callbacks' header map — guarding against future regressions in onHeadersComplete's client branch.
caleb2h
pushed a commit
that referenced
this pull request
Apr 19, 2026
HttpAsyncClient hosts one outbound TCP connection per send(), installs the existing HttpCodecFilter in client mode as a read-only filter, and formats the request bytes directly so callers keep full control over method, path, and headers. Each request context implements DeferredDeletable; on completion the client hands ownership to Dispatcher::deferredDelete so the connection, codec filter, and callbacks tear down past the current callback frame rather than unwinding from inside their own callbacks. Body handling reflects a quirk of the codec in client mode: each body chunk is emitted twice, once inline and once again with the accumulated body at message-complete. The client takes only the end_stream delivery so the response body matches what arrived on the wire.
This was referenced Apr 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HttpAsyncClient— a fire-and-forget HTTP/1.1 client built on top ofHttpCodecFilter(client mode) and the MCP socketinterface, for use by the forthcoming SSE gateway and other dispatcher-thread callers that need to emit outbound JSON-RPC-over-HTTP
without blocking on DNS or shelling out to libcurl.
send()creates an isolatedRequestContextowning its own connection + codec filter; completion hands the context toDispatcher::deferredDeleteso teardown runs past the current callback frame (same lifetime pattern as the crash fixes in Fix connection lifetime and init-path crashes #212).RealListenerTestBase: successful POST round-trip with header + body assertions, malformed-URLrejection (no callback fires), and malformed-response → error-callback path.
Design notes
send()assertsdispatcher_.isThreadSafe(); all callbacks fire on the dispatcher thread, matching theproject-wide convention.
http(s)://<IPv4>[:port][/path]. No DNS here — callers running on the dispatcher threadmust not block. Unsupported shapes return
falsefromsend()so callers get a clean error instead of a mystery connect attempt.HttpCodecFilter's client-mode write path rewrites outgoing bytes into its own MCP-orientedrequest shape; we format the request ourselves so callers keep full control over method, path, and headers.
onBodydedup. Client-modeHttpCodecFilteremits each body chunk twice — once inline (end_stream=false) and once accumulatedat message-complete (
end_stream=true). We only take the final delivery.onMessageComplete. Closing the connection while the codec parser is mid-dispatchflushes the readbuffer and makes its post-return
drain(consumed)overshoot ("Drain size exceeds buffer length"). Instead,finishRequesthands thecontext to
Dispatcher::deferredDelete; the connection and filter tear down at the next dispatcher pass.Files
include/mcp/http/http_async_client.h— public API (HttpRequest,HttpResponse,HttpAsyncClient).src/http/http_async_client.cc— implementation; per-requestRequestContextinheritsConnectionCallbacks,HttpCodecFilter::MessageCallbacks, andDeferredDeletable.tests/http/test_http_async_client.cc— 3 integration tests over a real ephemeral-port localhost listener.Test plan
HttpAsyncClientTest.PostRoundTripDeliversResponseBody— full POST → server sees correct wire format → client parses 200 + body +headers.
HttpAsyncClientTest.RejectsMalformedUrl—send()returnsfalse, neither callback fires (50 ms quiescent window).HttpAsyncClientTest.MalformedResponseFiresErrorCallback— non-HTTP bytes →onError→ error callback with codec message.http_transport_socket.h.send(); teardown viadeferredDelete.Context
Second carve-out from PR #211's larger "feature/gateway-sse-transport" bundle, following the crash fixes in #212. This PR lands the outbound HTTP/1.1 client that the SSE gateway needs (GET /sse endpoint announcement, POST /callback response routing) without the SSE server transport itself, which will follow as PR C. Tests for the remaining integration-level gaps call out in #212's "out-of-scope" list will follow as PR D.