|
|
@@ -1,15 +1,15 @@
|
|
|
//
|
|
|
// httplib.h
|
|
|
//
|
|
|
-// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
|
|
+// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
|
|
// MIT License
|
|
|
//
|
|
|
|
|
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
|
|
#define CPPHTTPLIB_HTTPLIB_H
|
|
|
|
|
|
-#define CPPHTTPLIB_VERSION "0.28.0"
|
|
|
-#define CPPHTTPLIB_VERSION_NUM "0x001C00"
|
|
|
+#define CPPHTTPLIB_VERSION "0.30.0"
|
|
|
+#define CPPHTTPLIB_VERSION_NUM "0x001E00"
|
|
|
|
|
|
/*
|
|
|
* Platform compatibility check
|
|
|
@@ -838,6 +838,50 @@ struct Response {
|
|
|
std::string file_content_content_type_;
|
|
|
};
|
|
|
|
|
|
+enum class Error {
|
|
|
+ Success = 0,
|
|
|
+ Unknown,
|
|
|
+ Connection,
|
|
|
+ BindIPAddress,
|
|
|
+ Read,
|
|
|
+ Write,
|
|
|
+ ExceedRedirectCount,
|
|
|
+ Canceled,
|
|
|
+ SSLConnection,
|
|
|
+ SSLLoadingCerts,
|
|
|
+ SSLServerVerification,
|
|
|
+ SSLServerHostnameVerification,
|
|
|
+ UnsupportedMultipartBoundaryChars,
|
|
|
+ Compression,
|
|
|
+ ConnectionTimeout,
|
|
|
+ ProxyConnection,
|
|
|
+ ConnectionClosed,
|
|
|
+ Timeout,
|
|
|
+ ResourceExhaustion,
|
|
|
+ TooManyFormDataFiles,
|
|
|
+ ExceedMaxPayloadSize,
|
|
|
+ ExceedUriMaxLength,
|
|
|
+ ExceedMaxSocketDescriptorCount,
|
|
|
+ InvalidRequestLine,
|
|
|
+ InvalidHTTPMethod,
|
|
|
+ InvalidHTTPVersion,
|
|
|
+ InvalidHeaders,
|
|
|
+ MultipartParsing,
|
|
|
+ OpenFile,
|
|
|
+ Listen,
|
|
|
+ GetSockName,
|
|
|
+ UnsupportedAddressFamily,
|
|
|
+ HTTPParsing,
|
|
|
+ InvalidRangeHeader,
|
|
|
+
|
|
|
+ // For internal use only
|
|
|
+ SSLPeerCouldBeClosed_,
|
|
|
+};
|
|
|
+
|
|
|
+std::string to_string(Error error);
|
|
|
+
|
|
|
+std::ostream &operator<<(std::ostream &os, const Error &obj);
|
|
|
+
|
|
|
class Stream {
|
|
|
public:
|
|
|
virtual ~Stream() = default;
|
|
|
@@ -856,6 +900,11 @@ public:
|
|
|
|
|
|
ssize_t write(const char *ptr);
|
|
|
ssize_t write(const std::string &s);
|
|
|
+
|
|
|
+ Error get_error() const { return error_; }
|
|
|
+
|
|
|
+protected:
|
|
|
+ Error error_ = Error::Success;
|
|
|
};
|
|
|
|
|
|
class TaskQueue {
|
|
|
@@ -873,6 +922,7 @@ class ThreadPool final : public TaskQueue {
|
|
|
public:
|
|
|
explicit ThreadPool(size_t n, size_t mqr = 0)
|
|
|
: shutdown_(false), max_queued_requests_(mqr) {
|
|
|
+ threads_.reserve(n);
|
|
|
while (n) {
|
|
|
threads_.emplace_back(worker(*this));
|
|
|
n--;
|
|
|
@@ -961,27 +1011,21 @@ using ErrorLogger = std::function<void(const Error &, const Request *)>;
|
|
|
|
|
|
using SocketOptions = std::function<void(socket_t sock)>;
|
|
|
|
|
|
-namespace detail {
|
|
|
-
|
|
|
-bool set_socket_opt_impl(socket_t sock, int level, int optname,
|
|
|
- const void *optval, socklen_t optlen);
|
|
|
-bool set_socket_opt(socket_t sock, int level, int optname, int opt);
|
|
|
-bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec,
|
|
|
- time_t usec);
|
|
|
-
|
|
|
-} // namespace detail
|
|
|
-
|
|
|
void default_socket_options(socket_t sock);
|
|
|
|
|
|
const char *status_message(int status);
|
|
|
|
|
|
+std::string to_string(Error error);
|
|
|
+
|
|
|
+std::ostream &operator<<(std::ostream &os, const Error &obj);
|
|
|
+
|
|
|
std::string get_bearer_token_auth(const Request &req);
|
|
|
|
|
|
namespace detail {
|
|
|
|
|
|
class MatcherBase {
|
|
|
public:
|
|
|
- MatcherBase(std::string pattern) : pattern_(pattern) {}
|
|
|
+ MatcherBase(std::string pattern) : pattern_(std::move(pattern)) {}
|
|
|
virtual ~MatcherBase() = default;
|
|
|
|
|
|
const std::string &pattern() const { return pattern_; }
|
|
|
@@ -1051,10 +1095,9 @@ private:
|
|
|
std::regex regex_;
|
|
|
};
|
|
|
|
|
|
-ssize_t write_headers(Stream &strm, const Headers &headers);
|
|
|
+int close_socket(socket_t sock);
|
|
|
|
|
|
-std::string make_host_and_port_string(const std::string &host, int port,
|
|
|
- bool is_ssl);
|
|
|
+ssize_t write_headers(Stream &strm, const Headers &headers);
|
|
|
|
|
|
} // namespace detail
|
|
|
|
|
|
@@ -1206,7 +1249,11 @@ private:
|
|
|
bool listen_internal();
|
|
|
|
|
|
bool routing(Request &req, Response &res, Stream &strm);
|
|
|
- bool handle_file_request(const Request &req, Response &res);
|
|
|
+ bool handle_file_request(Request &req, Response &res);
|
|
|
+ bool check_if_not_modified(const Request &req, Response &res,
|
|
|
+ const std::string &etag, time_t mtime) const;
|
|
|
+ bool check_if_range(Request &req, const std::string &etag,
|
|
|
+ time_t mtime) const;
|
|
|
bool dispatch_request(Request &req, Response &res,
|
|
|
const Handlers &handlers) const;
|
|
|
bool dispatch_request_for_content_reader(
|
|
|
@@ -1290,48 +1337,6 @@ private:
|
|
|
detail::write_headers;
|
|
|
};
|
|
|
|
|
|
-enum class Error {
|
|
|
- Success = 0,
|
|
|
- Unknown,
|
|
|
- Connection,
|
|
|
- BindIPAddress,
|
|
|
- Read,
|
|
|
- Write,
|
|
|
- ExceedRedirectCount,
|
|
|
- Canceled,
|
|
|
- SSLConnection,
|
|
|
- SSLLoadingCerts,
|
|
|
- SSLServerVerification,
|
|
|
- SSLServerHostnameVerification,
|
|
|
- UnsupportedMultipartBoundaryChars,
|
|
|
- Compression,
|
|
|
- ConnectionTimeout,
|
|
|
- ProxyConnection,
|
|
|
- ResourceExhaustion,
|
|
|
- TooManyFormDataFiles,
|
|
|
- ExceedMaxPayloadSize,
|
|
|
- ExceedUriMaxLength,
|
|
|
- ExceedMaxSocketDescriptorCount,
|
|
|
- InvalidRequestLine,
|
|
|
- InvalidHTTPMethod,
|
|
|
- InvalidHTTPVersion,
|
|
|
- InvalidHeaders,
|
|
|
- MultipartParsing,
|
|
|
- OpenFile,
|
|
|
- Listen,
|
|
|
- GetSockName,
|
|
|
- UnsupportedAddressFamily,
|
|
|
- HTTPParsing,
|
|
|
- InvalidRangeHeader,
|
|
|
-
|
|
|
- // For internal use only
|
|
|
- SSLPeerCouldBeClosed_,
|
|
|
-};
|
|
|
-
|
|
|
-std::string to_string(Error error);
|
|
|
-
|
|
|
-std::ostream &operator<<(std::ostream &os, const Error &obj);
|
|
|
-
|
|
|
class Result {
|
|
|
public:
|
|
|
Result() = default;
|
|
|
@@ -1390,6 +1395,87 @@ private:
|
|
|
#endif
|
|
|
};
|
|
|
|
|
|
+struct ClientConnection {
|
|
|
+ socket_t sock = INVALID_SOCKET;
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ SSL *ssl = nullptr;
|
|
|
+#endif
|
|
|
+
|
|
|
+ bool is_open() const { return sock != INVALID_SOCKET; }
|
|
|
+
|
|
|
+ ClientConnection() = default;
|
|
|
+
|
|
|
+ ~ClientConnection() {
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ if (ssl) {
|
|
|
+ SSL_free(ssl);
|
|
|
+ ssl = nullptr;
|
|
|
+ }
|
|
|
+#endif
|
|
|
+ if (sock != INVALID_SOCKET) {
|
|
|
+ detail::close_socket(sock);
|
|
|
+ sock = INVALID_SOCKET;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ClientConnection(const ClientConnection &) = delete;
|
|
|
+ ClientConnection &operator=(const ClientConnection &) = delete;
|
|
|
+
|
|
|
+ ClientConnection(ClientConnection &&other) noexcept
|
|
|
+ : sock(other.sock)
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ ,
|
|
|
+ ssl(other.ssl)
|
|
|
+#endif
|
|
|
+ {
|
|
|
+ other.sock = INVALID_SOCKET;
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ other.ssl = nullptr;
|
|
|
+#endif
|
|
|
+ }
|
|
|
+
|
|
|
+ ClientConnection &operator=(ClientConnection &&other) noexcept {
|
|
|
+ if (this != &other) {
|
|
|
+ sock = other.sock;
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ ssl = other.ssl;
|
|
|
+#endif
|
|
|
+ other.sock = INVALID_SOCKET;
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ other.ssl = nullptr;
|
|
|
+#endif
|
|
|
+ }
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+namespace detail {
|
|
|
+
|
|
|
+struct ChunkedDecoder;
|
|
|
+
|
|
|
+struct BodyReader {
|
|
|
+ Stream *stream = nullptr;
|
|
|
+ size_t content_length = 0;
|
|
|
+ size_t bytes_read = 0;
|
|
|
+ bool chunked = false;
|
|
|
+ bool eof = false;
|
|
|
+ std::unique_ptr<ChunkedDecoder> chunked_decoder;
|
|
|
+ Error last_error = Error::Success;
|
|
|
+
|
|
|
+ ssize_t read(char *buf, size_t len);
|
|
|
+ bool has_error() const { return last_error != Error::Success; }
|
|
|
+};
|
|
|
+
|
|
|
+inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf,
|
|
|
+ size_t len) {
|
|
|
+ (void)stream;
|
|
|
+ return br.read(buf, len);
|
|
|
+}
|
|
|
+
|
|
|
+class decompressor;
|
|
|
+
|
|
|
+} // namespace detail
|
|
|
+
|
|
|
class ClientImpl {
|
|
|
public:
|
|
|
explicit ClientImpl(const std::string &host);
|
|
|
@@ -1404,6 +1490,43 @@ public:
|
|
|
|
|
|
virtual bool is_valid() const;
|
|
|
|
|
|
+ struct StreamHandle {
|
|
|
+ std::unique_ptr<Response> response;
|
|
|
+ Error error = Error::Success;
|
|
|
+
|
|
|
+ StreamHandle() = default;
|
|
|
+ StreamHandle(const StreamHandle &) = delete;
|
|
|
+ StreamHandle &operator=(const StreamHandle &) = delete;
|
|
|
+ StreamHandle(StreamHandle &&) = default;
|
|
|
+ StreamHandle &operator=(StreamHandle &&) = default;
|
|
|
+ ~StreamHandle() = default;
|
|
|
+
|
|
|
+ bool is_valid() const {
|
|
|
+ return response != nullptr && error == Error::Success;
|
|
|
+ }
|
|
|
+
|
|
|
+ ssize_t read(char *buf, size_t len);
|
|
|
+ void parse_trailers_if_needed();
|
|
|
+ Error get_read_error() const { return body_reader_.last_error; }
|
|
|
+ bool has_read_error() const { return body_reader_.has_error(); }
|
|
|
+
|
|
|
+ bool trailers_parsed_ = false;
|
|
|
+
|
|
|
+ private:
|
|
|
+ friend class ClientImpl;
|
|
|
+
|
|
|
+ ssize_t read_with_decompression(char *buf, size_t len);
|
|
|
+
|
|
|
+ std::unique_ptr<ClientConnection> connection_;
|
|
|
+ std::unique_ptr<Stream> socket_stream_;
|
|
|
+ Stream *stream_ = nullptr;
|
|
|
+ detail::BodyReader body_reader_;
|
|
|
+
|
|
|
+ std::unique_ptr<detail::decompressor> decompressor_;
|
|
|
+ std::string decompress_buffer_;
|
|
|
+ size_t decompress_offset_ = 0;
|
|
|
+ };
|
|
|
+
|
|
|
// clang-format off
|
|
|
Result Get(const std::string &path, DownloadProgress progress = nullptr);
|
|
|
Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr);
|
|
|
@@ -1497,6 +1620,15 @@ public:
|
|
|
Result Options(const std::string &path, const Headers &headers);
|
|
|
// clang-format on
|
|
|
|
|
|
+ // Streaming API: Open a stream for reading response body incrementally
|
|
|
+ // Socket ownership is transferred to StreamHandle for true streaming
|
|
|
+ // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.)
|
|
|
+ StreamHandle open_stream(const std::string &method, const std::string &path,
|
|
|
+ const Params ¶ms = {},
|
|
|
+ const Headers &headers = {},
|
|
|
+ const std::string &body = {},
|
|
|
+ const std::string &content_type = {});
|
|
|
+
|
|
|
bool send(Request &req, Response &res, Error &error);
|
|
|
Result send(const Request &req);
|
|
|
|
|
|
@@ -1592,6 +1724,7 @@ protected:
|
|
|
};
|
|
|
|
|
|
virtual bool create_and_connect_socket(Socket &socket, Error &error);
|
|
|
+ virtual bool ensure_socket_connection(Socket &socket, Error &error);
|
|
|
|
|
|
// All of:
|
|
|
// shutdown_ssl
|
|
|
@@ -1618,7 +1751,6 @@ protected:
|
|
|
// Socket endpoint information
|
|
|
const std::string host_;
|
|
|
const int port_;
|
|
|
- const std::string host_and_port_;
|
|
|
|
|
|
// Current open socket
|
|
|
Socket socket_;
|
|
|
@@ -1717,6 +1849,8 @@ private:
|
|
|
Response &res) const;
|
|
|
bool write_request(Stream &strm, Request &req, bool close_connection,
|
|
|
Error &error);
|
|
|
+ void prepare_default_headers(Request &r, bool for_stream,
|
|
|
+ const std::string &ct);
|
|
|
bool redirect(Request &req, Response &res, Error &error);
|
|
|
bool create_redirect_client(const std::string &scheme,
|
|
|
const std::string &host, int port, Request &req,
|
|
|
@@ -1747,6 +1881,8 @@ private:
|
|
|
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
|
|
std::function<bool(Stream &strm)> callback);
|
|
|
virtual bool is_ssl() const;
|
|
|
+
|
|
|
+ void transfer_socket_ownership_to_handle(StreamHandle &handle);
|
|
|
};
|
|
|
|
|
|
class Client {
|
|
|
@@ -1865,6 +2001,16 @@ public:
|
|
|
Result Options(const std::string &path, const Headers &headers);
|
|
|
// clang-format on
|
|
|
|
|
|
+ // Streaming API: Open a stream for reading response body incrementally
|
|
|
+ // Socket ownership is transferred to StreamHandle for true streaming
|
|
|
+ // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.)
|
|
|
+ ClientImpl::StreamHandle open_stream(const std::string &method,
|
|
|
+ const std::string &path,
|
|
|
+ const Params ¶ms = {},
|
|
|
+ const Headers &headers = {},
|
|
|
+ const std::string &body = {},
|
|
|
+ const std::string &content_type = {});
|
|
|
+
|
|
|
bool send(Request &req, Response &res, Error &error);
|
|
|
Result send(const Request &req);
|
|
|
|
|
|
@@ -2027,6 +2173,7 @@ public:
|
|
|
|
|
|
private:
|
|
|
bool create_and_connect_socket(Socket &socket, Error &error) override;
|
|
|
+ bool ensure_socket_connection(Socket &socket, Error &error) override;
|
|
|
void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override;
|
|
|
void shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully);
|
|
|
|
|
|
@@ -2163,82 +2310,6 @@ inline void default_socket_options(socket_t sock) {
|
|
|
1);
|
|
|
}
|
|
|
|
|
|
-inline const char *status_message(int status) {
|
|
|
- switch (status) {
|
|
|
- case StatusCode::Continue_100: return "Continue";
|
|
|
- case StatusCode::SwitchingProtocol_101: return "Switching Protocol";
|
|
|
- case StatusCode::Processing_102: return "Processing";
|
|
|
- case StatusCode::EarlyHints_103: return "Early Hints";
|
|
|
- case StatusCode::OK_200: return "OK";
|
|
|
- case StatusCode::Created_201: return "Created";
|
|
|
- case StatusCode::Accepted_202: return "Accepted";
|
|
|
- case StatusCode::NonAuthoritativeInformation_203:
|
|
|
- return "Non-Authoritative Information";
|
|
|
- case StatusCode::NoContent_204: return "No Content";
|
|
|
- case StatusCode::ResetContent_205: return "Reset Content";
|
|
|
- case StatusCode::PartialContent_206: return "Partial Content";
|
|
|
- case StatusCode::MultiStatus_207: return "Multi-Status";
|
|
|
- case StatusCode::AlreadyReported_208: return "Already Reported";
|
|
|
- case StatusCode::IMUsed_226: return "IM Used";
|
|
|
- case StatusCode::MultipleChoices_300: return "Multiple Choices";
|
|
|
- case StatusCode::MovedPermanently_301: return "Moved Permanently";
|
|
|
- case StatusCode::Found_302: return "Found";
|
|
|
- case StatusCode::SeeOther_303: return "See Other";
|
|
|
- case StatusCode::NotModified_304: return "Not Modified";
|
|
|
- case StatusCode::UseProxy_305: return "Use Proxy";
|
|
|
- case StatusCode::unused_306: return "unused";
|
|
|
- case StatusCode::TemporaryRedirect_307: return "Temporary Redirect";
|
|
|
- case StatusCode::PermanentRedirect_308: return "Permanent Redirect";
|
|
|
- case StatusCode::BadRequest_400: return "Bad Request";
|
|
|
- case StatusCode::Unauthorized_401: return "Unauthorized";
|
|
|
- case StatusCode::PaymentRequired_402: return "Payment Required";
|
|
|
- case StatusCode::Forbidden_403: return "Forbidden";
|
|
|
- case StatusCode::NotFound_404: return "Not Found";
|
|
|
- case StatusCode::MethodNotAllowed_405: return "Method Not Allowed";
|
|
|
- case StatusCode::NotAcceptable_406: return "Not Acceptable";
|
|
|
- case StatusCode::ProxyAuthenticationRequired_407:
|
|
|
- return "Proxy Authentication Required";
|
|
|
- case StatusCode::RequestTimeout_408: return "Request Timeout";
|
|
|
- case StatusCode::Conflict_409: return "Conflict";
|
|
|
- case StatusCode::Gone_410: return "Gone";
|
|
|
- case StatusCode::LengthRequired_411: return "Length Required";
|
|
|
- case StatusCode::PreconditionFailed_412: return "Precondition Failed";
|
|
|
- case StatusCode::PayloadTooLarge_413: return "Payload Too Large";
|
|
|
- case StatusCode::UriTooLong_414: return "URI Too Long";
|
|
|
- case StatusCode::UnsupportedMediaType_415: return "Unsupported Media Type";
|
|
|
- case StatusCode::RangeNotSatisfiable_416: return "Range Not Satisfiable";
|
|
|
- case StatusCode::ExpectationFailed_417: return "Expectation Failed";
|
|
|
- case StatusCode::ImATeapot_418: return "I'm a teapot";
|
|
|
- case StatusCode::MisdirectedRequest_421: return "Misdirected Request";
|
|
|
- case StatusCode::UnprocessableContent_422: return "Unprocessable Content";
|
|
|
- case StatusCode::Locked_423: return "Locked";
|
|
|
- case StatusCode::FailedDependency_424: return "Failed Dependency";
|
|
|
- case StatusCode::TooEarly_425: return "Too Early";
|
|
|
- case StatusCode::UpgradeRequired_426: return "Upgrade Required";
|
|
|
- case StatusCode::PreconditionRequired_428: return "Precondition Required";
|
|
|
- case StatusCode::TooManyRequests_429: return "Too Many Requests";
|
|
|
- case StatusCode::RequestHeaderFieldsTooLarge_431:
|
|
|
- return "Request Header Fields Too Large";
|
|
|
- case StatusCode::UnavailableForLegalReasons_451:
|
|
|
- return "Unavailable For Legal Reasons";
|
|
|
- case StatusCode::NotImplemented_501: return "Not Implemented";
|
|
|
- case StatusCode::BadGateway_502: return "Bad Gateway";
|
|
|
- case StatusCode::ServiceUnavailable_503: return "Service Unavailable";
|
|
|
- case StatusCode::GatewayTimeout_504: return "Gateway Timeout";
|
|
|
- case StatusCode::HttpVersionNotSupported_505:
|
|
|
- return "HTTP Version Not Supported";
|
|
|
- case StatusCode::VariantAlsoNegotiates_506: return "Variant Also Negotiates";
|
|
|
- case StatusCode::InsufficientStorage_507: return "Insufficient Storage";
|
|
|
- case StatusCode::LoopDetected_508: return "Loop Detected";
|
|
|
- case StatusCode::NotExtended_510: return "Not Extended";
|
|
|
- case StatusCode::NetworkAuthenticationRequired_511:
|
|
|
- return "Network Authentication Required";
|
|
|
-
|
|
|
- default:
|
|
|
- case StatusCode::InternalServerError_500: return "Internal Server Error";
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
inline std::string get_bearer_token_auth(const Request &req) {
|
|
|
if (req.has_header("Authorization")) {
|
|
|
constexpr auto bearer_header_prefix_len = detail::str_len("Bearer ");
|
|
|
@@ -2272,55 +2343,6 @@ Server::set_idle_interval(const std::chrono::duration<Rep, Period> &duration) {
|
|
|
return *this;
|
|
|
}
|
|
|
|
|
|
-inline std::string to_string(const Error error) {
|
|
|
- switch (error) {
|
|
|
- case Error::Success: return "Success (no error)";
|
|
|
- case Error::Unknown: return "Unknown";
|
|
|
- case Error::Connection: return "Could not establish connection";
|
|
|
- case Error::BindIPAddress: return "Failed to bind IP address";
|
|
|
- case Error::Read: return "Failed to read connection";
|
|
|
- case Error::Write: return "Failed to write connection";
|
|
|
- case Error::ExceedRedirectCount: return "Maximum redirect count exceeded";
|
|
|
- case Error::Canceled: return "Connection handling canceled";
|
|
|
- case Error::SSLConnection: return "SSL connection failed";
|
|
|
- case Error::SSLLoadingCerts: return "SSL certificate loading failed";
|
|
|
- case Error::SSLServerVerification: return "SSL server verification failed";
|
|
|
- case Error::SSLServerHostnameVerification:
|
|
|
- return "SSL server hostname verification failed";
|
|
|
- case Error::UnsupportedMultipartBoundaryChars:
|
|
|
- return "Unsupported HTTP multipart boundary characters";
|
|
|
- case Error::Compression: return "Compression failed";
|
|
|
- case Error::ConnectionTimeout: return "Connection timed out";
|
|
|
- case Error::ProxyConnection: return "Proxy connection failed";
|
|
|
- case Error::ResourceExhaustion: return "Resource exhaustion";
|
|
|
- case Error::TooManyFormDataFiles: return "Too many form data files";
|
|
|
- case Error::ExceedMaxPayloadSize: return "Exceeded maximum payload size";
|
|
|
- case Error::ExceedUriMaxLength: return "Exceeded maximum URI length";
|
|
|
- case Error::ExceedMaxSocketDescriptorCount:
|
|
|
- return "Exceeded maximum socket descriptor count";
|
|
|
- case Error::InvalidRequestLine: return "Invalid request line";
|
|
|
- case Error::InvalidHTTPMethod: return "Invalid HTTP method";
|
|
|
- case Error::InvalidHTTPVersion: return "Invalid HTTP version";
|
|
|
- case Error::InvalidHeaders: return "Invalid headers";
|
|
|
- case Error::MultipartParsing: return "Multipart parsing failed";
|
|
|
- case Error::OpenFile: return "Failed to open file";
|
|
|
- case Error::Listen: return "Failed to listen on socket";
|
|
|
- case Error::GetSockName: return "Failed to get socket name";
|
|
|
- case Error::UnsupportedAddressFamily: return "Unsupported address family";
|
|
|
- case Error::HTTPParsing: return "HTTP parsing failed";
|
|
|
- case Error::InvalidRangeHeader: return "Invalid Range header";
|
|
|
- default: break;
|
|
|
- }
|
|
|
-
|
|
|
- return "Invalid";
|
|
|
-}
|
|
|
-
|
|
|
-inline std::ostream &operator<<(std::ostream &os, const Error &obj) {
|
|
|
- os << to_string(obj);
|
|
|
- os << " (" << static_cast<std::underlying_type<Error>::type>(obj) << ')';
|
|
|
- return os;
|
|
|
-}
|
|
|
-
|
|
|
inline size_t Result::get_request_header_value_u64(const std::string &key,
|
|
|
size_t def,
|
|
|
size_t id) const {
|
|
|
@@ -2439,6 +2461,8 @@ struct FileStat {
|
|
|
FileStat(const std::string &path);
|
|
|
bool is_file() const;
|
|
|
bool is_dir() const;
|
|
|
+ time_t mtime() const;
|
|
|
+ size_t size() const;
|
|
|
|
|
|
private:
|
|
|
#if defined(_WIN32)
|
|
|
@@ -2449,6 +2473,9 @@ private:
|
|
|
int ret_ = -1;
|
|
|
};
|
|
|
|
|
|
+std::string make_host_and_port_string(const std::string &host, int port,
|
|
|
+ bool is_ssl);
|
|
|
+
|
|
|
std::string trim_copy(const std::string &s);
|
|
|
|
|
|
void divide(
|
|
|
@@ -2669,6 +2696,25 @@ private:
|
|
|
std::string growable_buffer_;
|
|
|
};
|
|
|
|
|
|
+bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
|
|
|
+ const Headers &src_headers);
|
|
|
+
|
|
|
+struct ChunkedDecoder {
|
|
|
+ Stream &strm;
|
|
|
+ size_t chunk_remaining = 0;
|
|
|
+ bool finished = false;
|
|
|
+ char line_buf[64];
|
|
|
+ size_t last_chunk_total = 0;
|
|
|
+ size_t last_chunk_offset = 0;
|
|
|
+
|
|
|
+ explicit ChunkedDecoder(Stream &s);
|
|
|
+
|
|
|
+ ssize_t read_payload(char *buf, size_t len, size_t &out_chunk_offset,
|
|
|
+ size_t &out_chunk_total);
|
|
|
+
|
|
|
+ bool parse_trailers_into(Headers &dest, const Headers &src_headers);
|
|
|
+};
|
|
|
+
|
|
|
class mmap {
|
|
|
public:
|
|
|
mmap(const char *path);
|
|
|
@@ -2696,58 +2742,668 @@ private:
|
|
|
// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5
|
|
|
namespace fields {
|
|
|
|
|
|
-inline bool is_token_char(char c) {
|
|
|
- return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
|
|
|
- c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
|
|
|
- c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
|
|
|
-}
|
|
|
+bool is_token_char(char c);
|
|
|
+bool is_token(const std::string &s);
|
|
|
+bool is_field_name(const std::string &s);
|
|
|
+bool is_vchar(char c);
|
|
|
+bool is_obs_text(char c);
|
|
|
+bool is_field_vchar(char c);
|
|
|
+bool is_field_content(const std::string &s);
|
|
|
+bool is_field_value(const std::string &s);
|
|
|
+
|
|
|
+} // namespace fields
|
|
|
+
|
|
|
+} // namespace detail
|
|
|
|
|
|
-inline bool is_token(const std::string &s) {
|
|
|
- if (s.empty()) { return false; }
|
|
|
- for (auto c : s) {
|
|
|
- if (!is_token_char(c)) { return false; }
|
|
|
+namespace stream {
|
|
|
+
|
|
|
+class Result {
|
|
|
+public:
|
|
|
+ Result() : chunk_size_(8192) {}
|
|
|
+
|
|
|
+ explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192)
|
|
|
+ : handle_(std::move(handle)), chunk_size_(chunk_size) {}
|
|
|
+
|
|
|
+ Result(Result &&other) noexcept
|
|
|
+ : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
|
|
|
+ current_size_(other.current_size_), chunk_size_(other.chunk_size_),
|
|
|
+ finished_(other.finished_) {
|
|
|
+ other.current_size_ = 0;
|
|
|
+ other.finished_ = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ Result &operator=(Result &&other) noexcept {
|
|
|
+ if (this != &other) {
|
|
|
+ handle_ = std::move(other.handle_);
|
|
|
+ buffer_ = std::move(other.buffer_);
|
|
|
+ current_size_ = other.current_size_;
|
|
|
+ chunk_size_ = other.chunk_size_;
|
|
|
+ finished_ = other.finished_;
|
|
|
+ other.current_size_ = 0;
|
|
|
+ other.finished_ = true;
|
|
|
+ }
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ Result(const Result &) = delete;
|
|
|
+ Result &operator=(const Result &) = delete;
|
|
|
+
|
|
|
+ // Check if the result is valid (connection succeeded and response received)
|
|
|
+ bool is_valid() const { return handle_.is_valid(); }
|
|
|
+ explicit operator bool() const { return is_valid(); }
|
|
|
+
|
|
|
+ // Response status code
|
|
|
+ int status() const {
|
|
|
+ return handle_.response ? handle_.response->status : -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Response headers
|
|
|
+ const Headers &headers() const {
|
|
|
+ static const Headers empty_headers;
|
|
|
+ return handle_.response ? handle_.response->headers : empty_headers;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string get_header_value(const std::string &key,
|
|
|
+ const char *def = "") const {
|
|
|
+ return handle_.response ? handle_.response->get_header_value(key, def)
|
|
|
+ : def;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool has_header(const std::string &key) const {
|
|
|
+ return handle_.response ? handle_.response->has_header(key) : false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Error information
|
|
|
+ Error error() const { return handle_.error; }
|
|
|
+ Error read_error() const { return handle_.get_read_error(); }
|
|
|
+ bool has_read_error() const { return handle_.has_read_error(); }
|
|
|
+
|
|
|
+ // Streaming iteration API
|
|
|
+ // Call next() to read the next chunk, then access data via data()/size()
|
|
|
+ // Returns true if data was read, false when stream is exhausted
|
|
|
+ bool next() {
|
|
|
+ if (!handle_.is_valid() || finished_) { return false; }
|
|
|
+
|
|
|
+ if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
|
|
|
+
|
|
|
+ ssize_t n = handle_.read(&buffer_[0], chunk_size_);
|
|
|
+ if (n > 0) {
|
|
|
+ current_size_ = static_cast<size_t>(n);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ current_size_ = 0;
|
|
|
+ finished_ = true;
|
|
|
+ return false;
|
|
|
}
|
|
|
- return true;
|
|
|
+
|
|
|
+ // Pointer to current chunk data (valid after next() returns true)
|
|
|
+ const char *data() const { return buffer_.data(); }
|
|
|
+
|
|
|
+ // Size of current chunk (valid after next() returns true)
|
|
|
+ size_t size() const { return current_size_; }
|
|
|
+
|
|
|
+ // Convenience method: read all remaining data into a string
|
|
|
+ std::string read_all() {
|
|
|
+ std::string result;
|
|
|
+ while (next()) {
|
|
|
+ result.append(data(), size());
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ ClientImpl::StreamHandle handle_;
|
|
|
+ std::string buffer_;
|
|
|
+ size_t current_size_ = 0;
|
|
|
+ size_t chunk_size_;
|
|
|
+ bool finished_ = false;
|
|
|
+};
|
|
|
+
|
|
|
+// GET
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Get(ClientType &cli, const std::string &path,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("GET", path), chunk_size};
|
|
|
}
|
|
|
|
|
|
-inline bool is_field_name(const std::string &s) { return is_token(s); }
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Get(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("GET", path, {}, headers), chunk_size};
|
|
|
+}
|
|
|
|
|
|
-inline bool is_vchar(char c) { return c >= 33 && c <= 126; }
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Get(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("GET", path, params), chunk_size};
|
|
|
+}
|
|
|
|
|
|
-inline bool is_obs_text(char c) { return 128 <= static_cast<unsigned char>(c); }
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Get(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("GET", path, params, headers), chunk_size};
|
|
|
+}
|
|
|
|
|
|
-inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); }
|
|
|
+// POST
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Post(ClientType &cli, const std::string &path,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("POST", path, {}, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
|
|
|
-inline bool is_field_content(const std::string &s) {
|
|
|
- if (s.empty()) { return true; }
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Post(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, const std::string &body,
|
|
|
+ const std::string &content_type, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("POST", path, {}, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
|
|
|
- if (s.size() == 1) {
|
|
|
- return is_field_vchar(s[0]);
|
|
|
- } else if (s.size() == 2) {
|
|
|
- return is_field_vchar(s[0]) && is_field_vchar(s[1]);
|
|
|
- } else {
|
|
|
- size_t i = 0;
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Post(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const std::string &body,
|
|
|
+ const std::string &content_type, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("POST", path, params, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
|
|
|
- if (!is_field_vchar(s[i])) { return false; }
|
|
|
- i++;
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Post(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{
|
|
|
+ cli.open_stream("POST", path, params, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
|
|
|
- while (i < s.size() - 1) {
|
|
|
- auto c = s[i++];
|
|
|
- if (c == ' ' || c == '\t' || is_field_vchar(c)) {
|
|
|
- } else {
|
|
|
- return false;
|
|
|
+// PUT
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Put(ClientType &cli, const std::string &path,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("PUT", path, {}, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Put(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, const std::string &body,
|
|
|
+ const std::string &content_type, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("PUT", path, {}, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Put(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const std::string &body,
|
|
|
+ const std::string &content_type, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("PUT", path, params, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Put(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{
|
|
|
+ cli.open_stream("PUT", path, params, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+// PATCH
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Patch(ClientType &cli, const std::string &path,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("PATCH", path, {}, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Patch(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, const std::string &body,
|
|
|
+ const std::string &content_type, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("PATCH", path, {}, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Patch(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const std::string &body,
|
|
|
+ const std::string &content_type, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("PATCH", path, params, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Patch(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{
|
|
|
+ cli.open_stream("PATCH", path, params, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+// DELETE
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("DELETE", path), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("DELETE", path, {}, headers), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("DELETE", path, {}, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, const std::string &body,
|
|
|
+ const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{
|
|
|
+ cli.open_stream("DELETE", path, {}, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("DELETE", path, params), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("DELETE", path, params, headers), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const std::string &body,
|
|
|
+ const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("DELETE", path, params, {}, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Delete(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ const std::string &body, const std::string &content_type,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{
|
|
|
+ cli.open_stream("DELETE", path, params, headers, body, content_type),
|
|
|
+ chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+// HEAD
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Head(ClientType &cli, const std::string &path,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("HEAD", path), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Head(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("HEAD", path, {}, headers), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Head(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("HEAD", path, params), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Head(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("HEAD", path, params, headers), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+// OPTIONS
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Options(ClientType &cli, const std::string &path,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("OPTIONS", path), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Options(ClientType &cli, const std::string &path,
|
|
|
+ const Headers &headers, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("OPTIONS", path, {}, headers), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Options(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("OPTIONS", path, params), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+template <typename ClientType>
|
|
|
+inline Result Options(ClientType &cli, const std::string &path,
|
|
|
+ const Params ¶ms, const Headers &headers,
|
|
|
+ size_t chunk_size = 8192) {
|
|
|
+ return Result{cli.open_stream("OPTIONS", path, params, headers), chunk_size};
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace stream
|
|
|
+
|
|
|
+namespace sse {
|
|
|
+
|
|
|
+struct SSEMessage {
|
|
|
+ std::string event; // Event type (default: "message")
|
|
|
+ std::string data; // Event payload
|
|
|
+ std::string id; // Event ID for Last-Event-ID header
|
|
|
+
|
|
|
+ SSEMessage() : event("message") {}
|
|
|
+
|
|
|
+ void clear() {
|
|
|
+ event = "message";
|
|
|
+ data.clear();
|
|
|
+ id.clear();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+class SSEClient {
|
|
|
+public:
|
|
|
+ using MessageHandler = std::function<void(const SSEMessage &)>;
|
|
|
+ using ErrorHandler = std::function<void(Error)>;
|
|
|
+ using OpenHandler = std::function<void()>;
|
|
|
+
|
|
|
+ SSEClient(Client &client, const std::string &path)
|
|
|
+ : client_(client), path_(path) {}
|
|
|
+
|
|
|
+ SSEClient(Client &client, const std::string &path, const Headers &headers)
|
|
|
+ : client_(client), path_(path), headers_(headers) {}
|
|
|
+
|
|
|
+ ~SSEClient() { stop(); }
|
|
|
+
|
|
|
+ SSEClient(const SSEClient &) = delete;
|
|
|
+ SSEClient &operator=(const SSEClient &) = delete;
|
|
|
+
|
|
|
+ // Event handlers
|
|
|
+ SSEClient &on_message(MessageHandler handler) {
|
|
|
+ on_message_ = std::move(handler);
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ SSEClient &on_event(const std::string &type, MessageHandler handler) {
|
|
|
+ event_handlers_[type] = std::move(handler);
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ SSEClient &on_open(OpenHandler handler) {
|
|
|
+ on_open_ = std::move(handler);
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ SSEClient &on_error(ErrorHandler handler) {
|
|
|
+ on_error_ = std::move(handler);
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ SSEClient &set_reconnect_interval(int ms) {
|
|
|
+ reconnect_interval_ms_ = ms;
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ SSEClient &set_max_reconnect_attempts(int n) {
|
|
|
+ max_reconnect_attempts_ = n;
|
|
|
+ return *this;
|
|
|
+ }
|
|
|
+
|
|
|
+ // State accessors
|
|
|
+ bool is_connected() const { return connected_.load(); }
|
|
|
+ const std::string &last_event_id() const { return last_event_id_; }
|
|
|
+
|
|
|
+ // Blocking start - runs event loop with auto-reconnect
|
|
|
+ void start() {
|
|
|
+ running_.store(true);
|
|
|
+ run_event_loop();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Non-blocking start - runs in background thread
|
|
|
+ void start_async() {
|
|
|
+ running_.store(true);
|
|
|
+ async_thread_ = std::thread([this]() { run_event_loop(); });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stop the client (thread-safe)
|
|
|
+ void stop() {
|
|
|
+ running_.store(false);
|
|
|
+ client_.stop(); // Cancel any pending operations
|
|
|
+ if (async_thread_.joinable()) { async_thread_.join(); }
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ // Parse a single SSE field line
|
|
|
+ // Returns true if this line ends an event (blank line)
|
|
|
+ bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms) {
|
|
|
+ // Blank line signals end of event
|
|
|
+ if (line.empty() || line == "\r") { return true; }
|
|
|
+
|
|
|
+ // Lines starting with ':' are comments (ignored)
|
|
|
+ if (!line.empty() && line[0] == ':') { return false; }
|
|
|
+
|
|
|
+ // Find the colon separator
|
|
|
+ auto colon_pos = line.find(':');
|
|
|
+ if (colon_pos == std::string::npos) {
|
|
|
+ // Line with no colon is treated as field name with empty value
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto field = line.substr(0, colon_pos);
|
|
|
+ std::string value;
|
|
|
+
|
|
|
+ // Value starts after colon, skip optional single space
|
|
|
+ if (colon_pos + 1 < line.size()) {
|
|
|
+ auto value_start = colon_pos + 1;
|
|
|
+ if (line[value_start] == ' ') { value_start++; }
|
|
|
+ value = line.substr(value_start);
|
|
|
+ // Remove trailing \r if present
|
|
|
+ if (!value.empty() && value.back() == '\r') { value.pop_back(); }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle known fields
|
|
|
+ if (field == "event") {
|
|
|
+ msg.event = value;
|
|
|
+ } else if (field == "data") {
|
|
|
+ // Multiple data lines are concatenated with newlines
|
|
|
+ if (!msg.data.empty()) { msg.data += "\n"; }
|
|
|
+ msg.data += value;
|
|
|
+ } else if (field == "id") {
|
|
|
+ // Empty id is valid (clears the last event ID)
|
|
|
+ msg.id = value;
|
|
|
+ } else if (field == "retry") {
|
|
|
+ // Parse retry interval in milliseconds
|
|
|
+ try {
|
|
|
+ retry_ms = std::stoi(value);
|
|
|
+ } catch (...) {
|
|
|
+ // Invalid retry value, ignore
|
|
|
}
|
|
|
}
|
|
|
+ // Unknown fields are ignored per SSE spec
|
|
|
|
|
|
- return is_field_vchar(s[i]);
|
|
|
+ return false;
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-inline bool is_field_value(const std::string &s) { return is_field_content(s); }
|
|
|
+ // Main event loop with auto-reconnect
|
|
|
+ void run_event_loop() {
|
|
|
+ auto reconnect_count = 0;
|
|
|
|
|
|
-} // namespace fields
|
|
|
+ while (running_.load()) {
|
|
|
+ // Build headers, including Last-Event-ID if we have one
|
|
|
+ auto request_headers = headers_;
|
|
|
+ if (!last_event_id_.empty()) {
|
|
|
+ request_headers.emplace("Last-Event-ID", last_event_id_);
|
|
|
+ }
|
|
|
|
|
|
-} // namespace detail
|
|
|
+ // Open streaming connection
|
|
|
+ auto result = stream::Get(client_, path_, request_headers);
|
|
|
+
|
|
|
+ // Connection error handling
|
|
|
+ if (!result) {
|
|
|
+ connected_.store(false);
|
|
|
+ if (on_error_) { on_error_(result.error()); }
|
|
|
+
|
|
|
+ if (!should_reconnect(reconnect_count)) { break; }
|
|
|
+ wait_for_reconnect();
|
|
|
+ reconnect_count++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result.status() != 200) {
|
|
|
+ connected_.store(false);
|
|
|
+ // For certain errors, don't reconnect
|
|
|
+ if (result.status() == 204 || // No Content - server wants us to stop
|
|
|
+ result.status() == 404 || // Not Found
|
|
|
+ result.status() == 401 || // Unauthorized
|
|
|
+ result.status() == 403) { // Forbidden
|
|
|
+ if (on_error_) { on_error_(Error::Connection); }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (on_error_) { on_error_(Error::Connection); }
|
|
|
+
|
|
|
+ if (!should_reconnect(reconnect_count)) { break; }
|
|
|
+ wait_for_reconnect();
|
|
|
+ reconnect_count++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Connection successful
|
|
|
+ connected_.store(true);
|
|
|
+ reconnect_count = 0;
|
|
|
+ if (on_open_) { on_open_(); }
|
|
|
+
|
|
|
+ // Event receiving loop
|
|
|
+ std::string buffer;
|
|
|
+ SSEMessage current_msg;
|
|
|
+
|
|
|
+ while (running_.load() && result.next()) {
|
|
|
+ buffer.append(result.data(), result.size());
|
|
|
+
|
|
|
+ // Process complete lines in the buffer
|
|
|
+ size_t line_start = 0;
|
|
|
+ size_t newline_pos;
|
|
|
+
|
|
|
+ while ((newline_pos = buffer.find('\n', line_start)) !=
|
|
|
+ std::string::npos) {
|
|
|
+ auto line = buffer.substr(line_start, newline_pos - line_start);
|
|
|
+ line_start = newline_pos + 1;
|
|
|
+
|
|
|
+ // Parse the line and check if event is complete
|
|
|
+ auto event_complete =
|
|
|
+ parse_sse_line(line, current_msg, reconnect_interval_ms_);
|
|
|
+
|
|
|
+ if (event_complete && !current_msg.data.empty()) {
|
|
|
+ // Update last_event_id for reconnection
|
|
|
+ if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
|
|
+
|
|
|
+ // Dispatch event to appropriate handler
|
|
|
+ dispatch_event(current_msg);
|
|
|
+
|
|
|
+ current_msg.clear();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Keep unprocessed data in buffer
|
|
|
+ buffer.erase(0, line_start);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Connection ended
|
|
|
+ connected_.store(false);
|
|
|
+
|
|
|
+ if (!running_.load()) { break; }
|
|
|
+
|
|
|
+ // Check for read errors
|
|
|
+ if (result.has_read_error()) {
|
|
|
+ if (on_error_) { on_error_(result.read_error()); }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!should_reconnect(reconnect_count)) { break; }
|
|
|
+ wait_for_reconnect();
|
|
|
+ reconnect_count++;
|
|
|
+ }
|
|
|
+
|
|
|
+ connected_.store(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Dispatch event to appropriate handler
|
|
|
+ void dispatch_event(const SSEMessage &msg) {
|
|
|
+ // Check for specific event type handler first
|
|
|
+ auto it = event_handlers_.find(msg.event);
|
|
|
+ if (it != event_handlers_.end()) {
|
|
|
+ it->second(msg);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fall back to generic message handler
|
|
|
+ if (on_message_) { on_message_(msg); }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if we should attempt to reconnect
|
|
|
+ bool should_reconnect(int count) const {
|
|
|
+ if (!running_.load()) { return false; }
|
|
|
+ if (max_reconnect_attempts_ == 0) { return true; } // unlimited
|
|
|
+ return count < max_reconnect_attempts_;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Wait for reconnect interval
|
|
|
+ void wait_for_reconnect() {
|
|
|
+ // Use small increments to check running_ flag frequently
|
|
|
+ auto waited = 0;
|
|
|
+ while (running_.load() && waited < reconnect_interval_ms_) {
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
|
+ waited += 100;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Client and path
|
|
|
+ Client &client_;
|
|
|
+ std::string path_;
|
|
|
+ Headers headers_;
|
|
|
+
|
|
|
+ // Callbacks
|
|
|
+ MessageHandler on_message_;
|
|
|
+ std::map<std::string, MessageHandler> event_handlers_;
|
|
|
+ OpenHandler on_open_;
|
|
|
+ ErrorHandler on_error_;
|
|
|
+
|
|
|
+ // Configuration
|
|
|
+ int reconnect_interval_ms_ = 3000;
|
|
|
+ int max_reconnect_attempts_ = 0; // 0 = unlimited
|
|
|
+
|
|
|
+ // State
|
|
|
+ std::atomic<bool> running_{false};
|
|
|
+ std::atomic<bool> connected_{false};
|
|
|
+ std::string last_event_id_;
|
|
|
+
|
|
|
+ // Async support
|
|
|
+ std::thread async_thread_;
|
|
|
+};
|
|
|
+
|
|
|
+} // namespace sse
|
|
|
|
|
|
|
|
|
|