Skip to content

Commit b820ab8

Browse files
author
gophergogo
committed
Add HttpAsyncClient built on HttpCodecFilter (#213)
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.
1 parent 8c8ec21 commit b820ab8

3 files changed

Lines changed: 469 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ set(MCP_CLIENT_SERVER_SOURCES
489489
set(MCP_HTTP_SOURCES
490490
src/http/http_parser.cc
491491
src/http/sse_parser.cc
492+
src/http/http_async_client.cc # Async HTTP/1.1 client built on codec filter
492493
src/transport/http_sse_transport_socket.cc # HTTP+SSE with layered architecture
493494
src/transport/https_sse_transport_factory.cc # HTTPS+SSE factory
494495
)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#ifndef MCP_HTTP_HTTP_ASYNC_CLIENT_H
2+
#define MCP_HTTP_HTTP_ASYNC_CLIENT_H
3+
4+
#include <functional>
5+
#include <map>
6+
#include <memory>
7+
#include <string>
8+
9+
#include "mcp/event/event_loop.h"
10+
#include "mcp/network/socket_interface.h"
11+
#include "mcp/network/transport_socket.h"
12+
13+
namespace mcp {
14+
namespace http {
15+
16+
// Request handed to HttpAsyncClient::send.
17+
// The url is a full absolute URL (scheme://host[:port]/path). host must be
18+
// a dotted-quad IPv4 literal — hostname resolution is not performed here
19+
// so that callers running on the dispatcher thread never block on DNS.
20+
struct HttpRequest {
21+
std::string method{"POST"};
22+
std::string url;
23+
std::map<std::string, std::string> headers;
24+
std::string body;
25+
};
26+
27+
// Response delivered to the HttpResponseCallback on success.
28+
struct HttpResponse {
29+
int status_code{0};
30+
std::string status_text;
31+
std::map<std::string, std::string> headers;
32+
std::string body;
33+
};
34+
35+
// Exactly one of these fires per send().
36+
using HttpResponseCallback = std::function<void(HttpResponse)>;
37+
using HttpErrorCallback = std::function<void(const std::string& error)>;
38+
39+
/**
40+
* HttpAsyncClient — minimal fire-and-forget HTTP/1.1 client.
41+
*
42+
* Each send() creates an isolated request context: its own client
43+
* connection, its own HttpCodecFilter (client mode), and its own
44+
* callbacks. Request contexts are owned by the client until they
45+
* complete, then handed to Dispatcher::deferredDelete so teardown
46+
* runs past the current callback frame (Envoy-style lifetime, which
47+
* avoids destroying a connection from inside its own callback).
48+
*
49+
* All public methods must be invoked from the dispatcher thread —
50+
* this matches the project-wide convention that mutating network
51+
* objects off-thread is undefined.
52+
*/
53+
class HttpAsyncClient {
54+
public:
55+
HttpAsyncClient(
56+
event::Dispatcher& dispatcher,
57+
network::SocketInterface& socket_interface,
58+
std::unique_ptr<network::TransportSocketFactoryBase> transport_factory);
59+
~HttpAsyncClient();
60+
61+
HttpAsyncClient(const HttpAsyncClient&) = delete;
62+
HttpAsyncClient& operator=(const HttpAsyncClient&) = delete;
63+
64+
/**
65+
* Send an HTTP request. Exactly one of on_response or on_error will
66+
* be invoked (on the dispatcher thread) before the request context
67+
* is torn down. Returns false if the URL could not be parsed; in
68+
* that case no callback fires.
69+
*/
70+
bool send(const HttpRequest& request,
71+
HttpResponseCallback on_response,
72+
HttpErrorCallback on_error);
73+
74+
private:
75+
class RequestContext;
76+
77+
// Called by RequestContext when it completes (success or failure).
78+
// Extracts the unique_ptr from active_requests_ and hands it to
79+
// Dispatcher::deferredDelete so the connection and filter tear down
80+
// after the current callback returns.
81+
void finishRequest(RequestContext* ctx);
82+
83+
event::Dispatcher& dispatcher_;
84+
network::SocketInterface& socket_interface_;
85+
std::unique_ptr<network::TransportSocketFactoryBase> transport_factory_;
86+
std::map<RequestContext*, std::unique_ptr<RequestContext>> active_requests_;
87+
};
88+
89+
using HttpAsyncClientPtr = std::unique_ptr<HttpAsyncClient>;
90+
91+
} // namespace http
92+
} // namespace mcp
93+
94+
#endif // MCP_HTTP_HTTP_ASYNC_CLIENT_H

0 commit comments

Comments
 (0)