OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
http_client_request.hpp
1#pragma once
2#include <ossia/detail/config.hpp>
3
4#include <ossia/detail/fmt.hpp>
6#include <ossia/detail/parse_relax.hpp>
7
8#include <boost/asio.hpp>
9
10#include <utility>
11#include <vector>
12
13namespace ossia::net
14{
15using tcp = boost::asio::ip::tcp;
16
17// Full HTTP client supporting all methods, custom headers, request body.
18// Success callback receives (request, status_code, response_body).
19// Error callback receives (request, error_message).
20template <typename Fun, typename Err>
21class http_client_request
22 : public std::enable_shared_from_this<http_client_request<Fun, Err>>
23{
24 fmt::memory_buffer m_request;
25
26public:
27 using std::enable_shared_from_this<http_client_request<Fun, Err>>::shared_from_this;
28
29 http_client_request(
30 Fun f, Err err, boost::asio::io_context& ctx, std::string_view verb,
31 std::string_view host, std::string_view path,
32 const std::vector<std::pair<std::string, std::string>>& headers = {},
33 std::string_view body = {})
34 : m_resolver(ctx)
35 , m_socket(ctx)
36 , m_fun{std::move(f)}
37 , m_err{std::move(err)}
38 {
39 m_request.reserve(256 + host.size() + path.size() + body.size());
40 m_response.prepare(Fun::reserve_expect);
41
42 // Request line: VERB /path HTTP/1.1
43 fmt::format_to(fmt::appender(m_request), "{} ", verb);
44 for(auto c : path)
45 {
46 if(c != ' ')
47 fmt::format_to(fmt::appender(m_request), "{}", c);
48 else
49 fmt::format_to(fmt::appender(m_request), "%20");
50 }
51 fmt::format_to(fmt::appender(m_request), " HTTP/1.1\r\n");
52
53 // Host header (always required)
54 fmt::format_to(fmt::appender(m_request), "Host: {}\r\n", host);
55
56 // Track which default headers the user already provided
57 bool hasAccept = false;
58 bool hasConnection = false;
59 bool hasContentLength = false;
60 bool hasContentType = false;
61
62 // User-supplied headers
63 for(const auto& [key, value] : headers)
64 {
65 fmt::format_to(fmt::appender(m_request), "{}: {}\r\n", key, value);
66 if(key == "Accept")
67 hasAccept = true;
68 else if(key == "Connection")
69 hasConnection = true;
70 else if(key == "Content-Length")
71 hasContentLength = true;
72 else if(key == "Content-Type")
73 hasContentType = true;
74 }
75
76 // Fill in defaults for headers the user didn't set
77 if(!hasAccept)
78 fmt::format_to(fmt::appender(m_request), "Accept: */*\r\n");
79 if(!hasConnection)
80 fmt::format_to(fmt::appender(m_request), "Connection: close\r\n");
81
82 if(!body.empty())
83 {
84 if(!hasContentLength)
85 fmt::format_to(
86 fmt::appender(m_request), "Content-Length: {}\r\n", body.size());
87 if(!hasContentType)
88 fmt::format_to(
89 fmt::appender(m_request), "Content-Type: application/octet-stream\r\n");
90 }
91
92 // End of headers + body
93 fmt::format_to(fmt::appender(m_request), "\r\n");
94 if(!body.empty())
95 fmt::format_to(fmt::appender(m_request), "{}", body);
96 }
97
98 void resolve(const std::string& server, const std::string& port)
99 {
100 m_resolver.async_resolve(
101 server, port,
102 [self = this->shared_from_this()](
103 const boost::system::error_code& err,
104 const tcp::resolver::results_type& endpoints) {
105 self->handle_resolve(err, endpoints);
106 });
107 }
108
109 void close() { m_socket.close(); }
110
111private:
112 void handle_resolve(
113 const boost::system::error_code& err,
114 const tcp::resolver::results_type& endpoints)
115 {
116 if(!err)
117 {
118 boost::asio::async_connect(
119 m_socket, endpoints,
120 [self = this->shared_from_this()](
121 const boost::system::error_code& err, auto&&...) {
122 self->handle_connect(err);
123 });
124 }
125 else
126 {
127 ossia::logger().error("HTTP Error: {}", err.message());
128 m_err(*this, err.message());
129 }
130 }
131
132 void handle_connect(const boost::system::error_code& err)
133 {
134 if(!err)
135 {
136 boost::asio::const_buffer request(m_request.data(), m_request.size());
137 boost::asio::async_write(
138 m_socket, request,
139 [self = this->shared_from_this()](
140 const boost::system::error_code& err, std::size_t size) {
141 self->handle_write_request(err, size);
142 });
143 }
144 else
145 {
146 ossia::logger().error("HTTP Error: {}", err.message());
147 m_err(*this, err.message());
148 }
149 }
150
151 void handle_write_request(const boost::system::error_code& err, std::size_t size)
152 {
153 if(!err)
154 {
155 boost::asio::async_read_until(
156 m_socket, m_response, "\r\n",
157 [self = this->shared_from_this()](
158 const boost::system::error_code& err, std::size_t size) {
159 self->handle_read_status_line(err, size);
160 });
161 }
162 else
163 {
164 ossia::logger().error("HTTP Error: {}", err.message());
165 m_err(*this, err.message());
166 }
167 }
168
169 void handle_read_status_line(const boost::system::error_code& err, std::size_t size)
170 {
171 if(!err || err == boost::asio::error::eof)
172 {
173 std::istream response_stream(&m_response);
174 std::string http_version;
175 response_stream >> http_version;
176 response_stream >> m_statusCode;
177 std::string status_message;
178 std::getline(response_stream, status_message);
179
180 if(!response_stream || http_version.substr(0, 5) != "HTTP/")
181 {
182 ossia::logger().error("HTTP Error: Invalid response");
183 m_err(*this, "Invalid HTTP response");
184 return;
185 }
186
187 // Read headers (terminated by blank line)
188 boost::asio::async_read_until(
189 m_socket, m_response, "\r\n\r\n",
190 [self = this->shared_from_this()](
191 const boost::system::error_code& err, std::size_t size) {
192 self->handle_read_headers(err, size);
193 });
194 }
195 else
196 {
197 ossia::logger().error("HTTP Error: {}", err.message());
198 m_err(*this, err.message());
199 }
200 }
201
202 void handle_read_headers(const boost::system::error_code& err, std::size_t size)
203 {
204 if(!err || err == boost::asio::error::eof)
205 {
206 std::istream response_stream(&m_response);
207 std::string header;
208 while(std::getline(response_stream, header) && header != "\r")
209 {
210 if(header.starts_with("Content-Length: "))
211 {
212 std::string_view sz(
213 header.begin() + strlen("Content-Length: "), header.end());
214 if(auto num = ossia::parse_relax<int>(sz))
215 m_contentLength = *num;
216 }
217 }
218
219 if(m_contentLength == 0)
220 {
221 // Empty body (e.g. 204 No Content)
222 finish_read(boost::asio::error::eof, 0);
223 }
224 else if(m_contentLength > 0)
225 {
226 if(m_contentLength == (int)m_response.size())
227 {
228 finish_read(boost::asio::error::eof, size);
229 }
230 else
231 {
232 boost::asio::async_read(
233 m_socket, m_response,
234 boost::asio::transfer_exactly(m_contentLength - m_response.size()),
235 [self = this->shared_from_this()](
236 const boost::system::error_code& err, std::size_t size) {
237 self->handle_read_content(err, size);
238 });
239 }
240 }
241 else
242 {
243 // No Content-Length — read until EOF
244 boost::asio::async_read(
245 m_socket, m_response, boost::asio::transfer_all(),
246 [self = this->shared_from_this()](
247 const boost::system::error_code& err, std::size_t size) {
248 self->handle_read_content(err, size);
249 });
250 }
251 }
252 else
253 {
254 ossia::logger().error("HTTP Error: {}", err.message());
255 m_err(*this, err.message());
256 }
257 }
258
259 void handle_read_content(const boost::system::error_code& err, std::size_t size)
260 {
261 if(!err || err == boost::asio::error::eof)
262 finish_read(err, size);
263 else
264 {
265 ossia::logger().error("HTTP Error: {}", err.message());
266 m_err(*this, err.message());
267 }
268 }
269
270 void finish_read(const boost::system::error_code& err, std::size_t size)
271 {
272 const auto& dat = m_response.data();
273 auto begin = boost::asio::buffers_begin(dat);
274 auto end = boost::asio::buffers_end(dat);
275 auto sz = end - begin;
276 std::string str;
277 str.reserve(sz + 16);
278 str.assign(begin, end);
279 m_fun(*this, m_statusCode, str);
280 close();
281 }
282
283 tcp::resolver m_resolver;
284 tcp::socket m_socket;
285 boost::asio::streambuf m_response;
286 int m_contentLength{-1};
287 int m_statusCode{0};
288 Fun m_fun;
289 Err m_err;
290};
291}
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:118