11#include "fastmm/net/byte_stream.hpp"
12#include "fastmm/net/http_message.hpp"
14#include "fastmm/net/recv_buffer.hpp"
15#include "fastmm/net/wire_buffer.hpp"
29 NetError
error = NetError::None;
35 std::string_view
header(std::string_view name)
const noexcept {
return headers.get(name); }
36 bool ok() const noexcept {
return error == NetError::None &&
status >= 200 &&
status < 300; }
65class HttpResponseParser {
67 enum class Result : std::uint8_t { NeedMore, Complete,
Invalid };
69 void begin(
bool head_request)
noexcept {
71 head_request_ = head_request;
73 void reset()
noexcept;
81 bool connection_close()
const noexcept {
return connection_close_; }
82 const char* error()
const noexcept {
return error_; }
85 enum class Stage : std::uint8_t {
95 Result parse_head(RecvBuffer& rx)
noexcept;
99 return Result::Invalid;
102 Stage stage_ = Stage::Head;
103 bool head_request_ =
false;
104 bool connection_close_ =
false;
105 std::size_t head_len_ = 0;
106 std::size_t body_len_ = 0;
107 std::size_t scan_off_ = 0;
108 std::size_t chunk_remaining_ = 0;
109 std::size_t expected_len_ = 0;
110 const char* error_ =
"";
115template <ByteStream Stream>
116class HttpClient final :
public IoHandler {
125 stream_(std::move(
stream)),
126 host_(std::move(host)),
130 rx_(cfg.recv_capacity),
131 tx_(cfg.send_capacity) {}
133 HttpClient(
const HttpClient&) =
delete;
147 timer_ = kInvalidTimer;
149 fail_all(NetError::Timeout,
"connect timed out");
157 bool request(std::string_view method,
158 std::string_view target,
159 std::string_view extra_headers,
160 std::string_view body,
162 if (!has_room())
return false;
164 p.head_request = method ==
"HEAD";
165 p.cb = std::move(cb);
166 format(p.bytes, method, target, extra_headers, body);
167 return enqueue(std::move(p));
173 bool request(std::string_view method,
175 std::string_view extra_headers,
176 std::string_view body,
178 if (!has_room() || !make_target)
return false;
180 p.head_request = method ==
"HEAD";
181 p.cb = std::move(cb);
182 p.make_target = std::move(make_target);
184 p.headers = extra_headers;
186 return enqueue(std::move(p));
190 void close() noexcept { fail_all(NetError::Canceled,
"closed"); }
195 std::size_t
queued() const noexcept {
return queue_.size(); }
199 Stream&
stream() noexcept {
return stream_; }
216 void on_error(
int)
override { fail_all(NetError::Syscall,
"socket error"); }
227 bool head_request =
false;
230 bool has_room() const noexcept {
234 bool enqueue(Pending&& p) {
235 queue_.push_back(std::move(p));
241 void format(std::string& out,
242 std::string_view method,
243 std::string_view target,
244 std::string_view extra_headers,
245 std::string_view body)
const {
247 out.reserve(method.size() + target.size() + host_.size() + extra_headers.size() + body.size() +
249 out.append(method).append(
" ").append(target).append(
" HTTP/1.1\r\nHost: ").append(host_);
250 if (port_ != 0 && port_ != (tls_ ? 443 : 80)) out.append(
":").append(std::
to_string(port_));
251 out.append(
"\r\n").append(extra_headers);
252 if (!body.empty() || method ==
"POST" || method ==
"PUT" || method ==
"PATCH") {
253 out.append(
"Content-Length: ").append(std::to_string(body.size())).append(
"\r\n");
255 out.append(
"\r\n").append(body);
258 void drive_connect() {
259 const IoResult r = stream_.handshake();
261 fail_all(r.error, r.error == NetError::Tls ?
"tls handshake failed" :
"connect failed");
264 if (r.would_block())
return;
274 const std::string target = p.make_target();
275 p.make_target =
nullptr;
276 if (target.empty()) {
277 Pending failed = std::move(queue_.front());
279 deliver_error(failed, NetError::Canceled,
"request target withdrawn");
283 format(p.bytes, p.method, target, p.headers, p.body);
288 if (!tx_.append(p.bytes)) {
290 Pending failed = std::move(queue_.front());
292 deliver_error(failed, NetError::Overflow,
"request exceeds send buffer");
297 parser_.begin(p.head_request);
299 [
this] { on_timeout(); });
307 fail_all(NetError::Timeout,
"request timed out",
true);
310 void flush() noexcept {
311 if constexpr (
requires(Stream& s) { s.flush(); }) {
312 const IoResult fl = stream_.flush();
314 fail_all(fl.error,
"flush failed");
317 if (fl.want_write)
return;
319 while (!tx_.empty()) {
320 const IoResult w = stream_.write(tx_.pending());
321 tx_.consumed(w.bytes);
323 fail_all(w.error,
"write failed");
326 if (w.would_block())
return;
330 void read_responses() {
332 const std::span<std::byte> w = rx_.writable();
334 fail_all(NetError::Overflow,
"response exceeds receive buffer");
337 const IoResult r = stream_.read(w);
340 if (!process())
return;
347 fail_all(r.error,
"read failed");
350 if (r.would_block())
return;
358 if (!rx_.empty()) fail_all(NetError::Protocol,
"unsolicited response bytes");
362 std::size_t consume = 0;
363 const auto res = parser_.parse(rx_, resp, consume);
364 if (res == detail::HttpResponseParser::Result::NeedMore)
return true;
365 if (res == detail::HttpResponseParser::Result::Invalid) {
366 fail_all(NetError::Protocol, parser_.error());
369 complete(resp, consume);
376 std::size_t consume = 0;
377 if (parser_.on_eof(rx_, resp, consume) == detail::HttpResponseParser::Result::Complete) {
378 complete(resp, consume);
381 fail_all(NetError::Closed,
"connection closed");
384 void complete(HttpResponse& resp, std::size_t consume) {
389 Pending done = std::move(queue_.front());
391 const bool close_after = parser_.connection_close();
393 rx_.consume(consume);
395 fail_all(NetError::Closed,
"server closed the connection");
401 void cancel_timer() noexcept {
408 void deliver_error(Pending& p, NetError err,
const char* detail) {
411 resp.error_detail = detail;
413 if (p.cb) p.cb(resp);
418 void fail_all(NetError err,
const char* detail,
bool first_only_error =
false) noexcept {
423 reactor_.
remove(stream_.fd());
428 std::deque<Pending> pending;
429 pending.swap(queue_);
431 for (Pending& p : pending) {
432 deliver_error(p, (first || !first_only_error) ? err : NetError::
Canceled, detail);
442 HttpClientConfig cfg_;
445 detail::HttpResponseParser parser_;
446 std::deque<Pending> queue_;
447 HttpClientStats stats_;
450 bool registered_ =
false;
451 bool in_flight_ =
false;
Definition http_client.hpp:118
bool in_flight() const noexcept
Definition http_client.hpp:196
bool start()
Definition http_client.hpp:138
Stream & stream() noexcept
Definition http_client.hpp:201
void on_error(int) override
POLLERR (or a poll request the kernel refused).
Definition http_client.hpp:217
std::size_t queued() const noexcept
Definition http_client.hpp:197
bool is_ready() const noexcept
Definition http_client.hpp:195
bool request(std::string_view method, std::string_view target, std::string_view extra_headers, std::string_view body, HttpResponseCallback cb)
Queues a request.
Definition http_client.hpp:159
void on_writable() override
Definition http_client.hpp:210
void set_max_queue(std::size_t n) noexcept
Requests queued already stay queued when this is lowered below their number.
Definition http_client.hpp:199
~HttpClient() override
Definition http_client.hpp:134
HttpClient & operator=(const HttpClient &)=delete
const HttpClientStats & stats() const noexcept
Definition http_client.hpp:200
void close() noexcept
Tears the connection down; every queued/in-flight request gets NetError::Canceled.
Definition http_client.hpp:192
HttpClientState state() const noexcept
Definition http_client.hpp:194
void on_readable() override
Definition http_client.hpp:203
Definition reactor.hpp:72
TimerId add_timer_after(std::int64_t delay_ns, TimerCallback cb)
Definition reactor.hpp:105
bool cancel_timer(TimerId id) noexcept
bool add(int fd, IoHandler &handler, IoEvent events) noexcept
fd registration (reactor thread only).
static std::int64_t now_ns() noexcept
CLOCK_MONOTONIC.
bool remove(int fd) noexcept
Definition reactor.hpp:37
std::uint64_t TimerId
Definition reactor.hpp:69
std::string_view to_string(ReactorBackend backend) noexcept
"epoll" / "io_uring", the spelling of [engine] net_backend.
HttpClientState
Definition http_client.hpp:60
std::function< void(const HttpResponse &)> HttpResponseCallback
Definition http_client.hpp:41
constexpr TimerId kInvalidTimer
Definition reactor.hpp:70
std::function< std::string()> HttpTargetMaker
Makes a request's target (path and query) when the request is written.
Definition http_client.hpp:44
@ Pending
orders on their way: sent, not yet working, or a requote waiting its turn
constexpr Unexpected< std::decay_t< E > > fail(E &&e) noexcept
Definition result.hpp:25
@ Invalid
empty name, null schema or factory, or an invalid kind
Single-threaded event loop with a timer min-heap and a thread-safe mailbox (post/wake via eventfd),...
Definition http_client.hpp:46
std::uint32_t timeout_ms
per request, from the moment it is written; the connect too
Definition http_client.hpp:49
std::size_t recv_capacity
Definition http_client.hpp:47
std::size_t max_queue
in-flight + waiting
Definition http_client.hpp:50
std::size_t send_capacity
Definition http_client.hpp:48
Definition http_client.hpp:53
std::uint64_t errors
Definition http_client.hpp:57
std::uint64_t timeouts
Definition http_client.hpp:56
std::uint64_t responses
Definition http_client.hpp:55
std::uint64_t requests
Definition http_client.hpp:54
Definition http_client.hpp:29
std::string_view body
decoded (de-chunked) body
Definition http_client.hpp:34
HttpHeaders headers
views into the receive buffer
Definition http_client.hpp:33
std::int64_t rx_ts
reactor clock when the response completed
Definition http_client.hpp:35
std::string_view error_detail
Definition http_client.hpp:32
std::string_view header(std::string_view name) const noexcept
Definition http_client.hpp:37
int status
0 when error is set
Definition http_client.hpp:30
NetError error
Timeout, Closed, Protocol, Canceled, ...
Definition http_client.hpp:31
bool ok() const noexcept
Definition http_client.hpp:38