Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ set(MCP_CLIENT_SERVER_SOURCES
set(MCP_HTTP_SOURCES
src/http/http_parser.cc
src/http/sse_parser.cc
src/http/http_async_client.cc # Async HTTP/1.1 client built on codec filter
src/transport/http_sse_transport_socket.cc # HTTP+SSE with layered architecture
src/transport/https_sse_transport_factory.cc # HTTPS+SSE factory
)
Expand Down
94 changes: 94 additions & 0 deletions include/mcp/http/http_async_client.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#ifndef MCP_HTTP_HTTP_ASYNC_CLIENT_H
#define MCP_HTTP_HTTP_ASYNC_CLIENT_H

#include <functional>
#include <map>
#include <memory>
#include <string>

#include "mcp/event/event_loop.h"
#include "mcp/network/socket_interface.h"
#include "mcp/network/transport_socket.h"

namespace mcp {
namespace http {

// Request handed to HttpAsyncClient::send.
// The url is a full absolute URL (scheme://host[:port]/path). host must be
// a dotted-quad IPv4 literal — hostname resolution is not performed here
// so that callers running on the dispatcher thread never block on DNS.
struct HttpRequest {
std::string method{"POST"};
std::string url;
std::map<std::string, std::string> headers;
std::string body;
};

// Response delivered to the HttpResponseCallback on success.
struct HttpResponse {
int status_code{0};
std::string status_text;
std::map<std::string, std::string> headers;
std::string body;
};

// Exactly one of these fires per send().
using HttpResponseCallback = std::function<void(HttpResponse)>;
using HttpErrorCallback = std::function<void(const std::string& error)>;

/**
* HttpAsyncClient — minimal fire-and-forget HTTP/1.1 client.
*
* Each send() creates an isolated request context: its own client
* connection, its own HttpCodecFilter (client mode), and its own
* callbacks. Request contexts are owned by the client until they
* complete, then handed to Dispatcher::deferredDelete so teardown
* runs past the current callback frame (Envoy-style lifetime, which
* avoids destroying a connection from inside its own callback).
*
* All public methods must be invoked from the dispatcher thread —
* this matches the project-wide convention that mutating network
* objects off-thread is undefined.
*/
class HttpAsyncClient {
public:
HttpAsyncClient(
event::Dispatcher& dispatcher,
network::SocketInterface& socket_interface,
std::unique_ptr<network::TransportSocketFactoryBase> transport_factory);
~HttpAsyncClient();

HttpAsyncClient(const HttpAsyncClient&) = delete;
HttpAsyncClient& operator=(const HttpAsyncClient&) = delete;

/**
* Send an HTTP request. Exactly one of on_response or on_error will
* be invoked (on the dispatcher thread) before the request context
* is torn down. Returns false if the URL could not be parsed; in
* that case no callback fires.
*/
bool send(const HttpRequest& request,
HttpResponseCallback on_response,
HttpErrorCallback on_error);

private:
class RequestContext;

// Called by RequestContext when it completes (success or failure).
// Extracts the unique_ptr from active_requests_ and hands it to
// Dispatcher::deferredDelete so the connection and filter tear down
// after the current callback returns.
void finishRequest(RequestContext* ctx);

event::Dispatcher& dispatcher_;
network::SocketInterface& socket_interface_;
std::unique_ptr<network::TransportSocketFactoryBase> transport_factory_;
std::map<RequestContext*, std::unique_ptr<RequestContext>> active_requests_;
};

using HttpAsyncClientPtr = std::unique_ptr<HttpAsyncClient>;

} // namespace http
} // namespace mcp

#endif // MCP_HTTP_HTTP_ASYNC_CLIENT_H
6 changes: 6 additions & 0 deletions src/filter/http_codec_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,12 @@ HttpCodecFilter::ParserCallbacks::onHeadersComplete() {
}
parent_.current_stream_->headers[":method"] = method_str;
parent_.current_stream_->method = method_str;
} else {
// Client mode: surface numeric response status as :status pseudo-header.
// Callers (HttpAsyncClient, etc.) need the numeric code, not just the
// reason phrase that onStatus already captures into headers["status"].
auto status_code = static_cast<uint16_t>(parent_.parser_->statusCode());
parent_.current_stream_->headers[":status"] = std::to_string(status_code);
}

// Check keep-alive
Expand Down
Loading
Loading