Line data Source code
1 : #ifndef THINGER_HTTP_CLIENT_STANDALONE_HPP
2 : #define THINGER_HTTP_CLIENT_STANDALONE_HPP
3 :
4 : #include "http_client_base.hpp"
5 : #include "request_builder.hpp"
6 : #include "../../asio/io_context_of.hpp"
7 : #include <boost/asio/io_context.hpp>
8 : #include <boost/asio/use_future.hpp>
9 : #include <chrono>
10 : #include <future>
11 : #include <stdexcept>
12 :
13 : namespace thinger::http {
14 :
15 : /**
16 : * Standalone HTTP client with simple synchronous API.
17 : * Perfect for scripts, CLI tools, and simple applications.
18 : *
19 : * Usage:
20 : * http::client client;
21 : *
22 : * // Simple GET
23 : * auto response = client.get("https://api.example.com/users");
24 : * if (response) {
25 : * std::cout << response.body() << std::endl;
26 : * }
27 : *
28 : * // POST with JSON
29 : * auto response = client.post("https://api.example.com/users", R"({"name":"test"})");
30 : *
31 : * // With configuration (fluent API)
32 : * client.timeout(std::chrono::seconds(30)).verify_ssl(false);
33 : * auto response = client.get("https://internal-api/data");
34 : *
35 : * For async/concurrent operations, use http::async_client instead.
36 : */
37 : class client : public http_client_base {
38 : private:
39 : boost::asio::io_context io_context_;
40 :
41 : // Internal helper to run an awaitable synchronously: until it completes (an open
42 : // WebSocket keeps the io_context busy with its keepalive). Refused within a handler of the
43 : // io_context (e.g. a stream callback), which would run it re-entrantly
44 : template<typename T>
45 2671 : T exec(awaitable<T> coro) {
46 2671 : if (io_context_.get_executor().running_in_this_thread()) {
47 3 : throw std::logic_error("http::client: synchronous call made within one of its own callbacks, "
48 : "where it would run its io_context re-entrantly");
49 : }
50 2668 : auto result = co_spawn(io_context_, std::move(coro), boost::asio::use_future);
51 11276 : asio::run_until(io_context_, [&] {
52 41043 : return result.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
53 : });
54 5336 : return result.get();
55 2668 : }
56 :
57 : protected:
58 2165 : boost::asio::io_context& get_io_context() override {
59 2165 : return io_context_;
60 : }
61 :
62 143 : bool owns_io_context() const override { return true; }
63 :
64 : public:
65 : // The io_context runs only during a call, until its operation completes (as the WebSocket
66 : // calls): idle connections are not closed by timers, but when found expired as a request
67 : // starts or ends. Between calls nothing runs: clear_connections() closes them
68 1899 : client() { pool_.use_idle_timers(false); }
69 : // The idle connections are closed while their io_context exists
70 1908 : ~client() override { clear_connections(); }
71 :
72 : // ============================================
73 : // Synchronous HTTP methods
74 : // ============================================
75 :
76 1791 : client_response get(const std::string& url, headers_map headers = {}) {
77 1797 : return exec(http_client_base::get(url, std::move(headers)));
78 : }
79 :
80 317 : client_response post(const std::string& url, std::string body = {},
81 : std::string content_type = "application/json", headers_map headers = {}) {
82 317 : return exec(http_client_base::post(url, std::move(body), std::move(content_type), std::move(headers)));
83 : }
84 :
85 3 : client_response post(const std::string& url, const form& form, headers_map headers = {}) {
86 3 : return exec(http_client_base::post(url, form, std::move(headers)));
87 : }
88 :
89 27 : client_response put(const std::string& url, std::string body = {},
90 : std::string content_type = "application/json", headers_map headers = {}) {
91 27 : return exec(http_client_base::put(url, std::move(body), std::move(content_type), std::move(headers)));
92 : }
93 :
94 18 : client_response patch(const std::string& url, std::string body = {},
95 : std::string content_type = "application/json", headers_map headers = {}) {
96 18 : return exec(http_client_base::patch(url, std::move(body), std::move(content_type), std::move(headers)));
97 : }
98 :
99 17 : client_response del(const std::string& url, headers_map headers = {}) {
100 17 : return exec(http_client_base::del(url, std::move(headers)));
101 : }
102 :
103 19 : client_response head(const std::string& url, headers_map headers = {}) {
104 19 : return exec(http_client_base::head(url, std::move(headers)));
105 : }
106 :
107 8 : client_response options(const std::string& url, headers_map headers = {}) {
108 8 : return exec(http_client_base::options(url, std::move(headers)));
109 : }
110 :
111 : // Generic send with custom request
112 215 : client_response send(std::shared_ptr<http_request> request) {
113 215 : return exec(http_client_base::send(std::move(request)));
114 : }
115 :
116 : // Streaming send
117 89 : stream_result send_streaming(std::shared_ptr<http_request> request, stream_callback callback) {
118 89 : return exec(http_client_base::send_streaming(std::move(request), std::move(callback)));
119 : }
120 :
121 : // ============================================
122 : // Request builder for fluent API
123 : // ============================================
124 :
125 : /**
126 : * Create a request builder for fluent API with streaming support.
127 : *
128 : * Usage:
129 : * auto res = client.request("https://api.com/data")
130 : * .header("Authorization", "Bearer xxx")
131 : * .get();
132 : */
133 205 : request_builder<client> request(const std::string& url) {
134 205 : return request_builder<client>(this, url);
135 : }
136 :
137 : // ============================================
138 : // WebSocket
139 : // ============================================
140 :
141 : /**
142 : * Connect to a WebSocket server (simple API).
143 : *
144 : * Usage:
145 : * if (auto ws = client.websocket("ws://server.com/path")) {
146 : * ws->send_text("Hello!");
147 : * auto [msg, binary] = ws->receive();
148 : * ws->close();
149 : * }
150 : */
151 150 : std::optional<websocket_client> websocket(const std::string& url,
152 : const std::string& subprotocol = "") {
153 150 : return exec(http_client_base::upgrade_websocket(url, subprotocol));
154 : }
155 :
156 : /**
157 : * Connect to a WebSocket server with custom request/headers (used by request_builder).
158 : */
159 17 : std::optional<websocket_client> websocket(std::shared_ptr<http_request> request,
160 : const std::string& subprotocol = "") {
161 17 : return exec(http_client_base::upgrade_websocket(std::move(request), subprotocol));
162 : }
163 : };
164 :
165 : } // namespace thinger::http
166 :
167 : #endif
|