cofetch
Chainable, high-performance async HTTP client for C++ event loops
Loading...
Searching...
No Matches
cofetch.h
Go to the documentation of this file.
1#pragma once
2// cofetch: async HTTP client on top of libcurl's multi interface and ASIO.
3//
4// One implementation, any ASIO completion token. C++17 and up; the
5// co_await interface additionally needs C++20.
6//
7// // fluent chain, finished by the HTTP verb (zero-overhead hot path):
8// http.request(url)
9// .headers({"content-type: application/json"})
10// .body(payload)
11// .post([](std::error_code ec, const cofetch::Response& res) {});
12//
13// // std::future:
14// auto fut = http.async_get(url, asio::use_future);
15//
16// // .then()-style chaining (see test/cofetch_tests.cpp):
17// http.async_get(url, asio::deferred)(asio::deferred(next))(handler);
18//
19// // C++20 coroutine:
20// auto res = co_await http.async_get(url, asio::use_awaitable);
21//
22// Drive it with io_context::run(), or io_context::poll() in a busy loop.
23// Requests are cancellable through asio's cancellation slots
24// (asio::cancel_after, asio::bind_cancellation_slot, co_spawn).
25// Not thread-safe: run the client and its io_context on one thread.
26// Define COFETCH_USE_BOOST_ASIO to build on Boost.Asio instead of
27// standalone asio. The API is identical; cofetch::error_code follows the
28// flavor (std::error_code, or boost::system::error_code) — curl category.
29#if defined(COFETCH_USE_BOOST_ASIO)
30#include <boost/asio.hpp>
31#else
32#include <asio.hpp>
33#endif
34//
35#include <curl/curl.h>
36
37#include <chrono>
38#include <cstdint>
39#include <functional>
40#include <list>
41#include <memory>
42#include <optional>
43#include <string>
44#include <string_view>
45#include <system_error>
46#include <unordered_map>
47#include <utility>
48#include <vector>
49
50namespace cofetch {
51
52#if defined(COFETCH_USE_BOOST_ASIO)
53namespace net = boost::asio;
54// The error type follows the asio flavor so completion tokens recognise
55// it (Boost.Asio only unwraps boost::system::error_code).
56using error_code = boost::system::error_code;
57using error_category = boost::system::error_category;
58#else
59namespace net = asio;
60using error_code = std::error_code;
61using error_category = std::error_category;
62#endif
63
68 class category final : public error_category {
69 public:
70 const char* name() const noexcept override { return "curl"; }
71 std::string message(int ev) const override {
72 return curl_easy_strerror(static_cast<CURLcode>(ev));
73 }
74 };
75 static category instance;
76 return instance;
77}
78
79inline error_code make_error_code(CURLcode code) {
80 return {static_cast<int>(code), curl_category()};
81}
82
83namespace detail {
84
85// HTTP field names are case-insensitive (RFC 9110 §5.1), so the header map
86// hashes and compares them without regard to case. ASCII-only folding — field
87// names are ASCII tokens — which also sidesteps std::tolower's locale and
88// signed-char pitfalls.
89inline char ascii_lower(char c) {
90 return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
91}
92struct CiHash {
93 size_t operator()(std::string_view s) const noexcept {
94 size_t h = 0;
95 for (char c : s) h = h * 31 + static_cast<unsigned char>(ascii_lower(c));
96 return h;
97 }
98};
99struct CiEqual {
100 bool operator()(std::string_view a, std::string_view b) const noexcept {
101 if (a.size() != b.size()) return false;
102 for (size_t i = 0; i < a.size(); ++i) {
103 if (ascii_lower(a[i]) != ascii_lower(b[i])) return false;
104 }
105 return true;
106 }
107};
108
109} // namespace detail
110
111class Response {
112 public:
113 // Case-insensitive field-name -> value map (see headers()).
114 using Headers = std::unordered_map<std::string, std::string, detail::CiHash,
116
117 Response() = default;
118 Response(CURLcode curl_code, long http_code, std::string data,
119 std::string header_data)
120 : curl_code_(curl_code),
121 http_code_(http_code),
122 data_(std::move(data)),
123 header_data_(std::move(header_data)) {}
124
128 bool is_ok() const {
129 return curl_code_ == CURLE_OK && http_code_ >= 200 && http_code_ < 300;
130 }
131
136 const char* error() const { return curl_easy_strerror(curl_code_); }
137
145 Headers headers() const {
146 Headers out;
147 for_each_field([&](std::string_view name, std::string_view value) {
148 const auto [it, inserted] = out.try_emplace(std::string(name), value);
149 if (!inserted) {
150 it->second += ", ";
151 it->second += value;
152 }
153 });
154 return out;
155 }
156
161 std::optional<std::string> header(std::string_view name) const {
162 std::optional<std::string> found;
163 for_each_field([&](std::string_view field, std::string_view value) {
164 if (!detail::CiEqual{}(field, name)) return;
165 if (found) {
166 *found += ", ";
167 *found += value;
168 } else {
169 found = std::string(value);
170 }
171 });
172 return found;
173 }
174
175 CURLcode curl_code_ = CURLE_OK;
176 long http_code_ = 0;
177 std::string data_;
178 std::string header_data_;
179
180 private:
181 // Iterate the "name: value" fields in header_data_, trimmed, skipping the
182 // status line and blank separators. The views point into header_data_, so
183 // they are valid only for the lifetime of this Response.
184 template <typename F>
185 void for_each_field(F&& f) const {
186 std::string_view sv(header_data_);
187 size_t pos = 0;
188 while (pos < sv.size()) {
189 const size_t nl = sv.find('\n', pos);
190 std::string_view line =
191 sv.substr(pos, (nl == std::string_view::npos ? sv.size() : nl) - pos);
192 pos = (nl == std::string_view::npos) ? sv.size() : nl + 1;
193 if (!line.empty() && line.back() == '\r') line.remove_suffix(1);
194 const size_t colon = line.find(':');
195 if (colon == std::string_view::npos) continue; // status line or blank
196 f(trim(line.substr(0, colon)), trim(line.substr(colon + 1)));
197 }
198 }
199
200 // Strip leading/trailing HTTP optional whitespace (space and htab).
201 static std::string_view trim(std::string_view s) {
202 const auto b = s.find_first_not_of(" \t");
203 if (b == std::string_view::npos) return {};
204 return s.substr(b, s.find_last_not_of(" \t") - b + 1);
205 }
206};
207
211class Request {
212 public:
213 enum class Method { GET, POST, PUT, PATCH, DEL };
214
215 explicit Request(std::string url) : url_(std::move(url)) {}
216
218 method_ = m;
219 return *this;
220 }
221 Request& headers(std::vector<std::string> h) {
222 headers_ = std::move(h);
223 return *this;
224 }
225 Request& body(std::string b) {
226 body_ = std::move(b);
227 return *this;
228 }
234 Request& timeout(std::chrono::milliseconds t) {
235 timeout_ = t;
236 return *this;
237 }
242 Request& follow_redirects(long max = 30) {
243 max_redirects_ = max;
244 return *this;
245 }
252 Request& curl(std::function<void(CURL*)> fn) {
253 curl_setup_ = std::move(fn);
254 return *this;
255 }
256
257 std::string url_;
259 std::vector<std::string> headers_;
260 std::string body_;
261 std::chrono::milliseconds timeout_{std::chrono::seconds{5}};
262 long max_redirects_ = 0; // 0: do not follow redirects
263 std::function<void(CURL*)> curl_setup_;
264};
265
266class Client {
267 public:
273 explicit Client(net::io_context& io,
274 size_t max_pooled_connections = kDefaultMaxPooledHandles)
275 : io_(io), timer_(io), max_pooled_handles_(max_pooled_connections) {
276 curl_global_init(CURL_GLOBAL_ALL);
277 multi_ = curl_multi_init();
278 curl_multi_setopt(multi_, CURLMOPT_SOCKETFUNCTION, socket_cb);
279 curl_multi_setopt(multi_, CURLMOPT_SOCKETDATA, this);
280 curl_multi_setopt(multi_, CURLMOPT_TIMERFUNCTION, timer_cb);
281 curl_multi_setopt(multi_, CURLMOPT_TIMERDATA, this);
282 }
283
284 Client(const Client&) = delete;
285 Client& operator=(const Client&) = delete;
286
293 *alive_ = false;
294 timer_.cancel();
295 for (const auto& [fd, state] : sockets_) {
296 error_code ignored;
297 state->socket.close(ignored);
298 }
299 curl_multi_cleanup(multi_);
300 for (CURL* eh : pool_) {
301 curl_easy_cleanup(eh);
302 }
303 curl_global_cleanup();
304 }
305
317 template <typename CompletionToken>
318 auto async_perform(Request req, CompletionToken&& token) {
319 return net::async_initiate<CompletionToken, void(error_code, Response)>(
320 Initiation{this}, token, std::move(req));
321 }
322
323 template <typename CompletionToken>
324 auto async_get(std::string url, CompletionToken&& token) {
325 return async_perform(Request(std::move(url)),
326 std::forward<CompletionToken>(token));
327 }
328
329 template <typename CompletionToken>
330 auto async_post(std::string url, std::string body, CompletionToken&& token) {
331 return async_perform(Request(std::move(url))
332 .method(Request::Method::POST)
333 .body(std::move(body)),
334 std::forward<CompletionToken>(token));
335 }
336
346 public:
347 RequestBuilder(Client& client, std::string url)
348 : client_(client), req_(std::move(url)) {}
349
350 RequestBuilder& headers(std::vector<std::string> h) {
351 req_.headers(std::move(h));
352 return *this;
353 }
354 RequestBuilder& body(std::string b) {
355 req_.body(std::move(b));
356 return *this;
357 }
358 RequestBuilder& timeout(std::chrono::milliseconds t) {
359 req_.timeout(t);
360 return *this;
361 }
363 req_.follow_redirects(max);
364 return *this;
365 }
366 RequestBuilder& curl(std::function<void(CURL*)> fn) {
367 req_.curl(std::move(fn));
368 return *this;
369 }
370
371 template <typename CompletionToken>
372 auto get(CompletionToken&& token) {
373 return perform(Request::Method::GET,
374 std::forward<CompletionToken>(token));
375 }
376 template <typename CompletionToken>
377 auto post(CompletionToken&& token) {
378 return perform(Request::Method::POST,
379 std::forward<CompletionToken>(token));
380 }
381 template <typename CompletionToken>
382 auto put(CompletionToken&& token) {
383 return perform(Request::Method::PUT,
384 std::forward<CompletionToken>(token));
385 }
386 template <typename CompletionToken>
387 auto patch(CompletionToken&& token) {
388 return perform(Request::Method::PATCH,
389 std::forward<CompletionToken>(token));
390 }
391 template <typename CompletionToken>
392 auto del(CompletionToken&& token) {
393 return perform(Request::Method::DEL,
394 std::forward<CompletionToken>(token));
395 }
396
397 private:
398 template <typename CompletionToken>
399 auto perform(Request::Method m, CompletionToken&& token) {
400 req_.method(m);
401 return client_.async_perform(std::move(req_),
402 std::forward<CompletionToken>(token));
403 }
404
405 Client& client_;
406 Request req_;
407 };
408
412 RequestBuilder request(std::string url) {
413 return RequestBuilder(*this, std::move(url));
414 }
415
416 int pending_requests() const { return running_; }
417
418 private:
419 using Handler = net::any_completion_handler<void(error_code, Response)>;
420
421 // Initiation for async_perform. Exposing the io_context executor lets
422 // executor-aware tokens work — asio::cancel_after, for one, builds its
423 // timeout timer on Initiation::executor_type.
424 struct Initiation {
425 Client* self;
426 using executor_type = net::io_context::executor_type;
427 executor_type get_executor() const noexcept {
428 return self->io_.get_executor();
429 }
430 template <typename H>
431 void operator()(H handler, Request r) const {
432 self->start(std::move(r), Handler(std::move(handler)));
433 }
434 };
435
436 // Default cap on idle easy handles kept for reuse; beyond this they are
437 // freed so a burst of concurrent requests does not pin memory forever.
438 // Overridable per client through the constructor.
439 static constexpr size_t kDefaultMaxPooledHandles = 64;
440
441 struct Transfer {
442 Transfer(CURL* e, Handler h) : eh(e), handler(std::move(h)) {}
443 CURL* eh;
444 curl_slist* headers = nullptr;
445 std::string body;
446 std::string buffer;
447 std::string header_buffer;
448 Handler handler;
449 std::list<Transfer>::iterator self;
450 std::uint64_t id = 0;
451 // Set when Request::curl ran on this handle: unknown options must be
452 // wiped (curl_easy_reset + configure_handle) before the handle is
453 // pooled, or they would leak into whatever request draws it next.
454 bool scrub_on_done = false;
455 };
456
457 struct SocketState : std::enable_shared_from_this<SocketState> {
458 explicit SocketState(net::io_context& io) : socket(io) {}
459 net::ip::tcp::socket socket;
460 int watch = 0; // current CURL_POLL_* interest
461 bool read_armed = false;
462 bool write_armed = false;
463 };
464
465 void start(Request r, Handler h) {
466 CURL* eh = nullptr;
467 if (pool_.empty()) {
468 eh = curl_easy_init();
469 configure_handle(eh);
470 } // pooled handles keep their static options; no curl_easy_reset
471 else {
472 eh = pool_.back();
473 pool_.pop_back();
474 }
475
476 transfers_.emplace_front(eh, std::move(h));
477 const auto it = transfers_.begin();
478 it->self = it;
479 it->body = std::move(r.body_);
480
481 curl_easy_setopt(eh, CURLOPT_URL, r.url_.c_str());
482 curl_easy_setopt(eh, CURLOPT_WRITEDATA, &*it);
483 curl_easy_setopt(eh, CURLOPT_HEADERDATA, &it->header_buffer);
484 curl_easy_setopt(eh, CURLOPT_PRIVATE, &*it);
485 curl_easy_setopt(eh, CURLOPT_TIMEOUT_MS,
486 static_cast<long>(r.timeout_.count()));
487 // Always set: clears the previous transfer's values on pooled handles.
488 curl_easy_setopt(eh, CURLOPT_FOLLOWLOCATION,
489 r.max_redirects_ != 0 ? 1L : 0L);
490 curl_easy_setopt(eh, CURLOPT_MAXREDIRS, r.max_redirects_);
491
492 curl_slist* chunk = nullptr;
493 for (const auto& header : r.headers_) {
494 chunk = curl_slist_append(chunk, header.c_str());
495 }
496 // Always set: clears the previous transfer's list on pooled handles.
497 curl_easy_setopt(eh, CURLOPT_HTTPHEADER, chunk);
498 it->headers = chunk;
499
500 switch (r.method_) {
502 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST, nullptr);
503 curl_easy_setopt(eh, CURLOPT_HTTPGET, 1L);
504 break;
506 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST, nullptr);
507 curl_easy_setopt(eh, CURLOPT_POST, 1L);
508 set_body(*it);
509 break;
511 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST, "PUT");
512 curl_easy_setopt(eh, CURLOPT_POST, 1L);
513 set_body(*it);
514 break;
516 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST, "PATCH");
517 curl_easy_setopt(eh, CURLOPT_POST, 1L);
518 set_body(*it);
519 break;
521 curl_easy_setopt(eh, CURLOPT_CUSTOMREQUEST, "DELETE");
522 curl_easy_setopt(eh, CURLOPT_POST, 1L);
523 set_body(*it);
524 break;
525 }
526
527 // The escape hatch runs last so it can override anything above.
528 if (r.curl_setup_) {
529 it->scrub_on_done = true;
530 r.curl_setup_(eh);
531 }
532
533 it->id = ++next_transfer_id_;
534 auto slot = net::get_associated_cancellation_slot(it->handler);
535 if (slot.is_connected()) {
536 // Look the transfer up by id at emit time: the slot outlives the
537 // transfer (asio only guarantees clearing on handler destruction),
538 // so a late emit must find nothing rather than follow a dangling
539 // pointer. alive_ covers emits after ~Client.
540 slot.assign(
541 [this, alive = alive_, id = it->id](net::cancellation_type_t) {
542 if (*alive) cancel(id);
543 });
544 }
545
546 // curl schedules the kickstart itself through the timer callback.
547 curl_multi_add_handle(multi_, eh);
548 }
549
550 // Cooperative cancellation, reached through the completion handler's
551 // associated cancellation slot. Any cancellation type aborts: the
552 // transfer is torn down and the handler completes with
553 // operation_aborted. No-op when the transfer already completed.
554 void cancel(std::uint64_t id) {
555 for (auto& t : transfers_) {
556 if (t.id != id) continue;
557 // Also discards any DONE message this handle queued in the multi.
558 curl_multi_remove_handle(multi_, t.eh);
559 if (running_ > 0) --running_;
560 Handler handler = std::move(t.handler);
561 curl_slist_free_all(t.headers);
562 // Severed mid-flight: scrub before the handle is reused.
563 curl_easy_reset(t.eh);
564 configure_handle(t.eh);
565 if (pool_.size() < max_pooled_handles_) {
566 pool_.emplace_back(t.eh);
567 } else {
568 curl_easy_cleanup(t.eh);
569 }
570 transfers_.erase(t.self);
571 // Unlike normal completions this one is posted, not invoked: we are
572 // inside the cancellation emit, and completing here can destroy the
573 // very signal being emitted (asio::cancel_after owns its signal in
574 // the operation state the completion frees).
575 net::post(io_, net::bind_allocator(
576 net::recycling_allocator<void>(),
577 [h = std::move(handler)]() mutable {
578 std::move(h)(
579 error_code(net::error::operation_aborted),
580 Response{CURLE_ABORTED_BY_CALLBACK, 0, {}, {}});
581 }));
582 return;
583 }
584 }
585
586 // Request-independent options, set once per easy handle. Everything a
587 // transfer can vary must be (re)set in start() — pooled handles are
588 // reused without curl_easy_reset.
589 void configure_handle(CURL* eh) {
590 curl_easy_setopt(eh, CURLOPT_WRITEFUNCTION, body_write_cb);
591 curl_easy_setopt(eh, CURLOPT_HEADERFUNCTION, write_cb);
592 curl_easy_setopt(eh, CURLOPT_NOSIGNAL, 1L);
593#if LIBCURL_VERSION_NUM >= 0x075000 // 7.80.0
594 // Cap connection reuse age; on older libcurl the option is absent and
595 // pooled connections simply live longer.
596 curl_easy_setopt(eh, CURLOPT_MAXLIFETIME_CONN, 30L);
597#endif
598 // "" advertises every decoder curl was built with (gzip, br, ...).
599 curl_easy_setopt(eh, CURLOPT_ACCEPT_ENCODING, "");
600 // Wait for an in-progress connection to the same host and multiplex over
601 // it (HTTP/2) instead of racing to open one connection per request.
602 curl_easy_setopt(eh, CURLOPT_PIPEWAIT, 1L);
603 curl_easy_setopt(eh, CURLOPT_BUFFERSIZE, 512L * 1024L);
604 curl_easy_setopt(eh, CURLOPT_OPENSOCKETFUNCTION, open_socket_cb);
605 curl_easy_setopt(eh, CURLOPT_OPENSOCKETDATA, this);
606 curl_easy_setopt(eh, CURLOPT_CLOSESOCKETFUNCTION, close_socket_cb);
607 curl_easy_setopt(eh, CURLOPT_CLOSESOCKETDATA, this);
608 }
609
610 void set_body(const Transfer& t) {
611 curl_easy_setopt(t.eh, CURLOPT_POSTFIELDSIZE,
612 static_cast<long>(t.body.size()));
613 curl_easy_setopt(t.eh, CURLOPT_POSTFIELDS, t.body.c_str());
614 }
615
616 static size_t write_cb(char* data, size_t n, size_t l, std::string* buf) {
617 buf->append(data, n * l);
618 return n * l;
619 }
620
621 static size_t body_write_cb(char* data, size_t n, size_t l, Transfer* t) {
622 if (t->buffer.empty()) {
623 curl_off_t len = 0;
624 if (curl_easy_getinfo(t->eh, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &len) ==
625 CURLE_OK &&
626 len > 0) {
627 t->buffer.reserve(static_cast<size_t>(len));
628 }
629 }
630 t->buffer.append(data, n * l);
631 return n * l;
632 }
633
634 // curl asks us (not the OS directly) for sockets, so every fd it uses is
635 // backed by an ASIO object we can async_wait on. Cross-platform, no epoll.
636 static curl_socket_t open_socket_cb(void* clientp, curlsocktype purpose,
637 curl_sockaddr* address) {
638 auto* const self = static_cast<Client*>(clientp);
639 if (purpose != CURLSOCKTYPE_IPCXN) return CURL_SOCKET_BAD;
640 net::ip::tcp protocol = net::ip::tcp::v4();
641 if (address->family == AF_INET6) {
642 protocol = net::ip::tcp::v6();
643 } else if (address->family != AF_INET) {
644 return CURL_SOCKET_BAD;
645 }
646 auto state = std::make_shared<SocketState>(self->io_);
647 error_code ec;
648 state->socket.open(protocol, ec);
649 if (ec) return CURL_SOCKET_BAD;
650 const curl_socket_t fd = state->socket.native_handle();
651 self->sockets_[fd] = std::move(state);
652 return fd;
653 }
654
655 static int close_socket_cb(void* clientp, curl_socket_t fd) {
656 auto* const self = static_cast<Client*>(clientp);
657 const auto it = self->sockets_.find(fd);
658 if (it == self->sockets_.end()) return 1;
659 it->second->watch = 0;
660 error_code ignored;
661 it->second->socket.close(ignored);
662 self->sockets_.erase(it);
663 return 0;
664 }
665
666 static int socket_cb(CURL*, curl_socket_t fd, int what, void* userp,
667 void* socketp) {
668 auto* const self = static_cast<Client*>(userp);
669 auto* state = static_cast<SocketState*>(socketp);
670 if (state == nullptr) {
671 // First notification for this socket: attach the state so curl hands
672 // it back on later calls and we skip the lookup.
673 const auto it = self->sockets_.find(fd);
674 if (it == self->sockets_.end()) return 0;
675 state = it->second.get();
676 curl_multi_assign(self->multi_, fd, state);
677 }
678 state->watch = (what == CURL_POLL_REMOVE) ? 0 : what;
679 if (state->watch != 0) self->arm(state->shared_from_this());
680 return 0;
681 }
682
683 void arm(const std::shared_ptr<SocketState>& state) {
684 const curl_socket_t fd = state->socket.native_handle();
685 if ((state->watch & CURL_POLL_IN) && !state->read_armed) {
686 state->read_armed = true;
687 state->socket.async_wait(
688 net::ip::tcp::socket::wait_read,
689 net::bind_allocator(
690 net::recycling_allocator<void>(),
691 [this, w = std::weak_ptr<SocketState>(state), fd](error_code ec) {
692 on_event(w, fd, CURL_CSELECT_IN, ec);
693 }));
694 }
695 if ((state->watch & CURL_POLL_OUT) && !state->write_armed) {
696 state->write_armed = true;
697 state->socket.async_wait(
698 net::ip::tcp::socket::wait_write,
699 net::bind_allocator(
700 net::recycling_allocator<void>(),
701 [this, w = std::weak_ptr<SocketState>(state), fd](error_code ec) {
702 on_event(w, fd, CURL_CSELECT_OUT, ec);
703 }));
704 }
705 }
706
707 void on_event(const std::weak_ptr<SocketState>& weak, curl_socket_t fd,
708 int flag, error_code ec) {
709 // The shared_ptr keeps the state alive across the socket_action call
710 // below, which may close this very socket via close_socket_cb.
711 const auto state = weak.lock();
712 if (!state) return;
713 (flag == CURL_CSELECT_IN ? state->read_armed : state->write_armed) = false;
714 if (ec == net::error::operation_aborted) return;
715 curl_multi_socket_action(multi_, fd, ec ? CURL_CSELECT_ERR : flag,
716 &running_);
717 check_completions();
718 if (state->socket.is_open() && state->watch != 0) arm(state);
719 }
720
721 static int timer_cb(CURLM*, long timeout_ms, void* userp) {
722 auto* const self = static_cast<Client*>(userp);
723 if (timeout_ms < 0) {
724 self->timer_.cancel();
725 self->timer_armed_ = false;
726 return 0;
727 }
728 if (timeout_ms == 0) {
729 // "Act as soon as possible" — the common per-transfer kick. A plain
730 // post (deduplicated) is much cheaper than rescheduling the timer,
731 // and we may not call curl back from inside its own callback.
732 if (!self->kick_pending_) {
733 self->kick_pending_ = true;
734 net::post(self->io_,
735 net::bind_allocator(net::recycling_allocator<void>(),
736 [self, alive = self->alive_] {
737 if (!*alive) return;
738 self->kick_pending_ = false;
739 self->kick();
740 }));
741 }
742 return 0;
743 }
744 const auto deadline = std::chrono::steady_clock::now() +
745 std::chrono::milliseconds(timeout_ms);
746 // A pending wait that fires no later than the new deadline is good
747 // enough: a kick() finding nothing due is a cheap no-op, while
748 // rescheduling reprograms the timer every time.
749 if (self->timer_armed_ && self->timer_.expiry() <= deadline) return 0;
750 self->timer_.expires_at(deadline);
751 self->timer_armed_ = true;
752 self->timer_.async_wait(
753 net::bind_allocator(net::recycling_allocator<void>(),
754 [self, alive = self->alive_](error_code ec) {
755 if (ec || !*alive) return;
756 self->timer_armed_ = false;
757 self->kick();
758 }));
759 return 0;
760 }
761
762 void kick() {
763 curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, &running_);
764 check_completions();
765 }
766
767 void check_completions() {
768 int msgs_left = 0;
769 while (CURLMsg* msg = curl_multi_info_read(multi_, &msgs_left)) {
770 if (msg->msg != CURLMSG_DONE) continue;
771 Transfer* t = nullptr;
772 long http_code = 0;
773 // msg must not be dereferenced after curl_multi_remove_handle().
774 const CURLcode curl_code = msg->data.result;
775 CURL* const eh = msg->easy_handle;
776 curl_easy_getinfo(eh, CURLINFO_PRIVATE, &t);
777 curl_easy_getinfo(eh, CURLINFO_RESPONSE_CODE, &http_code);
778 curl_multi_remove_handle(multi_, eh);
779
780 Response res{curl_code, http_code, std::move(t->buffer),
781 std::move(t->header_buffer)};
782 Handler handler = std::move(t->handler);
783 curl_slist_free_all(t->headers);
784 if (t->scrub_on_done) {
785 curl_easy_reset(eh);
786 configure_handle(eh);
787 }
788 if (pool_.size() < max_pooled_handles_) {
789 pool_.emplace_back(t->eh);
790 } else {
791 curl_easy_cleanup(t->eh);
792 }
793 transfers_.erase(t->self);
794
795 const error_code ec =
796 curl_code == CURLE_OK ? error_code{} : make_error_code(curl_code);
797 // Single-threaded by contract: the handler's executor is this
798 // io_context, where we already are — invoke without the
799 // type-erased dispatch hop.
800 std::move(handler)(ec, std::move(res));
801 }
802 }
803
804 net::io_context& io_;
805 CURLM* multi_;
806 net::steady_timer timer_;
807 std::unordered_map<curl_socket_t, std::shared_ptr<SocketState>> sockets_;
808 std::list<Transfer> transfers_;
809 std::vector<CURL*> pool_;
810 const size_t max_pooled_handles_; // cap on pool_; set by the constructor
811 int running_ = 0;
812 std::uint64_t next_transfer_id_ = 0;
813 bool kick_pending_ = false;
814 bool timer_armed_ = false;
815 // Outlives the client inside posted/timed kicks: they bail out when the
816 // client is gone instead of touching a destroyed multi handle.
817 std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
818};
819
820} // namespace cofetch
Fluent builder bound to this client.
Definition cofetch.h:345
RequestBuilder & headers(std::vector< std::string > h)
Definition cofetch.h:350
RequestBuilder(Client &client, std::string url)
Definition cofetch.h:347
auto patch(CompletionToken &&token)
Definition cofetch.h:387
RequestBuilder & curl(std::function< void(CURL *)> fn)
Definition cofetch.h:366
auto post(CompletionToken &&token)
Definition cofetch.h:377
auto del(CompletionToken &&token)
Definition cofetch.h:392
RequestBuilder & body(std::string b)
Definition cofetch.h:354
RequestBuilder & follow_redirects(long max=30)
Definition cofetch.h:362
auto get(CompletionToken &&token)
Definition cofetch.h:372
RequestBuilder & timeout(std::chrono::milliseconds t)
Definition cofetch.h:358
auto put(CompletionToken &&token)
Definition cofetch.h:382
Definition cofetch.h:266
RequestBuilder request(std::string url)
Start a fluent request chain: request(url).body(...).post(token).
Definition cofetch.h:412
Client & operator=(const Client &)=delete
~Client()
Destroy the client.
Definition cofetch.h:292
auto async_perform(Request req, CompletionToken &&token)
Start a transfer described by req.
Definition cofetch.h:318
Client(net::io_context &io, size_t max_pooled_connections=kDefaultMaxPooledHandles)
Construct a client driven by io.
Definition cofetch.h:273
int pending_requests() const
Definition cofetch.h:416
auto async_post(std::string url, std::string body, CompletionToken &&token)
Definition cofetch.h:330
auto async_get(std::string url, CompletionToken &&token)
Definition cofetch.h:324
Client(const Client &)=delete
Value-type description of a request; pass to Client::async_perform.
Definition cofetch.h:211
Request(std::string url)
Definition cofetch.h:215
Request & headers(std::vector< std::string > h)
Definition cofetch.h:221
long max_redirects_
Definition cofetch.h:262
std::string body_
Definition cofetch.h:260
Method
Definition cofetch.h:213
Request & body(std::string b)
Definition cofetch.h:225
Request & method(Method m)
Definition cofetch.h:217
Request & follow_redirects(long max=30)
Follow HTTP 3xx redirects, at most max hops (the transfer fails with CURLE_TOO_MANY_REDIRECTS beyond ...
Definition cofetch.h:242
std::chrono::milliseconds timeout_
Definition cofetch.h:261
Method method_
Definition cofetch.h:258
std::function< void(CURL *)> curl_setup_
Definition cofetch.h:263
std::vector< std::string > headers_
Definition cofetch.h:259
Request & curl(std::function< void(CURL *)> fn)
Escape hatch: fn runs on the underlying easy handle after cofetch's own options, so it can set (or ov...
Definition cofetch.h:252
std::string url_
Definition cofetch.h:257
Request & timeout(std::chrono::milliseconds t)
Whole-transfer timeout (default 5s), millisecond resolution.
Definition cofetch.h:234
Definition cofetch.h:111
Response(CURLcode curl_code, long http_code, std::string data, std::string header_data)
Definition cofetch.h:118
long http_code_
Definition cofetch.h:176
const char * error() const
Human readable description of the transport error ("No error" when the transfer itself succeeded).
Definition cofetch.h:136
CURLcode curl_code_
Definition cofetch.h:175
Response()=default
Headers headers() const
Parse header_data_ into a case-insensitive name->value map.
Definition cofetch.h:145
bool is_ok() const
True when the transfer succeeded and the HTTP status is 2xx.
Definition cofetch.h:128
std::string header_data_
Definition cofetch.h:178
std::optional< std::string > header(std::string_view name) const
Case-insensitive lookup of one field without building the full map; repeats are comma-combined.
Definition cofetch.h:161
std::unordered_map< std::string, std::string, detail::CiHash, detail::CiEqual > Headers
Definition cofetch.h:115
std::string data_
Definition cofetch.h:177
char ascii_lower(char c)
Definition cofetch.h:89
Definition cofetch.h:50
error_code make_error_code(CURLcode code)
Definition cofetch.h:79
const error_category & curl_category()
std::error_category for libcurl transport errors (CURLcode values).
Definition cofetch.h:67
std::error_category error_category
Definition cofetch.h:61
std::error_code error_code
Definition cofetch.h:60
Definition cofetch.h:99
bool operator()(std::string_view a, std::string_view b) const noexcept
Definition cofetch.h:100
Definition cofetch.h:92
size_t operator()(std::string_view s) const noexcept
Definition cofetch.h:93