2#include <ossia/detail/config.hpp>
4#include <ossia/detail/fmt.hpp>
6#include <ossia/detail/parse_relax.hpp>
8#include <boost/asio.hpp>
15using tcp = boost::asio::ip::tcp;
20inline bool http_verb_is(std::string_view verb, std::string_view upper)
noexcept
22 if(verb.size() != upper.size())
24 for(std::size_t i = 0; i < verb.size(); ++i)
25 if((verb[i] | 0x20) != (upper[i] | 0x20))
33template <
typename Fun,
typename Err>
34class http_client_request
35 :
public std::enable_shared_from_this<http_client_request<Fun, Err>>
37 fmt::memory_buffer m_request;
40 using std::enable_shared_from_this<http_client_request<Fun, Err>>::shared_from_this;
43 Fun f, Err err, boost::asio::io_context& ctx, std::string_view verb,
44 std::string_view host, std::string_view path,
45 const std::vector<std::pair<std::string, std::string>>& headers = {},
46 std::string_view body = {})
49 , m_headRequest{http_verb_is(verb,
"HEAD")}
50 , m_connectRequest{http_verb_is(verb,
"CONNECT")}
52 , m_err{std::move(err)}
54 m_request.reserve(256 + host.size() + path.size() + body.size());
55 m_response.prepare(Fun::reserve_expect);
58 fmt::format_to(fmt::appender(m_request),
"{} ", verb);
62 fmt::format_to(fmt::appender(m_request),
"{}", c);
64 fmt::format_to(fmt::appender(m_request),
"%20");
66 fmt::format_to(fmt::appender(m_request),
" HTTP/1.1\r\n");
69 fmt::format_to(fmt::appender(m_request),
"Host: {}\r\n", host);
72 bool hasAccept =
false;
73 bool hasConnection =
false;
74 bool hasContentLength =
false;
75 bool hasContentType =
false;
78 for(
const auto& [key, value] : headers)
80 fmt::format_to(fmt::appender(m_request),
"{}: {}\r\n", key, value);
83 else if(key ==
"Connection")
85 else if(key ==
"Content-Length")
86 hasContentLength =
true;
87 else if(key ==
"Content-Type")
88 hasContentType =
true;
93 fmt::format_to(fmt::appender(m_request),
"Accept: */*\r\n");
95 fmt::format_to(fmt::appender(m_request),
"Connection: close\r\n");
101 fmt::appender(m_request),
"Content-Length: {}\r\n", body.size());
104 fmt::appender(m_request),
"Content-Type: application/octet-stream\r\n");
108 fmt::format_to(fmt::appender(m_request),
"\r\n");
110 fmt::format_to(fmt::appender(m_request),
"{}", body);
113 void resolve(
const std::string& server,
const std::string& port)
115 m_resolver.async_resolve(
117 [self = this->shared_from_this()](
118 const boost::system::error_code& err,
119 const tcp::resolver::results_type& endpoints) {
120 self->handle_resolve(err, endpoints);
124 void close() { m_socket.close(); }
128 const boost::system::error_code& err,
129 const tcp::resolver::results_type& endpoints)
133 boost::asio::async_connect(
135 [self = this->shared_from_this()](
136 const boost::system::error_code& err,
auto&&...) {
137 self->handle_connect(err);
143 m_err(*
this, err.message());
147 void handle_connect(
const boost::system::error_code& err)
151 boost::asio::const_buffer request(m_request.data(), m_request.size());
152 boost::asio::async_write(
154 [self = this->shared_from_this()](
155 const boost::system::error_code& err, std::size_t size) {
156 self->handle_write_request(err, size);
162 m_err(*
this, err.message());
166 void handle_write_request(
const boost::system::error_code& err, std::size_t size)
170 boost::asio::async_read_until(
171 m_socket, m_response,
"\r\n",
172 [self = this->shared_from_this()](
173 const boost::system::error_code& err, std::size_t size) {
174 self->handle_read_status_line(err, size);
180 m_err(*
this, err.message());
184 void handle_read_status_line(
const boost::system::error_code& err, std::size_t size)
186 if(!err || err == boost::asio::error::eof)
188 std::istream response_stream(&m_response);
189 std::string http_version;
190 response_stream >> http_version;
191 response_stream >> m_statusCode;
192 std::string status_message;
193 std::getline(response_stream, status_message);
195 if(!response_stream || http_version.substr(0, 5) !=
"HTTP/")
198 m_err(*
this,
"Invalid HTTP response");
207 m_err(*
this, err.message());
214 void read_header_line()
216 boost::asio::async_read_until(
217 m_socket, m_response,
"\r\n",
218 [self = this->shared_from_this()](
219 const boost::system::error_code& err, std::size_t size) {
220 self->handle_read_header_line(err, size);
224 void handle_read_header_line(
const boost::system::error_code& err, std::size_t size)
226 if(err && err != boost::asio::error::eof)
229 m_err(*
this, err.message());
233 std::istream response_stream(&m_response);
235 if(!std::getline(response_stream, header))
237 ossia::logger().error(
"HTTP Error: response ended inside its headers");
238 m_err(*
this,
"HTTP response ended inside its headers");
242 if(!(header.empty() || header ==
"\r"))
244 if(header.starts_with(
"Content-Length: "))
246 std::string_view sz(header.begin() + strlen(
"Content-Length: "), header.end());
247 if(
auto num = ossia::parse_relax<int>(sz))
248 m_contentLength = *num;
254 handle_headers_complete(size);
257 void handle_headers_complete(std::size_t size)
261 if(m_statusCode >= 100 && m_statusCode < 200)
264 m_contentLength = -1;
265 boost::asio::async_read_until(
266 m_socket, m_response,
"\r\n",
267 [self = this->shared_from_this()](
268 const boost::system::error_code& err, std::size_t size) {
269 self->handle_read_status_line(err, size);
277 = m_headRequest || m_statusCode == 204 || m_statusCode == 304
278 || (m_connectRequest && m_statusCode >= 200 && m_statusCode < 300);
282 m_response.consume(m_response.size());
284 finish_read(boost::asio::error::eof, 0);
286 else if(m_contentLength == 0)
288 finish_read(boost::asio::error::eof, 0);
290 else if(m_contentLength > 0)
292 if(m_contentLength <= (
int)m_response.size())
294 finish_read(boost::asio::error::eof, size);
298 boost::asio::async_read(
299 m_socket, m_response,
300 boost::asio::transfer_exactly(m_contentLength - m_response.size()),
301 [self = this->shared_from_this()](
302 const boost::system::error_code& err, std::size_t size) {
303 self->handle_read_content(err, size);
310 boost::asio::async_read(
311 m_socket, m_response, boost::asio::transfer_all(),
312 [self = this->shared_from_this()](
313 const boost::system::error_code& err, std::size_t size) {
314 self->handle_read_content(err, size);
319 void handle_read_content(
const boost::system::error_code& err, std::size_t size)
323 finish_read(err, size);
325 else if(err == boost::asio::error::eof)
329 if(m_contentLength > 0 && (
int)m_response.size() < m_contentLength)
331 ossia::logger().error(
"HTTP Error: response truncated before Content-Length");
332 m_err(*
this,
"HTTP response truncated before Content-Length");
335 finish_read(err, size);
340 m_err(*
this, err.message());
344 void finish_read(
const boost::system::error_code& err, std::size_t size)
346 const auto& dat = m_response.data();
347 auto begin = boost::asio::buffers_begin(dat);
348 auto end = boost::asio::buffers_end(dat);
349 auto sz = end - begin;
353 if(m_contentLength >= 0 && sz > (std::ptrdiff_t)m_contentLength)
355 sz = m_contentLength;
360 str.reserve(sz + 16);
361 str.assign(begin, end);
362 m_fun(*
this, m_statusCode, str);
366 tcp::resolver m_resolver;
367 tcp::socket m_socket;
368 boost::asio::streambuf m_response;
369 int m_contentLength{-1};
370 bool m_headRequest{};
371 bool m_connectRequest{};
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:120