FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
http_client.hpp
Go to the documentation of this file.
1#pragma once
2// Keep-alive HTTP/1.1 client over any ByteStream: one request in flight per connection plus
3// a small FIFO, response bodies framed by Content-Length / chunked / connection-close, and a
4// per-request reactor timer. Callbacks run on the reactor thread; header and body views are
5// valid only for the duration of the callback (they point into the receive buffer).
6//
7// REST is the venue *control* path (exchangeInfo, listenKey, cancel-all fallback); order
8// flow goes over the WebSocket API. Requests are therefore serialised into a std::string at
9// enqueue time - simple and safe, not zero-allocation - or, for a target that must be made when
10// the request goes on the wire (a signed query's timestamp), when it reaches the queue's head.
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"
16
17#include <cstddef>
18#include <cstdint>
19#include <deque>
20#include <functional>
21#include <string>
22#include <string_view>
23#include <utility>
24
25namespace fastmm::net {
26
27struct HttpResponse {
28 int status = 0; // 0 when `error` is set
29 NetError error = NetError::None; // Timeout, Closed, Protocol, Canceled, ...
30 std::string_view error_detail;
31 HttpHeaders headers; // views into the receive buffer
32 std::string_view body; // decoded (de-chunked) body
33 std::int64_t rx_ts = 0; // reactor clock when the response completed
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; }
37};
39using HttpResponseCallback = std::function<void(const HttpResponse&)>;
40// Makes a request's target (path and query) when the request is written. Empty: the request is
41// not sent and fails with NetError::Canceled.
42using HttpTargetMaker = std::function<std::string()>;
43
45 std::size_t recv_capacity = std::size_t{1024} * 1024;
46 std::size_t send_capacity = std::size_t{256} * 1024;
47 std::uint32_t timeout_ms = 5000; // per request, from the moment it is written; the connect too
48 std::size_t max_queue = 8; // in-flight + waiting
49};
51struct HttpClientStats {
52 std::uint64_t requests = 0;
53 std::uint64_t responses = 0;
54 std::uint64_t timeouts = 0;
55 std::uint64_t errors = 0;
56};
58enum class HttpClientState : std::uint8_t { Idle, Connecting, Ready, Closed };
59
60namespace detail {
61
62// Incremental HTTP/1.1 response parser working in place inside a RecvBuffer. Chunked bodies
63// are de-chunked by moving chunk data left over the chunk-size lines, so the body ends up
64// contiguous right behind the head. Trailers are skipped.
65class HttpResponseParser {
66 public:
67 enum class Result : std::uint8_t { NeedMore, Complete, Invalid };
68
69 void begin(bool head_request) noexcept {
70 reset();
71 head_request_ = head_request;
72 }
73 void reset() noexcept;
74
75 // Advances over rx.readable(). On Complete, `out` is filled and `consume_len` is the
76 // number of bytes to consume once the callback returns.
77 Result parse(RecvBuffer& rx, HttpResponse& out, std::size_t& consume_len) noexcept;
78 // The peer closed: completes a close-delimited body, otherwise reports truncation.
79 Result on_eof(RecvBuffer& rx, HttpResponse& out, std::size_t& consume_len) noexcept;
80
81 bool connection_close() const noexcept { return connection_close_; }
82 const char* error() const noexcept { return error_; }
83
84 private:
85 enum class Stage : std::uint8_t {
86 Head,
87 FixedBody,
88 ChunkSize,
89 ChunkData,
90 ChunkEnd,
91 Trailers,
92 UntilClose
93 };
94
95 Result parse_head(RecvBuffer& rx) noexcept;
96 Result finish(RecvBuffer& rx, HttpResponse& out, std::size_t& consume_len) noexcept;
97 Result fail(const char* why) noexcept {
98 error_ = why;
99 return Result::Invalid;
100 }
101
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; // decoded body bytes at [head_len_, head_len_ + body_len_)
107 std::size_t scan_off_ = 0; // next unparsed byte (relative to readable())
108 std::size_t chunk_remaining_ = 0;
109 std::size_t expected_len_ = 0; // Content-Length
110 const char* error_ = "";
111};
112
113} // namespace detail
114
115template <ByteStream Stream>
116class HttpClient final : public IoHandler {
117 public:
119 Stream&& stream,
120 std::string host,
121 std::uint16_t port,
122 bool tls,
123 const HttpClientConfig& cfg = {})
124 : reactor_(reactor),
125 stream_(std::move(stream)),
126 host_(std::move(host)),
127 port_(port),
128 tls_(tls),
129 cfg_(cfg),
130 rx_(cfg.recv_capacity),
131 tx_(cfg.send_capacity) {}
132 ~HttpClient() override { close(); }
133 HttpClient(const HttpClient&) = delete;
134 HttpClient& operator=(const HttpClient&) = delete;
136 bool start() {
137 if (state_ != HttpClientState::Idle) return false;
138 if (!reactor_.add(stream_.fd(), *this, IoEvent::ReadWrite)) return false;
139 registered_ = true;
141 drive_connect();
142 // The connect and the TLS handshake are bounded like a request: a peer that accepts the TCP
143 // connection and never answers the ClientHello would otherwise hold every request queued.
144 if (state_ == HttpClientState::Connecting) {
145 timer_ =
146 reactor_.add_timer_after(static_cast<std::int64_t>(cfg_.timeout_ms) * 1'000'000, [this] {
147 timer_ = kInvalidTimer;
148 ++stats_.timeouts;
149 fail_all(NetError::Timeout, "connect timed out");
150 });
151 }
152 return true;
153 }
154
155 // Queues a request. `extra_headers` is a pre-formatted "Name: value\r\n" block. Returns
156 // false if the client is closed or the queue is full (callback not invoked).
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;
163 Pending p;
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));
168 }
169
170 // The same, with the target made by `make_target` when the request is written rather than now:
171 // a request waiting behind others (one in flight per connection) carries what is true when it
172 // leaves, such as a signed query's timestamp, which the venue checks against a window.
173 bool request(std::string_view method,
174 HttpTargetMaker make_target,
175 std::string_view extra_headers,
176 std::string_view body,
178 if (!has_room() || !make_target) return false;
179 Pending p;
180 p.head_request = method == "HEAD";
181 p.cb = std::move(cb);
182 p.make_target = std::move(make_target);
183 p.method = method;
184 p.headers = extra_headers;
185 p.body = body;
186 return enqueue(std::move(p));
187 }
188
189 // Tears the connection down; every queued/in-flight request gets NetError::Canceled.
190 void close() noexcept { fail_all(NetError::Canceled, "closed"); }
191
192 HttpClientState state() const noexcept { return state_; }
193 bool is_ready() const noexcept { return state_ == HttpClientState::Ready; }
194 bool in_flight() const noexcept { return in_flight_; }
195 std::size_t queued() const noexcept { return queue_.size(); }
196 // Requests queued already stay queued when this is lowered below their number.
197 void set_max_queue(std::size_t n) noexcept { cfg_.max_queue = n; }
198 const HttpClientStats& stats() const noexcept { return stats_; }
199 Stream& stream() noexcept { return stream_; }
201 // --- IoHandler -------------------------------------------------------------------------
202 void on_readable() override {
204 drive_connect();
205 } else if (state_ == HttpClientState::Ready) {
206 read_responses();
207 }
208 }
209 void on_writable() override {
211 drive_connect();
212 } else if (state_ == HttpClientState::Ready) {
213 flush();
214 }
215 }
216 void on_error(int) override { fail_all(NetError::Syscall, "socket error"); }
218 private:
219 struct Pending {
220 std::string bytes;
222 // Set: `bytes` is made from these when the request is written.
223 HttpTargetMaker make_target;
224 std::string method;
225 std::string headers;
226 std::string body;
227 bool head_request = false;
228 };
229
230 bool has_room() const noexcept {
231 return state_ != HttpClientState::Closed && queue_.size() < cfg_.max_queue;
232 }
233
234 bool enqueue(Pending&& p) {
235 queue_.push_back(std::move(p));
236 ++stats_.requests;
237 if (state_ == HttpClientState::Ready) send_next();
238 return true;
239 }
240
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 {
246 out.clear();
247 out.reserve(method.size() + target.size() + host_.size() + extra_headers.size() + body.size() +
248 64);
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");
254 }
255 out.append("\r\n").append(body);
256 }
257
258 void drive_connect() {
259 const IoResult r = stream_.handshake();
260 if (r.failed()) {
261 fail_all(r.error, r.error == NetError::Tls ? "tls handshake failed" : "connect failed");
262 return;
263 }
264 if (r.would_block()) return;
265 cancel_timer();
266 state_ = HttpClientState::Ready;
267 send_next();
268 }
269
270 void send_next() {
271 if (in_flight_ || queue_.empty() || state_ != HttpClientState::Ready) return;
272 Pending& p = queue_.front();
273 if (p.make_target) {
274 const std::string target = p.make_target();
275 p.make_target = nullptr;
276 if (target.empty()) {
277 Pending failed = std::move(queue_.front());
278 queue_.pop_front();
279 deliver_error(failed, NetError::Canceled, "request target withdrawn");
280 send_next();
281 return;
282 }
283 format(p.bytes, p.method, target, p.headers, p.body);
284 p.method.clear();
285 p.headers.clear();
286 p.body.clear();
287 }
288 if (!tx_.append(p.bytes)) {
289 // Larger than the whole send buffer: fail just this request.
290 Pending failed = std::move(queue_.front());
291 queue_.pop_front();
292 deliver_error(failed, NetError::Overflow, "request exceeds send buffer");
293 send_next();
294 return;
295 }
296 in_flight_ = true;
297 parser_.begin(p.head_request);
298 timer_ = reactor_.add_timer_after(static_cast<std::int64_t>(cfg_.timeout_ms) * 1'000'000,
299 [this] { on_timeout(); });
300 flush();
301 }
302
303 void on_timeout() {
304 timer_ = kInvalidTimer;
305 ++stats_.timeouts;
306 // The response boundary is now unknown; the connection cannot be reused.
307 fail_all(NetError::Timeout, "request timed out", /*first_only_error=*/true);
308 }
309
310 void flush() noexcept {
311 if constexpr (requires(Stream& s) { s.flush(); }) {
312 const IoResult fl = stream_.flush();
313 if (fl.failed()) {
314 fail_all(fl.error, "flush failed");
315 return;
316 }
317 if (fl.want_write) return;
318 }
319 while (!tx_.empty()) {
320 const IoResult w = stream_.write(tx_.pending());
321 tx_.consumed(w.bytes);
322 if (w.failed()) {
323 fail_all(w.error, "write failed");
324 return;
325 }
326 if (w.would_block()) return;
327 }
328 }
329
330 void read_responses() {
331 for (;;) {
332 const std::span<std::byte> w = rx_.writable();
333 if (w.empty()) {
334 fail_all(NetError::Overflow, "response exceeds receive buffer");
335 return;
336 }
337 const IoResult r = stream_.read(w);
338 if (r.bytes > 0) {
339 rx_.commit(r.bytes);
340 if (!process()) return;
341 }
342 if (r.closed) {
343 on_eof();
344 return;
345 }
346 if (r.failed()) {
347 fail_all(r.error, "read failed");
348 return;
349 }
350 if (r.would_block()) return;
351 }
352 }
353
354 // Parses the in-flight response; returns false if the connection was torn down.
355 bool process() {
356 if (!in_flight_) {
357 // Bytes without a request in flight: the server is misbehaving.
358 if (!rx_.empty()) fail_all(NetError::Protocol, "unsolicited response bytes");
359 return false;
360 }
361 HttpResponse resp;
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());
367 return false;
368 }
369 complete(resp, consume);
370 return state_ == HttpClientState::Ready;
371 }
372
373 void on_eof() {
374 if (in_flight_) {
375 HttpResponse resp;
376 std::size_t consume = 0;
377 if (parser_.on_eof(rx_, resp, consume) == detail::HttpResponseParser::Result::Complete) {
378 complete(resp, consume);
379 }
380 }
381 fail_all(NetError::Closed, "connection closed");
382 }
383
384 void complete(HttpResponse& resp, std::size_t consume) {
385 cancel_timer();
386 in_flight_ = false;
387 ++stats_.responses;
388 resp.rx_ts = Reactor::now_ns();
389 Pending done = std::move(queue_.front());
390 queue_.pop_front();
391 const bool close_after = parser_.connection_close();
392 done.cb(resp);
393 rx_.consume(consume);
394 if (close_after) {
395 fail_all(NetError::Closed, "server closed the connection");
396 return;
397 }
398 if (state_ == HttpClientState::Ready) send_next();
399 }
400
401 void cancel_timer() noexcept {
402 if (timer_ != kInvalidTimer) {
403 reactor_.cancel_timer(timer_);
404 timer_ = kInvalidTimer;
405 }
406 }
407
408 void deliver_error(Pending& p, NetError err, const char* detail) {
409 HttpResponse resp;
410 resp.error = err;
411 resp.error_detail = detail;
412 ++stats_.errors;
413 if (p.cb) p.cb(resp);
414 }
415
416 // Closes the stream and fails every pending request. With first_only_error, the
417 // in-flight request receives `err` and the rest are Canceled.
418 void fail_all(NetError err, const char* detail, bool first_only_error = false) noexcept {
419 if (state_ == HttpClientState::Closed) return;
421 cancel_timer();
422 if (registered_) {
423 reactor_.remove(stream_.fd());
424 registered_ = false;
425 }
426 stream_.close();
427 in_flight_ = false;
428 std::deque<Pending> pending;
429 pending.swap(queue_);
430 bool first = true;
431 for (Pending& p : pending) {
432 deliver_error(p, (first || !first_only_error) ? err : NetError::Canceled, detail);
433 first = false;
434 }
435 }
436
437 Reactor& reactor_;
438 Stream stream_;
439 std::string host_;
440 std::uint16_t port_;
441 bool tls_;
442 HttpClientConfig cfg_;
443 RecvBuffer rx_;
444 WireBuffer tx_;
445 detail::HttpResponseParser parser_;
446 std::deque<Pending> queue_;
447 HttpClientStats stats_;
449 TimerId timer_ = kInvalidTimer;
450 bool registered_ = false;
451 bool in_flight_ = false;
452};
453
454} // namespace fastmm::net
Definition result.hpp:30
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