aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/include
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-09 11:02:41 +0200
committerGitHub Enterprise <[email protected]>2026-04-09 11:02:41 +0200
commit5900f6a6d892fbe582c46063cc399a840e60ef2e (patch)
tree76735ff6de39c2c515a866ecc9d7b4309d63669d /src/zenhttp/include
parentmigrate from http_parser to llhttp (#929) (diff)
downloadzen-5900f6a6d892fbe582c46063cc399a840e60ef2e.tar.xz
zen-5900f6a6d892fbe582c46063cc399a840e60ef2e.zip
Add async HTTP client (curl_multi + ASIO) (#918)
- Adds `AsyncHttpClient` — an asynchronous HTTP client using `curl_multi_socket_action` integrated with ASIO for event-driven I/O. Supports GET, POST, PUT, DELETE, HEAD with both callback-based and `std::future`-based APIs. - Extracts shared curl helpers (callbacks, URL encoding, header construction, error mapping) into `httpclientcurlhelpers.h`, eliminating duplication between the sync and async implementations. ## Design - All curl_multi state is serialized on an `asio::strand`, safe with multi-threaded io_contexts. - Two construction modes: owned io_context (creates internal thread) or external io_context (caller runs the loop). - Socket readiness is detected via `asio::ip::tcp::socket::async_wait` driven by curl's `CURLMOPT_SOCKETFUNCTION`/`CURLMOPT_TIMERFUNCTION` — no polling, sub-millisecond latency. - Completion callbacks are dispatched off the strand onto the io_context so slow callbacks don't starve the curl event loop. Exceptions in callbacks are caught and logged. ## Files | File | Change | |------|--------| | `zenhttp/include/zenhttp/asynchttpclient.h` | New public header | | `zenhttp/clients/asynchttpclient.cpp` | Implementation (~1000 lines) | | `zenhttp/clients/httpclientcurlhelpers.h` | Shared curl helpers extracted from sync client | | `zenhttp/clients/httpclientcurl.cpp` | Removed duplicated helpers, uses shared header | | `zenhttp/asynchttpclient_test.cpp` | 8 test cases: verbs, payloads, callbacks, concurrency, external io_context, connection errors | | `zenhttp/zenhttp.cpp` | Forcelink registration for new tests |
Diffstat (limited to 'src/zenhttp/include')
-rw-r--r--src/zenhttp/include/zenhttp/asynchttpclient.h123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/zenhttp/include/zenhttp/asynchttpclient.h b/src/zenhttp/include/zenhttp/asynchttpclient.h
new file mode 100644
index 000000000..58429349d
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/asynchttpclient.h
@@ -0,0 +1,123 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "zenhttp.h"
+
+#include <zenhttp/httpclient.h>
+
+#include <functional>
+#include <future>
+#include <memory>
+
+namespace asio {
+class io_context;
+}
+
+namespace zen {
+
+/// Completion callback for async HTTP operations.
+using AsyncHttpCallback = std::function<void(HttpClient::Response)>;
+
+/** Asynchronous HTTP client backed by curl_multi and ASIO.
+ *
+ * Uses curl_multi_perform() driven by an ASIO steady_timer to process
+ * transfers without blocking the caller. All curl_multi operations are
+ * serialized on an internal strand; callers may issue requests from any
+ * thread, and the io_context may have multiple threads.
+ *
+ * Two construction modes:
+ * - Owned io_context: creates an internal thread (self-contained).
+ * - External io_context: caller runs the event loop.
+ *
+ * Completion callbacks are dispatched on the io_context (not the internal
+ * strand), so a slow callback will not block the curl poll loop. Future-
+ * based wrappers (Get, Post, ...) return a std::future<Response> for
+ * callers that prefer blocking on a result.
+ */
+class AsyncHttpClient
+{
+public:
+ using Response = HttpClient::Response;
+ using KeyValueMap = HttpClient::KeyValueMap;
+
+ /// Construct with an internally-owned io_context and thread.
+ explicit AsyncHttpClient(std::string_view BaseUri, const HttpClientSettings& Settings = {});
+
+ /// Construct with an externally-managed io_context. The io_context must
+ /// outlive this client and must be running (via run()) on at least one thread.
+ AsyncHttpClient(std::string_view BaseUri, asio::io_context& IoContext, const HttpClientSettings& Settings = {});
+
+ ~AsyncHttpClient();
+
+ AsyncHttpClient(const AsyncHttpClient&) = delete;
+ AsyncHttpClient& operator=(const AsyncHttpClient&) = delete;
+
+ // ── Callback-based API ──────────────────────────────────────────────
+
+ void AsyncGet(std::string_view Url,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ void AsyncHead(std::string_view Url, AsyncHttpCallback Callback, const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncDelete(std::string_view Url, AsyncHttpCallback Callback, const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncPost(std::string_view Url,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ void AsyncPost(std::string_view Url, const IoBuffer& Payload, AsyncHttpCallback Callback, const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncPost(std::string_view Url,
+ const IoBuffer& Payload,
+ ZenContentType ContentType,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncPut(std::string_view Url,
+ const IoBuffer& Payload,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ void AsyncPut(std::string_view Url, AsyncHttpCallback Callback, const KeyValueMap& Parameters = {});
+
+ // ── Future-based API ────────────────────────────────────────────────
+
+ [[nodiscard]] std::future<Response> Get(std::string_view Url,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ [[nodiscard]] std::future<Response> Head(std::string_view Url, const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Delete(std::string_view Url, const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Post(std::string_view Url,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ [[nodiscard]] std::future<Response> Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Post(std::string_view Url,
+ const IoBuffer& Payload,
+ ZenContentType ContentType,
+ const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Put(std::string_view Url,
+ const IoBuffer& Payload,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ [[nodiscard]] std::future<Response> Put(std::string_view Url, const KeyValueMap& Parameters = {});
+
+private:
+ struct Impl;
+ std::unique_ptr<Impl> m_Impl;
+};
+
+void asynchttpclient_test_forcelink(); // internal
+
+} // namespace zen