diff options
| author | Stefan Boberg <[email protected]> | 2026-04-09 11:02:41 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-09 11:02:41 +0200 |
| commit | 5900f6a6d892fbe582c46063cc399a840e60ef2e (patch) | |
| tree | 76735ff6de39c2c515a866ecc9d7b4309d63669d /src/zenhttp/include | |
| parent | migrate from http_parser to llhttp (#929) (diff) | |
| download | zen-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.h | 123 |
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 |