Files
vdm/core/include/vdm/net/http_client.hpp
T
samiandClaude Sonnet 5 bd1bc029f3 core: net/http_client — libcurl multi wrapper (stage 2)
One HttpClient owns a small pool of workers, each with its own CURLM; an
easy handle lives on one worker for its life. Public start/pause/resume/
cancel enqueue a command + curl_multi_wakeup(); callbacks (on_head /
on_data / on_finished) run on the worker thread and return a DataAction
(proceed / pause / abort). Covers redirects (final-response head only),
ranges (inclusive ByteRange -> CURLOPT_RANGE), proxy/SOCKS5, basic/digest
auth, cookies, verbatim headers, stall detection, a coarse recv-rate cap,
and a curl_share DNS/TLS cache across workers. CURLcode + HTTP status ->
vdm::Error in net/curl_error. A probe is on_head returning abort: it
finishes successfully (head_complete), not canceled.

Tests drive tools/testserver: full GET, ranged 206, redirect chain, 404
-> not_found, connection refused -> connect_failed, HEAD probe + ranged
0-0 probe (no body), cancel mid-transfer, pause/resume completes. Skip
cleanly if testserver isn't in the tree.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
2026-09-09 23:31:09 +04:00

126 lines
4.6 KiB
C++

// vdm/net/http_client.hpp — a libcurl-multi wrapper for the download engine.
//
// One HttpClient owns a small pool of worker threads, each with its own curl_multi
// (curl handles are not thread-safe; a handle lives on exactly one worker for its life).
// The engine hands it a Request plus callbacks and gets back a Transfer handle it can
// pause / resume / cancel from any thread.
//
// Layering: this is the ONLY core header that pulls in libcurl, and only in its .cpp —
// nothing here exposes a curl type. No JSON / SQL / Qt / RPC (CLAUDE.md §3).
//
// Callbacks run ON THE WORKER THREAD, one transfer at a time for a given Transfer.
// They must not block (that stalls every other transfer on that worker) and must not
// call back into this Transfer's pause/resume/cancel re-entrantly — post that work
// elsewhere. on_data must not allocate on the hot path (AGENT-CORE); the ring buffer it
// writes into is preallocated by the caller (stage 4).
//
// This header compiles standalone.
#ifndef VDM_NET_HTTP_CLIENT_HPP
#define VDM_NET_HTTP_CLIENT_HPP
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include "vdm/net/http_types.hpp"
#include "vdm/util/bytes.hpp"
#include "vdm/util/result.hpp"
namespace vdm::net {
// What on_data tells the client to do with the transfer after this chunk.
enum class DataAction {
proceed, // keep receiving
pause, // stop receiving; resumes on Transfer::resume()
abort, // end the transfer now (finishes with Error::canceled)
};
struct TransferStats {
std::uint64_t bytes_received = 0;
long http_status = 0;
std::string effective_url;
// Timings in milliseconds (from CURLINFO_*_TIME_T), 0 if the phase didn't happen.
long namelookup_ms = 0;
long connect_ms = 0;
long appconnect_ms = 0; // TLS handshake done
long starttransfer_ms = 0; // first response byte
long total_ms = 0;
};
struct TransferCallbacks {
// Response headers are in. Called at most once per transfer (a followed redirect's
// intermediate headers are not delivered). If the caller only wanted headers (a
// probe), return DataAction::abort here.
std::function<DataAction(const ResponseHead &)> on_head;
// A chunk of body bytes. The span is valid only for the duration of the call.
std::function<DataAction(ConstByteSpan)> on_data;
// The transfer ended — success carries stats, failure carries the mapped error
// (ErrorInfo.http_status is set for HTTP-status failures). Always called exactly once,
// last.
std::function<void(Result<TransferStats>)> on_finished;
};
class HttpClient;
// Lightweight handle to a running transfer. Copyable (shared state). All methods are
// safe to call from any thread; they post to the owning worker and return immediately.
// Dropping the last handle does NOT cancel — call cancel() for that.
class Transfer {
public:
Transfer() = default;
[[nodiscard]] std::uint64_t id() const noexcept;
[[nodiscard]] bool valid() const noexcept { return static_cast<bool>(state_); }
void pause(); // no-op if already paused / finished
void resume(); // no-op if not paused / finished
void cancel(); // idempotent; on_finished fires with Error::canceled
private:
friend class HttpClient;
struct State;
explicit Transfer(std::shared_ptr<State> s) : state_(std::move(s)) {}
std::shared_ptr<State> state_;
};
class HttpClient {
public:
struct Options {
// Worker threads, each with its own curl_multi. New transfers are assigned
// round-robin. 0 => pick from hardware_concurrency (min 1, max 4).
unsigned workers = 0;
// CURLMOPT_MAX_TOTAL_CONNECTIONS per worker (0 = curl default).
long max_connections_per_worker = 0;
// Shared DNS + TLS-session cache across this client's workers (curl_share).
bool share_dns_and_tls = true;
};
HttpClient(); // default Options
explicit HttpClient(Options opts);
~HttpClient();
HttpClient(const HttpClient &) = delete;
HttpClient &operator=(const HttpClient &) = delete;
[[nodiscard]] unsigned worker_count() const noexcept;
// Start a transfer. The Request is consumed (moved). Returns an invalid Transfer and
// never calls the callbacks only if the client is shutting down; every other failure
// (bad URL, DNS, ...) is delivered through on_finished.
Transfer start(Request req, TransferCallbacks cbs);
private:
friend class Transfer; // Transfer posts pause/resume/cancel commands to Impl
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace vdm::net
#endif // VDM_NET_HTTP_CLIENT_HPP