From 0b3758ec36bea9ec567aedc8fa1cacbdbc3a83d6 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 30 Jul 2025 17:39:40 -0400 Subject: [PATCH 01/29] Fix problem with Windows version check --- httplib.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 8e65d7d9e0..fc175484c8 100644 --- a/httplib.h +++ b/httplib.h @@ -27,7 +27,7 @@ #endif #ifdef _WIN32 -#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0602 +#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #error \ "cpp-httplib doesn't support Windows 8 or lower. Please use Windows 10 or later." #endif From acf28a362df8b5cc6d1d17d706ec33467fcdbbae Mon Sep 17 00:00:00 2001 From: Thomas Beutlich <115483027+thbeu@users.noreply.github.com> Date: Sat, 2 Aug 2025 02:16:43 +0200 Subject: [PATCH 02/29] #2191 Check for minimum required Windows version (#2192) --- CMakeLists.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 99d9bc4738..cf036b4066 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,12 +111,16 @@ option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build. option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON) # Defaults to static library option(BUILD_SHARED_LIBS "Build the library as a shared library instead of static. Has no effect if using header-only." OFF) -if (BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE) +if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE) # Necessary for Windows if building shared libs # See https://stackoverflow.com/a/40743080 set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() +if( CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0") + message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.") +endif() + # Set some variables that are used in-tree and while building based on our options set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE}) set(HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN ${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}) @@ -243,8 +247,8 @@ add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11) target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC} - $ - $ + $ + $ ) # Always require threads @@ -340,6 +344,6 @@ if(HTTPLIB_INSTALL) endif() if(HTTPLIB_TEST) - include(CTest) - add_subdirectory(test) + include(CTest) + add_subdirectory(test) endif() From b52d7d8411f92ab82588ce20f14559062788c71b Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 6 Aug 2025 17:38:18 -0400 Subject: [PATCH 03/29] ErrorLogger support (#870) (#2195) --- Dockerfile | 4 +- docker/main.cc | 212 +++++++++++++++++++++++++++++++++++---------- httplib.h | 228 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 379 insertions(+), 65 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4abae1793e..67aa028b7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,6 @@ FROM scratch COPY --from=builder /build/server /server COPY docker/html/index.html /html/index.html EXPOSE 80 -CMD ["/server"] + +ENTRYPOINT ["/server"] +CMD ["0.0.0.0", "80", "/", "/html"] diff --git a/docker/main.cc b/docker/main.cc index 62d0c74f5f..784c803b02 100644 --- a/docker/main.cc +++ b/docker/main.cc @@ -5,77 +5,203 @@ // MIT License // +#include #include #include #include #include #include +#include #include #include -constexpr auto error_html = R"( -{} {} - -

404 Not Found

-
cpp-httplib/{}
- - -)"; +using namespace httplib; -void sigint_handler(int s) { exit(1); } +auto SERVER_NAME = + std::format("cpp-httplib-nginxish-server/{}", CPPHTTPLIB_VERSION); -std::string time_local() { - auto p = std::chrono::system_clock::now(); - auto t = std::chrono::system_clock::to_time_t(p); +Server svr; + +void signal_handler(int signal) { + if (signal == SIGINT || signal == SIGTERM) { + std::cout << std::format("\nReceived signal, shutting down gracefully...") + << std::endl; + svr.stop(); + } +} + +std::string get_nginx_time_format() { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; - ss << std::put_time(std::localtime(&t), "%d/%b/%Y:%H:%M:%S %z"); + ss << std::put_time(std::localtime(&time_t), "%d/%b/%Y:%H:%M:%S %z"); return ss.str(); } -std::string log(auto &req, auto &res) { - auto remote_user = "-"; // TODO: - auto request = std::format("{} {} {}", req.method, req.path, req.version); - auto body_bytes_sent = res.get_header_value("Content-Length"); - auto http_referer = "-"; // TODO: - auto http_user_agent = req.get_header_value("User-Agent", "-"); - - // NOTE: From NGINX default access log format - // log_format combined '$remote_addr - $remote_user [$time_local] ' - // '"$request" $status $body_bytes_sent ' - // '"$http_referer" "$http_user_agent"'; - return std::format(R"({} - {} [{}] "{}" {} {} "{}" "{}")", req.remote_addr, - remote_user, time_local(), request, res.status, - body_bytes_sent, http_referer, http_user_agent); +std::string get_nginx_error_time_format() { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + std::stringstream ss; + ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S"); + return ss.str(); +} + +std::string get_client_ip(const Request &req) { + // Check for X-Forwarded-For header first (common in reverse proxy setups) + auto forwarded_for = req.get_header_value("X-Forwarded-For"); + if (!forwarded_for.empty()) { + // Get the first IP if there are multiple + auto comma_pos = forwarded_for.find(','); + if (comma_pos != std::string::npos) { + return forwarded_for.substr(0, comma_pos); + } + return forwarded_for; + } + + // Check for X-Real-IP header + auto real_ip = req.get_header_value("X-Real-IP"); + if (!real_ip.empty()) { return real_ip; } + + // Fallback to remote address (though cpp-httplib doesn't provide this + // directly) For demonstration, we'll use a placeholder + return "127.0.0.1"; } -int main(int argc, const char **argv) { - signal(SIGINT, sigint_handler); +// NGINX Combined log format: +// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent +// "$http_referer" "$http_user_agent" +void nginx_access_logger(const Request &req, const Response &res) { + std::string remote_addr = get_client_ip(req); + std::string remote_user = + "-"; // cpp-httplib doesn't have built-in auth user tracking + std::string time_local = get_nginx_time_format(); + std::string request = + std::format("{} {} {}", req.method, req.path, req.version); + int status = res.status; + size_t body_bytes_sent = res.body.size(); + std::string http_referer = req.get_header_value("Referer"); + if (http_referer.empty()) http_referer = "-"; + std::string http_user_agent = req.get_header_value("User-Agent"); + if (http_user_agent.empty()) http_user_agent = "-"; + + std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"", + remote_addr, remote_user, time_local, request, + status, body_bytes_sent, http_referer, + http_user_agent) + << std::endl; +} - auto base_dir = "./html"; - auto host = "0.0.0.0"; - auto port = 80; +// NGINX Error log format: +// YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request", +// host: "host" +void nginx_error_logger(const Error &err, const Request *req) { + std::string time_local = get_nginx_error_time_format(); + std::string level = "error"; + + if (req) { + std::string client_ip = get_client_ip(*req); + std::string request = + std::format("{} {} {}", req->method, req->path, req->version); + std::string host = req->get_header_value("Host"); + if (host.empty()) host = "-"; + + std::cerr << std::format("{} [{}] {}, client: {}, request: " + "\"{}\", host: \"{}\"", + time_local, level, to_string(err), client_ip, + request, host) + << std::endl; + } else { + // If no request context, just log the error + std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err)) + << std::endl; + } +} - httplib::Server svr; +void print_usage(const char *program_name) { + std::cout << std::format("Usage: {} " + "", + program_name) + << std::endl; - svr.set_error_handler([](auto & /*req*/, auto &res) { - auto body = - std::format(error_html, res.status, httplib::status_message(res.status), - CPPHTTPLIB_VERSION); + std::cout << std::format("Example: {} localhost 8080 /var/www/html .", + program_name) + << std::endl; +} - res.set_content(body, "text/html"); +int main(int argc, char *argv[]) { + if (argc != 5) { + print_usage(argv[0]); + return 1; + } + + std::string hostname = argv[1]; + auto port = std::atoi(argv[2]); + std::string mount_point = argv[3]; + std::string document_root = argv[4]; + + svr.set_logger(nginx_access_logger); + svr.set_error_logger(nginx_error_logger); + + auto ret = svr.set_mount_point(mount_point, document_root); + if (!ret) { + std::cerr + << std::format( + "Error: Cannot mount '{}' to '{}'. Directory may not exist.", + mount_point, document_root) + << std::endl; + return 1; + } + + svr.set_file_extension_and_mimetype_mapping("html", "text/html"); + svr.set_file_extension_and_mimetype_mapping("htm", "text/html"); + svr.set_file_extension_and_mimetype_mapping("css", "text/css"); + svr.set_file_extension_and_mimetype_mapping("js", "text/javascript"); + svr.set_file_extension_and_mimetype_mapping("json", "application/json"); + svr.set_file_extension_and_mimetype_mapping("xml", "application/xml"); + svr.set_file_extension_and_mimetype_mapping("png", "image/png"); + svr.set_file_extension_and_mimetype_mapping("jpg", "image/jpeg"); + svr.set_file_extension_and_mimetype_mapping("jpeg", "image/jpeg"); + svr.set_file_extension_and_mimetype_mapping("gif", "image/gif"); + svr.set_file_extension_and_mimetype_mapping("svg", "image/svg+xml"); + svr.set_file_extension_and_mimetype_mapping("ico", "image/x-icon"); + svr.set_file_extension_and_mimetype_mapping("pdf", "application/pdf"); + svr.set_file_extension_and_mimetype_mapping("zip", "application/zip"); + svr.set_file_extension_and_mimetype_mapping("txt", "text/plain"); + + svr.set_error_handler([](const Request & /*req*/, Response &res) { + if (res.status == 404) { + res.set_content( + std::format( + "404 Not Found" + "

404 Not Found

" + "

The requested resource was not found on this server.

" + "

{}

", + SERVER_NAME), + "text/html"); + } }); - svr.set_logger( - [](auto &req, auto &res) { std::cout << log(req, res) << std::endl; }); + svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) { + res.set_header("Server", SERVER_NAME); + return Server::HandlerResponse::Unhandled; + }); - svr.set_mount_point("/", base_dir); + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); - std::cout << std::format("Serving HTTP on {0} port {1} ...", host, port) + std::cout << std::format("Serving HTTP on {}:{}", hostname, port) + << std::endl; + std::cout << std::format("Mount point: {} -> {}", mount_point, document_root) << std::endl; + std::cout << std::format("Press Ctrl+C to shutdown gracefully...") + << std::endl; + + ret = svr.listen(hostname, port); - auto ret = svr.listen(host, port); + std::cout << std::format("Server has been shut down.") << std::endl; return ret ? 0 : 1; } diff --git a/httplib.h b/httplib.h index fc175484c8..c6976917e0 100644 --- a/httplib.h +++ b/httplib.h @@ -949,6 +949,10 @@ class ThreadPool final : public TaskQueue { using Logger = std::function; +// Forward declaration for Error type +enum class Error; +using ErrorLogger = std::function; + using SocketOptions = std::function; namespace detail { @@ -1109,6 +1113,7 @@ class Server { Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); Server &set_logger(Logger logger); Server &set_pre_compression_logger(Logger logger); + Server &set_error_logger(ErrorLogger error_logger); Server &set_address_family(int family); Server &set_tcp_nodelay(bool on); @@ -1220,6 +1225,11 @@ class Server { virtual bool process_and_close_socket(socket_t sock); + void output_log(const Request &req, const Response &res) const; + void output_pre_compression_log(const Request &req, + const Response &res) const; + void output_error_log(const Error &err, const Request *req) const; + std::atomic is_running_{false}; std::atomic is_decommissioned{false}; @@ -1251,8 +1261,10 @@ class Server { HandlerWithResponse pre_request_handler_; Expect100ContinueHandler expect_100_continue_handler_; + mutable std::mutex logger_mutex_; Logger logger_; Logger pre_compression_logger_; + ErrorLogger error_logger_; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; @@ -1281,6 +1293,22 @@ enum class Error { Compression, ConnectionTimeout, ProxyConnection, + ResourceExhaustion, + TooManyFormDataFiles, + ExceedMaxPayloadSize, + ExceedUriMaxLength, + ExceedMaxSocketDescriptorCount, + InvalidRequestLine, + InvalidHTTPMethod, + InvalidHTTPVersion, + InvalidHeaders, + MultipartParsing, + OpenFile, + Listen, + GetSockName, + UnsupportedAddressFamily, + HTTPParsing, + InvalidRangeHeader, // For internal use only SSLPeerCouldBeClosed_, @@ -1525,6 +1553,7 @@ class ClientImpl { #endif void set_logger(Logger logger); + void set_error_logger(ErrorLogger error_logger); protected: struct Socket { @@ -1557,6 +1586,9 @@ class ClientImpl { void copy_settings(const ClientImpl &rhs); + void output_log(const Request &req, const Response &res) const; + void output_error_log(const Error &err, const Request *req) const; + // Socket endpoint information const std::string host_; const int port_; @@ -1641,7 +1673,9 @@ class ClientImpl { std::function server_certificate_verifier_; #endif + mutable std::mutex logger_mutex_; Logger logger_; + ErrorLogger error_logger_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT int last_ssl_error_ = 0; @@ -1868,6 +1902,7 @@ class Client { #endif void set_logger(Logger logger); + void set_error_logger(ErrorLogger error_logger); // SSL #ifdef CPPHTTPLIB_OPENSSL_SUPPORT @@ -2199,6 +2234,7 @@ Server::set_idle_interval(const std::chrono::duration &duration) { 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"; @@ -2215,7 +2251,23 @@ inline std::string to_string(const Error error) { case Error::Compression: return "Compression failed"; case Error::ConnectionTimeout: return "Connection timed out"; case Error::ProxyConnection: return "Proxy connection failed"; - case Error::Unknown: return "Unknown"; + 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; } @@ -7359,6 +7411,11 @@ inline Server &Server::set_logger(Logger logger) { return *this; } +inline Server &Server::set_error_logger(ErrorLogger error_logger) { + error_logger_ = std::move(error_logger); + return *this; +} + inline Server &Server::set_pre_compression_logger(Logger logger) { pre_compression_logger_ = std::move(logger); return *this; @@ -7498,9 +7555,15 @@ inline bool Server::parse_request_line(const char *s, Request &req) const { "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; - if (methods.find(req.method) == methods.end()) { return false; } + if (methods.find(req.method) == methods.end()) { + output_error_log(Error::InvalidHTTPMethod, &req); + return false; + } - if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { return false; } + if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { + output_error_log(Error::InvalidHTTPVersion, &req); + return false; + } { // Skip URL fragment @@ -7607,7 +7670,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection, } // Log - if (logger_) { logger_(req, res); } + output_log(req, res); return ret; } @@ -7683,6 +7746,7 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) { // Multipart FormData [&](const FormData &file) { if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { + output_error_log(Error::TooManyFormDataFiles, &req); return false; } @@ -7712,6 +7776,7 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) { if (!content_type.find("application/x-www-form-urlencoded")) { if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414? + output_error_log(Error::ExceedMaxPayloadSize, &req); return false; } detail::parse_query_text(req.body, req.params); @@ -7740,6 +7805,7 @@ inline bool Server::read_content_core( std::string boundary; if (!detail::parse_multipart_boundary(content_type, boundary)) { res.status = StatusCode::BadRequest_400; + output_error_log(Error::MultipartParsing, &req); return false; } @@ -7765,6 +7831,7 @@ inline bool Server::read_content_core( if (req.is_multipart_form_data()) { if (!multipart_form_data_parser.is_valid()) { res.status = StatusCode::BadRequest_400; + output_error_log(Error::MultipartParsing, &req); return false; } } @@ -7794,7 +7861,10 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } auto mm = std::make_shared(path.c_str()); - if (!mm->is_open()) { return false; } + if (!mm->is_open()) { + output_error_log(Error::OpenFile, &req); + return false; + } res.set_content_provider( mm->size(), @@ -7810,6 +7880,8 @@ inline bool Server::handle_file_request(const Request &req, Response &res) { } return true; + } else { + output_error_log(Error::OpenFile, &req); } } } @@ -7824,11 +7896,15 @@ Server::create_server_socket(const std::string &host, int port, return detail::create_socket( host, std::string(), port, address_family_, socket_flags, tcp_nodelay_, ipv6_v6only_, std::move(socket_options), - [](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool { + [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool { if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { + output_error_log(Error::BindIPAddress, nullptr); + return false; + } + if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { + output_error_log(Error::Listen, nullptr); return false; } - if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { return false; } return true; }); } @@ -7847,6 +7923,7 @@ inline int Server::bind_internal(const std::string &host, int port, socklen_t addr_len = sizeof(addr); if (getsockname(svr_sock_, reinterpret_cast(&addr), &addr_len) == -1) { + output_error_log(Error::GetSockName, nullptr); return -1; } if (addr.ss_family == AF_INET) { @@ -7854,6 +7931,7 @@ inline int Server::bind_internal(const std::string &host, int port, } else if (addr.ss_family == AF_INET6) { return ntohs(reinterpret_cast(&addr)->sin6_port); } else { + output_error_log(Error::UnsupportedAddressFamily, nullptr); return -1; } } else { @@ -7907,6 +7985,7 @@ inline bool Server::listen_internal() { if (svr_sock_ != INVALID_SOCKET) { detail::close_socket(svr_sock_); ret = false; + output_error_log(Error::Connection, nullptr); } else { ; // The server socket was closed by user. } @@ -7920,6 +7999,7 @@ inline bool Server::listen_internal() { if (!task_queue->enqueue( [this, sock]() { process_and_close_socket(sock); })) { + output_error_log(Error::ResourceExhaustion, nullptr); detail::shutdown_socket(sock); detail::close_socket(sock); } @@ -7949,13 +8029,17 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) { { ContentReader reader( [&](ContentReceiver receiver) { - return read_content_with_content_receiver( + auto result = read_content_with_content_receiver( strm, req, res, std::move(receiver), nullptr, nullptr); + if (!result) { output_error_log(Error::Read, &req); } + return result; }, [&](FormDataHeader header, ContentReceiver receiver) { - return read_content_with_content_receiver(strm, req, res, nullptr, - std::move(header), - std::move(receiver)); + auto result = read_content_with_content_receiver( + strm, req, res, nullptr, std::move(header), + std::move(receiver)); + if (!result) { output_error_log(Error::Read, &req); } + return result; }); if (req.method == "POST") { @@ -7986,7 +8070,10 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) { } // Read content into `req.body` - if (!read_content(strm, req, res)) { return false; } + if (!read_content(strm, req, res)) { + output_error_log(Error::Read, &req); + return false; + } } // Regular handler @@ -8100,7 +8187,7 @@ inline void Server::apply_ranges(const Request &req, Response &res, } if (type != detail::EncodingType::None) { - if (pre_compression_logger_) { pre_compression_logger_(req, res); } + output_pre_compression_log(req, res); std::unique_ptr compressor; std::string content_encoding; @@ -8184,14 +8271,22 @@ Server::process_request(Stream &strm, const std::string &remote_addr, Headers dummy; detail::read_headers(strm, dummy); res.status = StatusCode::InternalServerError_500; + output_error_log(Error::ExceedMaxSocketDescriptorCount, &req); return write_response(strm, close_connection, req, res); } #endif // Request line and headers - if (!parse_request_line(line_reader.ptr(), req) || - !detail::read_headers(strm, req.headers)) { + if (!parse_request_line(line_reader.ptr(), req)) { res.status = StatusCode::BadRequest_400; + output_error_log(Error::InvalidRequestLine, &req); + return write_response(strm, close_connection, req, res); + } + + // Request headers + if (!detail::read_headers(strm, req.headers)) { + res.status = StatusCode::BadRequest_400; + output_error_log(Error::InvalidHeaders, &req); return write_response(strm, close_connection, req, res); } @@ -8200,6 +8295,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, Headers dummy; detail::read_headers(strm, dummy); res.status = StatusCode::UriTooLong_414; + output_error_log(Error::ExceedUriMaxLength, &req); return write_response(strm, close_connection, req, res); } @@ -8226,6 +8322,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, const auto &accept_header = req.get_header_value("Accept"); if (!detail::parse_accept_header(accept_header, req.accept_content_types)) { res.status = StatusCode::BadRequest_400; + output_error_log(Error::HTTPParsing, &req); return write_response(strm, close_connection, req, res); } } @@ -8234,6 +8331,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, const auto &range_header_value = req.get_header_value("Range"); if (!detail::parse_range_header(range_header_value, req.ranges)) { res.status = StatusCode::RangeNotSatisfiable_416; + output_error_log(Error::InvalidRangeHeader, &req); return write_response(strm, close_connection, req, res); } } @@ -8314,6 +8412,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::NotFound_404; + output_error_log(Error::OpenFile, &req); return write_response(strm, close_connection, req, res); } @@ -8373,6 +8472,29 @@ inline bool Server::process_and_close_socket(socket_t sock) { return ret; } +inline void Server::output_log(const Request &req, const Response &res) const { + if (logger_) { + std::lock_guard guard(logger_mutex_); + logger_(req, res); + } +} + +inline void Server::output_pre_compression_log(const Request &req, + const Response &res) const { + if (pre_compression_logger_) { + std::lock_guard guard(logger_mutex_); + pre_compression_logger_(req, res); + } +} + +inline void Server::output_error_log(const Error &err, + const Request *req) const { + if (error_logger_) { + std::lock_guard guard(logger_mutex_); + error_logger_(err, req); + } +} + // HTTP client implementation inline ClientImpl::ClientImpl(const std::string &host) : ClientImpl(host, 80, std::string(), std::string()) {} @@ -8451,6 +8573,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) { server_certificate_verifier_ = rhs.server_certificate_verifier_; #endif logger_ = rhs.logger_; + error_logger_ = rhs.error_logger_; } inline socket_t ClientImpl::create_client_socket(Error &error) const { @@ -8593,7 +8716,10 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { } if (!is_alive) { - if (!create_and_connect_socket(socket_, error)) { return false; } + if (!create_and_connect_socket(socket_, error)) { + output_error_log(error, &req); + return false; + } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // TODO: refactoring @@ -8603,11 +8729,15 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { auto success = false; if (!scli.connect_with_proxy(socket_, req.start_time_, res, success, error)) { + if (!success) { output_error_log(error, &req); } return success; } } - if (!scli.initialize_ssl(socket_, error)) { return false; } + if (!scli.initialize_ssl(socket_, error)) { + output_error_log(error, &req); + return false; + } } #endif } @@ -8653,7 +8783,10 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { }); if (!ret) { - if (error == Error::Success) { error = Error::Unknown; } + if (error == Error::Success) { + error = Error::Unknown; + output_error_log(error, &req); + } } return ret; @@ -8681,6 +8814,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req, Error &error) { if (req.path.empty()) { error = Error::Connection; + output_error_log(error, &req); return false; } @@ -8756,6 +8890,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req, inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { if (req.redirect_count_ == 0) { error = Error::ExceedRedirectCount; + output_error_log(error, &req); return false; } @@ -8859,6 +8994,7 @@ inline bool ClientImpl::create_redirect_client( #else // SSL not supported - set appropriate error error = Error::SSLConnection; + output_error_log(error, &req); return false; #endif } else { @@ -8931,6 +9067,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { // Copy logging and headers if (logger_) { client.set_logger(logger_); } + if (error_logger_) { client.set_error_logger(error_logger_); } // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers // Each new client should generate its own headers based on its target host @@ -9104,6 +9241,7 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req, auto &data = bstrm.get_buffer(); if (!detail::write_data(strm, data.data(), data.size())) { error = Error::Write; + output_error_log(error, &req); return false; } } @@ -9122,18 +9260,21 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req, size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written); if (!detail::write_data(strm, data + written, to_write)) { error = Error::Write; + output_error_log(error, &req); return false; } written += to_write; if (!req.upload_progress(written, body_size)) { error = Error::Canceled; + output_error_log(error, &req); return false; } } } else { if (!detail::write_data(strm, req.body.data(), req.body.size())) { error = Error::Write; + output_error_log(error, &req); return false; } } @@ -9185,6 +9326,7 @@ inline std::unique_ptr ClientImpl::send_with_content_provider( while (ok && offset < content_length) { if (!content_provider(offset, content_length - offset, data_sink)) { error = Error::Canceled; + output_error_log(error, &req); return nullptr; } } @@ -9195,6 +9337,7 @@ inline std::unique_ptr ClientImpl::send_with_content_provider( return true; })) { error = Error::Compression; + output_error_log(error, &req); return nullptr; } } @@ -9254,6 +9397,22 @@ ClientImpl::adjust_host_string(const std::string &host) const { return host; } +inline void ClientImpl::output_log(const Request &req, + const Response &res) const { + if (logger_) { + std::lock_guard guard(logger_mutex_); + logger_(req, res); + } +} + +inline void ClientImpl::output_error_log(const Error &err, + const Request *req) const { + if (error_logger_) { + std::lock_guard guard(logger_mutex_); + error_logger_(err, req); + } +} + inline bool ClientImpl::process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { @@ -9266,6 +9425,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, if (!is_proxy_enabled) { if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { error = Error::SSLPeerCouldBeClosed_; + output_error_log(error, &req); return false; } } @@ -9276,6 +9436,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, if (!read_response_line(strm, req, res) || !detail::read_headers(strm, res.headers)) { error = Error::Read; + output_error_log(error, &req); return false; } @@ -9289,6 +9450,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, if (req.response_handler && !redirect) { if (!req.response_handler(res)) { error = Error::Canceled; + output_error_log(error, &req); return false; } } @@ -9299,7 +9461,10 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, [&](const char *buf, size_t n, size_t off, size_t len) { if (redirect) { return true; } auto ret = req.content_receiver(buf, n, off, len); - if (!ret) { error = Error::Canceled; } + if (!ret) { + error = Error::Canceled; + output_error_log(error, &req); + } return ret; }) : static_cast( @@ -9313,7 +9478,10 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, auto progress = [&](size_t current, size_t total) { if (!req.download_progress || redirect) { return true; } auto ret = req.download_progress(current, total); - if (!ret) { error = Error::Canceled; } + if (!ret) { + error = Error::Canceled; + output_error_log(error, &req); + } return ret; }; @@ -9322,6 +9490,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, auto len = res.get_header_value_u64("Content-Length"); if (len > res.body.max_size()) { error = Error::Read; + output_error_log(error, &req); return false; } res.body.reserve(static_cast(len)); @@ -9334,13 +9503,14 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, dummy_status, std::move(progress), std::move(out), decompress_)) { if (error != Error::Canceled) { error = Error::Read; } + output_error_log(error, &req); return false; } } } // Log - if (logger_) { logger_(req, res); } + output_log(req, res); return true; } @@ -10244,6 +10414,10 @@ inline void ClientImpl::set_logger(Logger logger) { logger_ = std::move(logger); } +inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { + error_logger_ = std::move(error_logger); +} + /* * SSL Implementation */ @@ -10756,6 +10930,7 @@ inline bool SSLClient::connect_with_proxy( // Create a new socket for the authenticated CONNECT request if (!create_and_connect_socket(socket, error)) { success = false; + output_error_log(error, nullptr); return false; } @@ -10793,6 +10968,7 @@ inline bool SSLClient::connect_with_proxy( // as the response of the request if (proxy_res.status != StatusCode::OK_200) { error = Error::ProxyConnection; + output_error_log(error, nullptr); res = std::move(proxy_res); // Thread-safe to close everything because we are assuming there are // no requests in flight @@ -10845,6 +11021,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { if (server_certificate_verification_) { if (!load_certs()) { error = Error::SSLLoadingCerts; + output_error_log(error, nullptr); return false; } SSL_set_verify(ssl2, SSL_VERIFY_NONE, nullptr); @@ -10854,6 +11031,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { socket.sock, ssl2, SSL_connect, connection_timeout_sec_, connection_timeout_usec_, &last_ssl_error_)) { error = Error::SSLConnection; + output_error_log(error, nullptr); return false; } @@ -10867,6 +11045,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { if (verification_status == SSLVerifierResponse::CertificateRejected) { last_openssl_error_ = ERR_get_error(); error = Error::SSLServerVerification; + output_error_log(error, nullptr); return false; } @@ -10876,6 +11055,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { if (verify_result_ != X509_V_OK) { last_openssl_error_ = static_cast(verify_result_); error = Error::SSLServerVerification; + output_error_log(error, nullptr); return false; } @@ -10885,6 +11065,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { if (server_cert == nullptr) { last_openssl_error_ = ERR_get_error(); error = Error::SSLServerVerification; + output_error_log(error, nullptr); return false; } @@ -10892,6 +11073,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { if (!verify_host(server_cert)) { last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH; error = Error::SSLServerHostnameVerification; + output_error_log(error, nullptr); return false; } } @@ -11658,6 +11840,10 @@ inline void Client::set_logger(Logger logger) { cli_->set_logger(std::move(logger)); } +inline void Client::set_error_logger(ErrorLogger error_logger) { + cli_->set_error_logger(std::move(error_logger)); +} + #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void Client::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { From cdaed14925a4eb843cd30aa7e13655633f51ef44 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 6 Aug 2025 17:41:03 -0400 Subject: [PATCH 04/29] Update README --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8553114ff3..ff07694c22 100644 --- a/README.md +++ b/README.md @@ -269,23 +269,47 @@ svr.set_file_request_handler([](const Request &req, Response &res) { ### Logging +cpp-httplib provides separate logging capabilities for access logs and error logs, similar to web servers like Nginx and Apache. + +#### Access Logging + +Access loggers capture successful HTTP requests and responses: + ```cpp -svr.set_logger([](const auto& req, const auto& res) { - your_logger(req, res); +svr.set_logger([](const httplib::Request& req, const httplib::Response& res) { + std::cout << req.method << " " << req.path << " -> " << res.status << std::endl; }); ``` -You can also set a pre-compression logger to capture request/response data before compression is applied. This is useful for debugging and monitoring purposes when you need to see the original, uncompressed response content: +#### Pre-compression Logging + +You can also set a pre-compression logger to capture request/response data before compression is applied: ```cpp -svr.set_pre_compression_logger([](const auto& req, const auto& res) { +svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Response& res) { // Log before compression - res.body contains uncompressed content // Content-Encoding header is not yet set your_pre_compression_logger(req, res); }); ``` -The pre-compression logger is only called when compression would be applied. For responses without compression, only the regular logger is called. +The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called. + +#### Error Logging + +Error loggers capture failed requests and connection issues. Unlike access loggers, error loggers only receive the Error and Request information, as errors typically occur before a meaningful Response can be generated. + +```cpp +svr.set_error_logger([](const httplib::Error& err, const httplib::Request* req) { + std::cerr << httplib::to_string(err) << " while processing request"; + if (req) { + std::cerr << ", client: " << req->get_header_value("X-Forwarded-For") + << ", request: '" << req->method << " " << req->path << " " << req->version << "'" + << ", host: " << req->get_header_value("Host"); + } + std::cerr << std::endl; +}); +``` ### Error handler @@ -718,6 +742,51 @@ enum Error { }; ``` +### Client Logging + +#### Access Logging + +```cpp +cli.set_logger([](const httplib::Request& req, const httplib::Response& res) { + auto duration = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time).count(); + std::cout << "✓ " << req.method << " " << req.path + << " -> " << res.status << " (" << res.body.size() << " bytes, " + << duration << "ms)" << std::endl; +}); +``` + +#### Error Logging + +```cpp +cli.set_error_logger([](const httplib::Error& err, const httplib::Request* req) { + std::cerr << "✗ "; + if (req) { + std::cerr << req->method << " " << req->path << " "; + } + std::cerr << "failed: " << httplib::to_string(err); + + // Add specific guidance based on error type + switch (err) { + case httplib::Error::Connection: + std::cerr << " (verify server is running and reachable)"; + break; + case httplib::Error::SSLConnection: + std::cerr << " (check SSL certificate and TLS configuration)"; + break; + case httplib::Error::ConnectionTimeout: + std::cerr << " (increase timeout or check network latency)"; + break; + case httplib::Error::Read: + std::cerr << " (server may have closed connection prematurely)"; + break; + default: + break; + } + std::cerr << std::endl; +}); +``` + ### GET with HTTP headers ```c++ From 70cca55cafb4edb8f57dc0c9ee3e5ab227055a40 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 6 Aug 2025 17:54:08 -0400 Subject: [PATCH 05/29] Workaround for chocolatey issue with the latest OpenSSL --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 135bf7b500..45dc91c48e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -148,7 +148,7 @@ jobs: run: vcpkg install gtest curl zlib brotli zstd - name: Install OpenSSL if: ${{ matrix.config.with_ssl }} - run: choco install openssl + run: choco install openssl --version 3.5.2 # workaround for chocolatey issue with the latest OpenSSL - name: Configure CMake ${{ matrix.config.name }} run: > cmake -B build -S . From fbee136dca542e1824cffcdf50ddc04738113e51 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 6 Aug 2025 23:05:42 -0400 Subject: [PATCH 06/29] Fix #2193. Allow _WIN32 --- httplib.h | 88 +++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/httplib.h b/httplib.h index c6976917e0..98854aff85 100644 --- a/httplib.h +++ b/httplib.h @@ -16,7 +16,7 @@ */ #if defined(_WIN32) && !defined(_WIN64) -#error \ +#warning \ "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler." #elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8 #warning \ @@ -98,7 +98,7 @@ #endif #ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND -#ifdef _WIN64 +#ifdef _WIN32 #define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000 #else #define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0 @@ -184,7 +184,7 @@ * Headers */ -#ifdef _WIN64 +#ifdef _WIN32 #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif //_CRT_SECURE_NO_WARNINGS @@ -235,7 +235,7 @@ using nfds_t = unsigned long; using socket_t = SOCKET; using socklen_t = int; -#else // not _WIN64 +#else // not _WIN32 #include #if !defined(_AIX) && !defined(__MVS__) @@ -266,7 +266,7 @@ using socket_t = int; #ifndef INVALID_SOCKET #define INVALID_SOCKET (-1) #endif -#endif //_WIN64 +#endif //_WIN32 #if defined(__APPLE__) #include @@ -311,7 +311,7 @@ using socket_t = int; // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN #ifdef CPPHTTPLIB_OPENSSL_SUPPORT -#ifdef _WIN64 +#ifdef _WIN32 #include // these are defined in wincrypt.h and it breaks compilation if BoringSSL is @@ -324,7 +324,7 @@ using socket_t = int; #ifdef _MSC_VER #pragma comment(lib, "crypt32.lib") #endif -#endif // _WIN64 +#endif // _WIN32 #if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) #if TARGET_OS_OSX @@ -337,7 +337,7 @@ using socket_t = int; #include #include -#if defined(_WIN64) && defined(OPENSSL_USE_APPLINK) +#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) #include #endif @@ -2086,7 +2086,7 @@ namespace detail { inline bool set_socket_opt_impl(socket_t sock, int level, int optname, const void *optval, socklen_t optlen) { return setsockopt(sock, level, optname, -#ifdef _WIN64 +#ifdef _WIN32 reinterpret_cast(optval), #else optval, @@ -2100,7 +2100,7 @@ inline bool set_socket_opt(socket_t sock, int level, int optname, int optval) { inline bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, time_t usec) { -#ifdef _WIN64 +#ifdef _WIN32 auto timeout = static_cast(sec * 1000 + usec / 1000); #else timeval timeout; @@ -2378,7 +2378,7 @@ make_basic_authentication_header(const std::string &username, namespace detail { -#if defined(_WIN64) +#if defined(_WIN32) inline std::wstring u8string_to_wstring(const char *s) { std::wstring ws; auto len = static_cast(strlen(s)); @@ -2400,7 +2400,7 @@ struct FileStat { bool is_dir() const; private: -#if defined(_WIN64) +#if defined(_WIN32) struct _stat st_; #else struct stat st_; @@ -2641,7 +2641,7 @@ class mmap { const char *data() const; private: -#if defined(_WIN64) +#if defined(_WIN32) HANDLE hFile_ = NULL; HANDLE hMapping_ = NULL; #else @@ -2862,7 +2862,7 @@ inline bool is_valid_path(const std::string &path) { } inline FileStat::FileStat(const std::string &path) { -#if defined(_WIN64) +#if defined(_WIN32) auto wpath = u8string_to_wstring(path.c_str()); ret_ = _wstat(wpath.c_str(), &st_); #else @@ -3074,7 +3074,7 @@ inline mmap::~mmap() { close(); } inline bool mmap::open(const char *path) { close(); -#if defined(_WIN64) +#if defined(_WIN32) auto wpath = u8string_to_wstring(path); if (wpath.empty()) { return false; } @@ -3151,7 +3151,7 @@ inline const char *mmap::data() const { } inline void mmap::close() { -#if defined(_WIN64) +#if defined(_WIN32) if (addr_) { ::UnmapViewOfFile(addr_); addr_ = nullptr; @@ -3182,7 +3182,7 @@ inline void mmap::close() { size_ = 0; } inline int close_socket(socket_t sock) { -#ifdef _WIN64 +#ifdef _WIN32 return closesocket(sock); #else return close(sock); @@ -3205,7 +3205,7 @@ template inline ssize_t handle_EINTR(T fn) { inline ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return recv(sock, -#ifdef _WIN64 +#ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, @@ -3218,7 +3218,7 @@ inline ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return send(sock, -#ifdef _WIN64 +#ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, @@ -3228,7 +3228,7 @@ inline ssize_t send_socket(socket_t sock, const void *ptr, size_t size, } inline int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) { -#ifdef _WIN64 +#ifdef _WIN32 return ::WSAPoll(fds, nfds, timeout); #else return ::poll(fds, nfds, timeout); @@ -3487,7 +3487,7 @@ inline bool process_client_socket( } inline int shutdown_socket(socket_t sock) { -#ifdef _WIN64 +#ifdef _WIN32 return shutdown(sock, SD_BOTH); #else return shutdown(sock, SHUT_RDWR); @@ -3522,7 +3522,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, return getaddrinfo(node, service, hints, res); } -#ifdef _WIN64 +#ifdef _WIN32 // Windows-specific implementation using GetAddrInfoEx with overlapped I/O OVERLAPPED overlapped = {0}; HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); @@ -3855,7 +3855,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, hints.ai_flags = socket_flags; } -#if !defined(_WIN64) || defined(CPPHTTPLIB_HAVE_AFUNIX_H) +#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H) if (hints.ai_family == AF_UNIX) { const auto addrlen = host.length(); if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; } @@ -3879,14 +3879,14 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, sizeof(addr) - sizeof(addr.sun_path) + addrlen); #ifndef SOCK_CLOEXEC -#ifndef _WIN64 +#ifndef _WIN32 fcntl(sock, F_SETFD, FD_CLOEXEC); #endif #endif if (socket_options) { socket_options(sock); } -#ifdef _WIN64 +#ifdef _WIN32 // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so // remove the option. detail::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0); @@ -3915,7 +3915,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, for (auto rp = result; rp; rp = rp->ai_next) { // Create a socket -#ifdef _WIN64 +#ifdef _WIN32 auto sock = WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); @@ -3948,7 +3948,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, #endif if (sock == INVALID_SOCKET) { continue; } -#if !defined _WIN64 && !defined SOCK_CLOEXEC +#if !defined _WIN32 && !defined SOCK_CLOEXEC if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { close_socket(sock); continue; @@ -3976,7 +3976,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port, } inline void set_nonblocking(socket_t sock, bool nonblocking) { -#ifdef _WIN64 +#ifdef _WIN32 auto flags = nonblocking ? 1UL : 0UL; ioctlsocket(sock, FIONBIO, &flags); #else @@ -3987,7 +3987,7 @@ inline void set_nonblocking(socket_t sock, bool nonblocking) { } inline bool is_connection_error() { -#ifdef _WIN64 +#ifdef _WIN32 return WSAGetLastError() != WSAEWOULDBLOCK; #else return errno != EINPROGRESS; @@ -4021,7 +4021,7 @@ inline bool bind_ip_address(socket_t sock, const std::string &host) { return ret; } -#if !defined _WIN64 && !defined ANDROID && !defined _AIX && !defined __MVS__ +#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ #define USE_IF2IP #endif @@ -4160,7 +4160,7 @@ inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { if (!getpeername(sock, reinterpret_cast(&addr), &addr_len)) { -#ifndef _WIN64 +#ifndef _WIN32 if (addr.ss_family == AF_UNIX) { #if defined(__linux__) struct ucred ucred; @@ -6226,7 +6226,7 @@ inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) { SSL_get_error(ssl, 0) == SSL_ERROR_ZERO_RETURN; } -#ifdef _WIN64 +#ifdef _WIN32 // NOTE: This code came up with the following stackoverflow post: // https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store inline bool load_system_certs_on_windows(X509_STORE *store) { @@ -6342,10 +6342,10 @@ inline bool load_system_certs_on_macos(X509_STORE *store) { return result; } -#endif // _WIN64 +#endif // _WIN32 #endif // CPPHTTPLIB_OPENSSL_SUPPORT -#ifdef _WIN64 +#ifdef _WIN32 class WSInit { public: WSInit() { @@ -7041,7 +7041,7 @@ inline bool SocketStream::wait_writable() const { } inline ssize_t SocketStream::read(char *ptr, size_t size) { -#ifdef _WIN64 +#ifdef _WIN32 size = (std::min)(size, static_cast((std::numeric_limits::max)())); #else @@ -7089,7 +7089,7 @@ inline ssize_t SocketStream::read(char *ptr, size_t size) { inline ssize_t SocketStream::write(const char *ptr, size_t size) { if (!wait_writable()) { return -1; } -#if defined(_WIN64) && !defined(_WIN64) +#if defined(_WIN32) && !defined(_WIN64) size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif @@ -7252,7 +7252,7 @@ inline bool RegexMatcher::match(Request &request) const { inline Server::Server() : new_task_queue( [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) { -#ifndef _WIN64 +#ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif } @@ -7950,7 +7950,7 @@ inline bool Server::listen_internal() { std::unique_ptr task_queue(new_task_queue()); while (svr_sock_ != INVALID_SOCKET) { -#ifndef _WIN64 +#ifndef _WIN32 if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { #endif auto val = detail::select_read(svr_sock_, idle_interval_sec_, @@ -7959,11 +7959,11 @@ inline bool Server::listen_internal() { task_queue->on_idle(); continue; } -#ifndef _WIN64 +#ifndef _WIN32 } #endif -#if defined _WIN64 +#if defined _WIN32 // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT, // OVERLAPPED socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0); @@ -10571,7 +10571,7 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { if (ret < 0) { auto err = SSL_get_error(ssl_, ret); auto n = 1000; -#ifdef _WIN64 +#ifdef _WIN32 while (--n >= 0 && (err == SSL_ERROR_WANT_READ || (err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT))) { @@ -10606,7 +10606,7 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { if (ret < 0) { auto err = SSL_get_error(ssl_, ret); auto n = 1000; -#ifdef _WIN64 +#ifdef _WIN32 while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE || (err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT))) { @@ -11000,13 +11000,13 @@ inline bool SSLClient::load_certs() { } } else { auto loaded = false; -#ifdef _WIN64 +#ifdef _WIN32 loaded = detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); #elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \ defined(TARGET_OS_OSX) loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); -#endif // _WIN64 +#endif // _WIN32 if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } } }); From b1c1fa2dc6355b3d8f5a459a01681d620cc866bc Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 7 Aug 2025 00:09:09 -0400 Subject: [PATCH 07/29] Code cleanup --- docker/main.cc | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docker/main.cc b/docker/main.cc index 784c803b02..ae59b4e498 100644 --- a/docker/main.cc +++ b/docker/main.cc @@ -18,8 +18,7 @@ using namespace httplib; -auto SERVER_NAME = - std::format("cpp-httplib-nginxish-server/{}", CPPHTTPLIB_VERSION); +auto SERVER_NAME = std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION); Server svr; @@ -31,7 +30,7 @@ void signal_handler(int signal) { } } -std::string get_nginx_time_format() { +std::string get_time_format() { auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); @@ -40,7 +39,7 @@ std::string get_nginx_time_format() { return ss.str(); } -std::string get_nginx_error_time_format() { +std::string get_error_time_format() { auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); @@ -77,7 +76,7 @@ void nginx_access_logger(const Request &req, const Response &res) { std::string remote_addr = get_client_ip(req); std::string remote_user = "-"; // cpp-httplib doesn't have built-in auth user tracking - std::string time_local = get_nginx_time_format(); + std::string time_local = get_time_format(); std::string request = std::format("{} {} {}", req.method, req.path, req.version); int status = res.status; @@ -98,7 +97,7 @@ void nginx_access_logger(const Request &req, const Response &res) { // YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request", // host: "host" void nginx_error_logger(const Error &err, const Request *req) { - std::string time_local = get_nginx_error_time_format(); + std::string time_local = get_error_time_format(); std::string level = "error"; if (req) { From 7012e765e1315afa43b37d01fd43c7382a1bd1f8 Mon Sep 17 00:00:00 2001 From: Thomas Beutlich <115483027+thbeu@users.noreply.github.com> Date: Thu, 7 Aug 2025 23:14:19 +0200 Subject: [PATCH 08/29] CMake: Check pointer size at configure time (#2197) * Check pointer size at configure time * Relax check to warning --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf036b4066..d39958a05c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,9 +117,12 @@ if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() -if( CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0") +if(CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0") message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.") endif() +if(CMAKE_SIZEOF_VOID_P LESS 8) + message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.") +endif() # Set some variables that are used in-tree and while building based on our options set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE}) From a2bb6f6c1ea56f351e43bbf740e30503453679cd Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 7 Aug 2025 20:57:37 -0400 Subject: [PATCH 09/29] Update docker/main.cc --- docker/main.cc | 169 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 129 insertions(+), 40 deletions(-) diff --git a/docker/main.cc b/docker/main.cc index ae59b4e498..8ffbf2ca15 100644 --- a/docker/main.cc +++ b/docker/main.cc @@ -18,14 +18,14 @@ using namespace httplib; -auto SERVER_NAME = std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION); +const auto SERVER_NAME = + std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION); Server svr; void signal_handler(int signal) { if (signal == SIGINT || signal == SIGTERM) { - std::cout << std::format("\nReceived signal, shutting down gracefully...") - << std::endl; + std::cout << "\nReceived signal, shutting down gracefully...\n"; svr.stop(); } } @@ -73,17 +73,16 @@ std::string get_client_ip(const Request &req) { // $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent // "$http_referer" "$http_user_agent" void nginx_access_logger(const Request &req, const Response &res) { - std::string remote_addr = get_client_ip(req); + auto remote_addr = get_client_ip(req); std::string remote_user = "-"; // cpp-httplib doesn't have built-in auth user tracking - std::string time_local = get_time_format(); - std::string request = - std::format("{} {} {}", req.method, req.path, req.version); - int status = res.status; - size_t body_bytes_sent = res.body.size(); - std::string http_referer = req.get_header_value("Referer"); + auto time_local = get_time_format(); + auto request = std::format("{} {} {}", req.method, req.path, req.version); + auto status = res.status; + auto body_bytes_sent = res.body.size(); + auto http_referer = req.get_header_value("Referer"); if (http_referer.empty()) http_referer = "-"; - std::string http_user_agent = req.get_header_value("User-Agent"); + auto http_user_agent = req.get_header_value("User-Agent"); if (http_user_agent.empty()) http_user_agent = "-"; std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"", @@ -97,14 +96,14 @@ void nginx_access_logger(const Request &req, const Response &res) { // YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request", // host: "host" void nginx_error_logger(const Error &err, const Request *req) { - std::string time_local = get_error_time_format(); + auto time_local = get_error_time_format(); std::string level = "error"; if (req) { - std::string client_ip = get_client_ip(*req); - std::string request = + auto client_ip = get_client_ip(*req); + auto request = std::format("{} {} {}", req->method, req->path, req->version); - std::string host = req->get_header_value("Host"); + auto host = req->get_header_value("Host"); if (host.empty()) host = "-"; std::cerr << std::format("{} [{}] {}, client: {}, request: " @@ -120,38 +119,113 @@ void nginx_error_logger(const Error &err, const Request *req) { } void print_usage(const char *program_name) { - std::cout << std::format("Usage: {} " - "", - program_name) + std::cout << "Usage: " << program_name << " [OPTIONS]" << std::endl; + std::cout << std::endl; + std::cout << "Options:" << std::endl; + std::cout << " --host Server hostname (default: localhost)" << std::endl; - - std::cout << std::format("Example: {} localhost 8080 /var/www/html .", - program_name) + std::cout << " --port Server port (default: 8080)" + << std::endl; + std::cout << " --mount Mount point and document root" + << std::endl; + std::cout << " Format: mount_point:document_root" + << std::endl; + std::cout << " (default: /:./html)" << std::endl; + std::cout << " --version Show version information" << std::endl; + std::cout << " --help Show this help message" << std::endl; + std::cout << std::endl; + std::cout << "Examples:" << std::endl; + std::cout << " " << program_name + << " --host localhost --port 8080 --mount /:./html" << std::endl; + std::cout << " " << program_name + << " --host 0.0.0.0 --port 3000 --mount /api:./api" << std::endl; } -int main(int argc, char *argv[]) { - if (argc != 5) { - print_usage(argv[0]); - return 1; +struct ServerConfig { + std::string hostname = "localhost"; + int port = 8080; + std::string mount_point = "/"; + std::string document_root = "./html"; +}; + +enum class ParseResult { SUCCESS, HELP_REQUESTED, VERSION_REQUESTED, ERROR }; + +ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) { + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + print_usage(argv[0]); + return ParseResult::HELP_REQUESTED; + } else if (strcmp(argv[i], "--host") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --host requires a hostname argument" << std::endl; + print_usage(argv[0]); + return ParseResult::ERROR; + } + config.hostname = argv[++i]; + } else if (strcmp(argv[i], "--port") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --port requires a port number argument" + << std::endl; + print_usage(argv[0]); + return ParseResult::ERROR; + } + config.port = std::atoi(argv[++i]); + if (config.port <= 0 || config.port > 65535) { + std::cerr << "Error: Invalid port number. Must be between 1 and 65535" + << std::endl; + return ParseResult::ERROR; + } + } else if (strcmp(argv[i], "--mount") == 0) { + if (i + 1 >= argc) { + std::cerr + << "Error: --mount requires mount_point:document_root argument" + << std::endl; + print_usage(argv[0]); + return ParseResult::ERROR; + } + std::string mount_arg = argv[++i]; + auto colon_pos = mount_arg.find(':'); + if (colon_pos == std::string::npos) { + std::cerr << "Error: --mount argument must be in format " + "mount_point:document_root" + << std::endl; + print_usage(argv[0]); + return ParseResult::ERROR; + } + config.mount_point = mount_arg.substr(0, colon_pos); + config.document_root = mount_arg.substr(colon_pos + 1); + + if (config.mount_point.empty() || config.document_root.empty()) { + std::cerr + << "Error: Both mount_point and document_root must be non-empty" + << std::endl; + return ParseResult::ERROR; + } + } else if (strcmp(argv[i], "--version") == 0) { + std::cout << CPPHTTPLIB_VERSION << std::endl; + return ParseResult::VERSION_REQUESTED; + } else { + std::cerr << "Error: Unknown option '" << argv[i] << "'" << std::endl; + print_usage(argv[0]); + return ParseResult::ERROR; + } } + return ParseResult::SUCCESS; +} - std::string hostname = argv[1]; - auto port = std::atoi(argv[2]); - std::string mount_point = argv[3]; - std::string document_root = argv[4]; - +bool setup_server(Server &svr, const ServerConfig &config) { svr.set_logger(nginx_access_logger); svr.set_error_logger(nginx_error_logger); - auto ret = svr.set_mount_point(mount_point, document_root); + auto ret = svr.set_mount_point(config.mount_point, config.document_root); if (!ret) { std::cerr << std::format( "Error: Cannot mount '{}' to '{}'. Directory may not exist.", - mount_point, document_root) + config.mount_point, config.document_root) << std::endl; - return 1; + return false; } svr.set_file_extension_and_mimetype_mapping("html", "text/html"); @@ -191,16 +265,31 @@ int main(int argc, char *argv[]) { signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); - std::cout << std::format("Serving HTTP on {}:{}", hostname, port) - << std::endl; - std::cout << std::format("Mount point: {} -> {}", mount_point, document_root) - << std::endl; - std::cout << std::format("Press Ctrl+C to shutdown gracefully...") + return true; +} + +int main(int argc, char *argv[]) { + ServerConfig config; + + auto result = parse_command_line(argc, argv, config); + switch (result) { + case ParseResult::HELP_REQUESTED: + case ParseResult::VERSION_REQUESTED: return 0; + case ParseResult::ERROR: return 1; + case ParseResult::SUCCESS: break; + } + + if (!setup_server(svr, config)) { return 1; } + + std::cout << "Serving HTTP on " << config.hostname << ":" << config.port << std::endl; + std::cout << "Mount point: " << config.mount_point << " -> " + << config.document_root << std::endl; + std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl; - ret = svr.listen(hostname, port); + auto ret = svr.listen(config.hostname, config.port); - std::cout << std::format("Server has been shut down.") << std::endl; + std::cout << "Server has been shut down." << std::endl; return ret ? 0 : 1; } From 3f44c80fd345e859d6f61b2dda566aa3329ce35e Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 7 Aug 2025 20:58:39 -0400 Subject: [PATCH 10/29] Release v0.25.0 --- httplib.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httplib.h b/httplib.h index 98854aff85..52b1b829f7 100644 --- a/httplib.h +++ b/httplib.h @@ -8,8 +8,8 @@ #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.24.0" -#define CPPHTTPLIB_VERSION_NUM "0x001800" +#define CPPHTTPLIB_VERSION "0.25.0" +#define CPPHTTPLIB_VERSION_NUM "0x001900" /* * Platform compatibility check From dffce89514c248f9001c9f772fd15d64c8404a58 Mon Sep 17 00:00:00 2001 From: Thomas Beutlich <115483027+thbeu@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:06:09 +0200 Subject: [PATCH 11/29] #2201 Fix 32-bit MSVC compiler error due to unknown command #warning (#2202) --- httplib.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/httplib.h b/httplib.h index 52b1b829f7..25e1b2f1ab 100644 --- a/httplib.h +++ b/httplib.h @@ -16,8 +16,13 @@ */ #if defined(_WIN32) && !defined(_WIN64) +#if defined(_MSC_VER) +#pragma message( \ + "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.") +#else #warning \ "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler." +#endif #elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8 #warning \ "cpp-httplib doesn't support 32-bit platforms. Please use a 64-bit compiler." From fbd6ce7a3f9878a8a433e37d0c2ac9f7e2dcabd7 Mon Sep 17 00:00:00 2001 From: Thomas Beutlich <115483027+thbeu@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:57:39 +0200 Subject: [PATCH 12/29] Make code sample compilable (#2207) --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ff07694c22..a69f7be791 100644 --- a/README.md +++ b/README.md @@ -98,32 +98,31 @@ httplib::Client cli("https://example.com"); auto res = cli.Get("/"); if (!res) { // Check the error type - auto err = res.error(); + const auto err = res.error(); switch (err) { case httplib::Error::SSLConnection: std::cout << "SSL connection failed, SSL error: " - << res->ssl_error() << std::endl; + << res.ssl_error() << std::endl; break; case httplib::Error::SSLLoadingCerts: std::cout << "SSL cert loading failed, OpenSSL error: " - << std::hex << res->ssl_openssl_error() << std::endl; + << std::hex << res.ssl_openssl_error() << std::endl; break; case httplib::Error::SSLServerVerification: std::cout << "SSL verification failed, X509 error: " - << res->ssl_openssl_error() << std::endl; + << res.ssl_openssl_error() << std::endl; break; case httplib::Error::SSLServerHostnameVerification: std::cout << "SSL hostname verification failed, X509 error: " - << res->ssl_openssl_error() << std::endl; + << res.ssl_openssl_error() << std::endl; break; default: std::cout << "HTTP error: " << httplib::to_string(err) << std::endl; - } } } ``` From fe7fe15d2ea9acf290dc27002f386649f750fa15 Mon Sep 17 00:00:00 2001 From: Andrea Pappacoda Date: Tue, 19 Aug 2025 21:22:08 +0200 Subject: [PATCH 13/29] build(meson): fix new build option names (#2208) This is a follow-up to commit 4ff7a1c858faeab14d0f9e8d533a9787cab1de08, which introduced new simplified build options and deprecated the old ones. I forgot to also change the various get_option() calls, effectively rendering the new option names useless, as they would not get honoured. --- meson.build | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/meson.build b/meson.build index 8f667f8060..7e95bdea96 100644 --- a/meson.build +++ b/meson.build @@ -39,12 +39,12 @@ endif deps = [dependency('threads')] args = [] -openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('cpp-httplib_openssl')) +openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('openssl')) if openssl_dep.found() deps += openssl_dep args += '-DCPPHTTPLIB_OPENSSL_SUPPORT' if host_machine.system() == 'darwin' - macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('cpp-httplib_macosx_keychain')) + macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('macosx_keychain')) if macosx_keychain_dep.found() deps += macosx_keychain_dep args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN' @@ -52,15 +52,15 @@ if openssl_dep.found() endif endif -zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib')) +zlib_dep = dependency('zlib', required: get_option('zlib')) if zlib_dep.found() deps += zlib_dep args += '-DCPPHTTPLIB_ZLIB_SUPPORT' endif -brotli_deps = [dependency('libbrotlicommon', required: get_option('cpp-httplib_brotli'))] -brotli_deps += dependency('libbrotlidec', required: get_option('cpp-httplib_brotli')) -brotli_deps += dependency('libbrotlienc', required: get_option('cpp-httplib_brotli')) +brotli_deps = [dependency('libbrotlicommon', required: get_option('brotli'))] +brotli_deps += dependency('libbrotlidec', required: get_option('brotli')) +brotli_deps += dependency('libbrotlienc', required: get_option('brotli')) brotli_found_all = true foreach brotli_dep : brotli_deps @@ -74,7 +74,7 @@ if brotli_found_all args += '-DCPPHTTPLIB_BROTLI_SUPPORT' endif -async_ns_opt = get_option('cpp-httplib_non_blocking_getaddrinfo') +async_ns_opt = get_option('non_blocking_getaddrinfo') if host_machine.system() == 'windows' async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt) @@ -91,7 +91,7 @@ endif cpp_httplib_dep = dependency('', required: false) -if get_option('cpp-httplib_compile') +if get_option('compile') python3 = find_program('python3') httplib_ch = custom_target( @@ -135,6 +135,6 @@ endif meson.override_dependency('cpp-httplib', cpp_httplib_dep) -if get_option('cpp-httplib_test') +if get_option('test') subdir('test') endif From 3fae5f1473fbcde414fba36250f90c44dc78fa42 Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 26 Aug 2025 09:46:51 -0700 Subject: [PATCH 14/29] osx: fix inconsistent use of the macro TARGET_OS_OSX (#2222) * osx: fix inconsistent use of the macro `TARGET_OS_OSX` Fixed the build error on iOS: ``` httplib.h:3583:3: error: unknown type name 'CFStringRef' 870 | CFStringRef hostname_ref = CFStringCreateWithCString( ``` Note, `TARGET_OS_OSX` is defined but is 0 when `TARGET_OS_IOS` is 1, and vise versa. Hence, `TARGET_OS_MAC` should have been used, that is set to 1 for the both targets. * improve: non-blocking getaddrinfo() for all mac target variants `TARGET_OS_MAC` should have been used, that is set to 1 for all other targets: OSX, IPHONE (IOS, TV, WATCH, VISION, BRIDGE), SIMULATOR, DRIVERKIT. --- httplib.h | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/httplib.h b/httplib.h index 25e1b2f1ab..a8dd9052ef 100644 --- a/httplib.h +++ b/httplib.h @@ -308,7 +308,7 @@ using socket_t = int; #if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) || \ defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) -#if TARGET_OS_OSX +#if TARGET_OS_MAC #include #include #endif @@ -332,7 +332,7 @@ using socket_t = int; #endif // _WIN32 #if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) -#if TARGET_OS_OSX +#if TARGET_OS_MAC #include #endif #endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO @@ -3578,7 +3578,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, } return ret; -#elif defined(TARGET_OS_OSX) +#elif TARGET_OS_MAC // macOS implementation using CFHost API for asynchronous DNS resolution CFStringRef hostname_ref = CFStringCreateWithCString( kCFAllocatorDefault, node, kCFStringEncodingUTF8); @@ -6258,8 +6258,7 @@ inline bool load_system_certs_on_windows(X509_STORE *store) { return result; } -#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \ - defined(TARGET_OS_OSX) +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC template using CFObjectPtr = std::unique_ptr::type, void (*)(CFTypeRef)>; @@ -11008,8 +11007,7 @@ inline bool SSLClient::load_certs() { #ifdef _WIN32 loaded = detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); -#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \ - defined(TARGET_OS_OSX) +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); #endif // _WIN32 if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } From b8e21eac89a93234791b96e472de65354bf47cce Mon Sep 17 00:00:00 2001 From: tejas Date: Wed, 27 Aug 2025 01:04:13 +0530 Subject: [PATCH 15/29] Initialize start time for server (#2220) * Initialize start time for server By initializing start_time_ for server, I hope to measure the time taken to process a request at the end maybe in the set_logger callback and print it. I only see current usage in client with server retaining the inital min value * Add test to verify start time is initialized * Address review comments * run clang format --- httplib.h | 1 + test/test.cc | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/httplib.h b/httplib.h index a8dd9052ef..703c171a34 100644 --- a/httplib.h +++ b/httplib.h @@ -8264,6 +8264,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, if (!line_reader.getline()) { return false; } Request req; + req.start_time_ = std::chrono::steady_clock::now(); Response res; res.version = "HTTP/1.1"; diff --git a/test/test.cc b/test/test.cc index 0e4fd42d58..4788ada383 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3200,6 +3200,11 @@ class ServerTest : public ::testing::Test { [&](const Request & /*req*/, Response &res) { res.set_content("abcdefg", "text/plain"); }) + .Get("/test-start-time", + [&](const Request &req, Response &res) { + EXPECT_NE(req.start_time_, + std::chrono::steady_clock::time_point::min()); + }) .Get("/with-range-customized-response", [&](const Request & /*req*/, Response &res) { res.status = StatusCode::BadRequest_400; @@ -5800,6 +5805,8 @@ TEST_F(ServerTest, TooManyRedirect) { EXPECT_EQ(Error::ExceedRedirectCount, res.error()); } +TEST_F(ServerTest, StartTime) { auto res = cli_.Get("/test-start-time"); } + #ifdef CPPHTTPLIB_ZLIB_SUPPORT TEST_F(ServerTest, Gzip) { Headers headers; From 92b4f5301239bad096968dcc6e3de64dc0b0a5c4 Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 26 Aug 2025 20:29:26 -0400 Subject: [PATCH 16/29] clang-format --- test/test.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.cc b/test/test.cc index 4788ada383..867b093e9f 100644 --- a/test/test.cc +++ b/test/test.cc @@ -3201,7 +3201,7 @@ class ServerTest : public ::testing::Test { res.set_content("abcdefg", "text/plain"); }) .Get("/test-start-time", - [&](const Request &req, Response &res) { + [&](const Request &req, Response & /*res*/) { EXPECT_NE(req.start_time_, std::chrono::steady_clock::time_point::min()); }) From 4285d33992bfa05d0313843973341caab552388c Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 26 Aug 2025 21:42:13 -0400 Subject: [PATCH 17/29] Fix #2223 (#2224) * Fix #2223 * Fix build error --- httplib.h | 21 ++++++++++++++++----- test/test.cc | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/httplib.h b/httplib.h index 703c171a34..371db9a618 100644 --- a/httplib.h +++ b/httplib.h @@ -10429,6 +10429,13 @@ inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT namespace detail { +inline bool is_ip_address(const std::string &host) { + struct in_addr addr4; + struct in6_addr addr6; + return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || + inet_pton(AF_INET6, host.c_str(), &addr6) == 1; +} + template inline SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex, U SSL_connect_or_accept, V setup) { @@ -11087,14 +11094,18 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { return true; }, [&](SSL *ssl2) { + // Set SNI only if host is not IP address + if (!detail::is_ip_address(host_)) { #if defined(OPENSSL_IS_BORINGSSL) - SSL_set_tlsext_host_name(ssl2, host_.c_str()); + SSL_set_tlsext_host_name(ssl2, host_.c_str()); #else - // NOTE: Direct call instead of using the OpenSSL macro to suppress - // -Wold-style-cast warning - SSL_ctrl(ssl2, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, - static_cast(const_cast(host_.c_str()))); + // NOTE: Direct call instead of using the OpenSSL macro to suppress + // -Wold-style-cast warning + SSL_ctrl(ssl2, SSL_CTRL_SET_TLSEXT_HOSTNAME, + TLSEXT_NAMETYPE_host_name, + static_cast(const_cast(host_.c_str()))); #endif + } return true; }); diff --git a/test/test.cc b/test/test.cc index 867b093e9f..23dd0fb418 100644 --- a/test/test.cc +++ b/test/test.cc @@ -7366,6 +7366,48 @@ TEST(KeepAliveTest, SSLClientReconnectionPost) { ASSERT_TRUE(result); EXPECT_EQ(200, result->status); } + +TEST(SNI_AutoDetectionTest, SNI_Logic) { + { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/sni", [&](const Request &req, Response &res) { + std::string expected; + if (req.ssl) { + if (const char *sni = + SSL_get_servername(req.ssl, TLSEXT_NAMETYPE_host_name)) { + expected = sni; + } + } + EXPECT_EQ(expected, req.get_param_value("expected")); + res.set_content("ok", "text/plain"); + }); + + auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + listen_thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + { + SSLClient cli("localhost", PORT); + cli.enable_server_certificate_verification(false); + auto res = cli.Get("/sni?expected=localhost"); + ASSERT_TRUE(res); + } + + { + SSLClient cli("::1", PORT); + cli.enable_server_certificate_verification(false); + auto res = cli.Get("/sni?expected="); + ASSERT_TRUE(res); + } + } +} #endif TEST(ClientProblemDetectionTest, ContentProvider) { From f4cc542d4b5149ab78bac2e889e564c7550f2244 Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 26 Aug 2025 22:17:54 -0400 Subject: [PATCH 18/29] Fix Dockerfile problem with CMD --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 67aa028b7e..3495b42f24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ COPY docker/html/index.html /html/index.html EXPOSE 80 ENTRYPOINT ["/server"] -CMD ["0.0.0.0", "80", "/", "/html"] +CMD ["--host", "0.0.0.0", "--port", "80", "--mount", "/:./html"] From b20b5fdd1f5355c27a3cac42e2c4ea0e4729616d Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 26 Aug 2025 23:18:59 -0400 Subject: [PATCH 19/29] Add 'release-docker' workflow --- .github/workflows/release-docker.yml | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/release-docker.yml diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml new file mode 100644 index 0000000000..1038bf847c --- /dev/null +++ b/.github/workflows/release-docker.yml @@ -0,0 +1,35 @@ +name: Release Docker Image + +on: + release: + types: [published] + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract version tag without 'v' + id: set_tag + run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: | + yhirose4dockerhub/cpp-httplib-server:latest + yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag.outputs.tag }} From 54e75dc8ef4dd92cda46c5d83222ea01bfabf8a2 Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 26 Aug 2025 23:34:18 -0400 Subject: [PATCH 20/29] Add manual run --- .github/workflows/release-docker.yml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 1038bf847c..80a5c07cdc 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -1,8 +1,12 @@ + + + name: Release Docker Image on: release: types: [published] + workflow_dispatch: jobs: build-and-push: @@ -10,6 +14,23 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history and tags + + - name: Extract tag (manual) + if: github.event_name == 'workflow_dispatch' + id: set_tag_manual + run: | + # Checkout the latest tag and set output + git fetch --tags + LATEST_TAG=$(git describe --tags --abbrev=0) + git checkout $LATEST_TAG + echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT + + - name: Extract tag (release) + if: github.event_name == 'release' + id: set_tag_release + run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -20,16 +41,13 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Extract version tag without 'v' - id: set_tag - run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile push: true + # Use extracted tag without leading 'v' tags: | yhirose4dockerhub/cpp-httplib-server:latest - yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag.outputs.tag }} + yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag_manual.outputs.tag || steps.set_tag_release.outputs.tag }} From eb11032797f18b21d4a24abacd6c5df655e99dbb Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 27 Aug 2025 00:31:14 -0400 Subject: [PATCH 21/29] Fix platform problem --- .github/workflows/release-docker.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 80a5c07cdc..179ab82fae 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -1,6 +1,3 @@ - - - name: Release Docker Image on: @@ -47,6 +44,7 @@ jobs: context: . file: ./Dockerfile push: true + platforms: linux/amd64,linux/arm64 # Build for both amd64 and arm64 # Use extracted tag without leading 'v' tags: | yhirose4dockerhub/cpp-httplib-server:latest From 7a3b92bbd9937de24793d758b108e59fe31a0d1c Mon Sep 17 00:00:00 2001 From: kgokalp Date: Thu, 28 Aug 2025 18:08:32 +0300 Subject: [PATCH 22/29] Fix: handle EAI_ALLDONE from gai_suspend in getaddrinfo_with_timeout (#2228) --- httplib.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index 371db9a618..c035984394 100644 --- a/httplib.h +++ b/httplib.h @@ -3776,7 +3776,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, int wait_result = gai_suspend((const struct gaicb *const *)requests, 1, &timeout); - if (wait_result == 0) { + if (wait_result == 0 || wait_result == EAI_ALLDONE) { // Completed successfully, get the result int gai_result = gai_error(&request); if (gai_result == 0) { From eb5a65e0dfbaf52e9e43b253be5aae6fcbcfc20e Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 29 Aug 2025 15:01:59 -0400 Subject: [PATCH 23/29] Fix #2217 --- httplib.h | 5 ++++- test/test.cc | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index c035984394..d44766ad3f 100644 --- a/httplib.h +++ b/httplib.h @@ -8984,7 +8984,9 @@ inline bool ClientImpl::create_redirect_client( } // Handle CA certificate store and paths if available - if (ca_cert_store_) { redirect_client.set_ca_cert_store(ca_cert_store_); } + if (ca_cert_store_ && X509_STORE_up_ref(ca_cert_store_)) { + redirect_client.set_ca_cert_store(ca_cert_store_); + } if (!ca_cert_file_path_.empty()) { redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); } @@ -10878,6 +10880,7 @@ inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) { if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) { // Free memory allocated for old cert and use new store `ca_cert_store` SSL_CTX_set_cert_store(ctx_, ca_cert_store); + ca_cert_store_ = ca_cert_store; } } else { X509_STORE_free(ca_cert_store); diff --git a/test/test.cc b/test/test.cc index 23dd0fb418..490260675e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -9012,6 +9012,46 @@ TEST(HttpToHttpsRedirectTest, CertFile) { ASSERT_EQ(StatusCode::OK_200, res->status); } +TEST(SSLClientRedirectTest, CertFile) { + SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(ssl_svr1.is_valid()); + ssl_svr1.Get("/index", [&](const Request &, Response &res) { + res.set_redirect("https://127.0.0.1:1235/index"); + ssl_svr1.stop(); + }); + + SSLServer ssl_svr2(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(ssl_svr2.is_valid()); + ssl_svr2.Get("/index", [&](const Request &, Response &res) { + res.set_content("test", "text/plain"); + ssl_svr2.stop(); + }); + + thread t = thread([&]() { ASSERT_TRUE(ssl_svr1.listen("127.0.0.1", PORT)); }); + thread t2 = + thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", 1235)); }); + auto se = detail::scope_exit([&] { + t2.join(); + t.join(); + ASSERT_FALSE(ssl_svr1.is_running()); + }); + + ssl_svr1.wait_until_ready(); + ssl_svr2.wait_until_ready(); + + SSLClient cli("127.0.0.1", PORT); + std::string cert; + read_file(SERVER_CERT2_FILE, cert); + cli.load_ca_cert_store(cert.c_str(), cert.size()); + cli.enable_server_certificate_verification(true); + cli.set_follow_location(true); + cli.set_connection_timeout(30); + + auto res = cli.Get("/index"); + ASSERT_TRUE(res); + ASSERT_EQ(StatusCode::OK_200, res->status); +} + TEST(MultipartFormDataTest, LargeData) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); From 89c932f313c6437c38f2982869beacc89c2f2246 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 29 Aug 2025 16:05:44 -0400 Subject: [PATCH 24/29] Release v0.26.0 --- httplib.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httplib.h b/httplib.h index d44766ad3f..e15ba44f22 100644 --- a/httplib.h +++ b/httplib.h @@ -8,8 +8,8 @@ #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.25.0" -#define CPPHTTPLIB_VERSION_NUM "0x001900" +#define CPPHTTPLIB_VERSION "0.26.0" +#define CPPHTTPLIB_VERSION_NUM "0x001A00" /* * Platform compatibility check From f72b4582e60051e582543872377dd4223a8e1c33 Mon Sep 17 00:00:00 2001 From: apocelipes Date: Sun, 14 Sep 2025 20:05:51 +0800 Subject: [PATCH 25/29] Fix: Fix Windows Cross-Compilation (#2234) --- CMakeLists.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d39958a05c..d88fdde449 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,8 +117,15 @@ if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() -if(CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0") - message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.") +if(CMAKE_SYSTEM_NAME MATCHES "Windows") + if(CMAKE_SYSTEM_VERSION) + if(${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0") + message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.") + endif() + else() + set(CMAKE_SYSTEM_VERSION "10.0.19041.0") + message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.") + endif() endif() if(CMAKE_SIZEOF_VOID_P LESS 8) message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.") From 6e52d0a0574d02ec0aaabe8610aff89b03da336b Mon Sep 17 00:00:00 2001 From: Jonas van den Berg Date: Mon, 15 Sep 2025 02:05:09 +0200 Subject: [PATCH 26/29] Fix UB by use of dangling references in getaddrinfo_with_timeout (#2232) * Fix use of dangling references When the resolve thread is detached, local variables were still used, which could lead to a program crash. * Add test to verify dangling ref fix * Add missing brace initialization * Assert that the remaining fields are really zeroed * Fix use of chrono literals --- httplib.h | 56 +++++++++++++++++++++++++++++++++++++--------------- test/test.cc | 21 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/httplib.h b/httplib.h index e15ba44f22..054e37a7c2 100644 --- a/httplib.h +++ b/httplib.h @@ -3798,31 +3798,55 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, } #else // Fallback implementation using thread-based timeout for other Unix systems - std::mutex result_mutex; - std::condition_variable result_cv; - auto completed = false; - auto result = EAI_SYSTEM; - struct addrinfo *result_addrinfo = nullptr; - std::thread resolve_thread([&]() { - auto thread_result = getaddrinfo(node, service, hints, &result_addrinfo); + struct GetAddrInfoState { + std::mutex mutex{}; + std::condition_variable result_cv{}; + bool completed{false}; + int result{0}; + std::string node{}; + std::string service{}; + struct addrinfo hints {}; + struct addrinfo *info{nullptr}; + }; - std::lock_guard lock(result_mutex); - result = thread_result; - completed = true; - result_cv.notify_one(); + // Allocate on the heap, so the resolver thread can keep using the data. + auto state = std::make_shared(); + + state->result = EAI_SYSTEM; + state->node = node; + state->service = service; + state->hints.ai_flags = hints->ai_flags; + state->hints.ai_family = hints->ai_family; + state->hints.ai_socktype = hints->ai_socktype; + state->hints.ai_protocol = hints->ai_protocol; + // The remaining fields of "hints" must be zeroed, so do not copy them. + assert(hints->ai_addrlen == 0); + assert(hints->ai_addr == nullptr); + assert(hints->ai_canonname == nullptr); + assert(hints->ai_next == nullptr); + + std::thread resolve_thread([=]() { + auto thread_result = getaddrinfo( + state->node.c_str(), state->service.c_str(), hints, &state->info); + + std::lock_guard lock(state->mutex); + state->result = thread_result; + state->completed = true; + state->result_cv.notify_one(); }); // Wait for completion or timeout - std::unique_lock lock(result_mutex); - auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), - [&] { return completed; }); + std::unique_lock lock(state->mutex); + auto finished = + state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), + [&] { return state->completed; }); if (finished) { // Operation completed within timeout resolve_thread.join(); - *res = result_addrinfo; - return result; + *res = state->info; + return state->result; } else { // Timeout occurred resolve_thread.detach(); // Let the thread finish in background diff --git a/test/test.cc b/test/test.cc index 490260675e..a9ac0d17f1 100644 --- a/test/test.cc +++ b/test/test.cc @@ -1331,6 +1331,27 @@ TEST(RangeTest, FromHTTPBin_Online) { } } +TEST(GetAddrInfoDanglingRefTest, LongTimeout) { + auto host = "unresolvableaddress.local"; + auto path = std::string{"/"}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + auto port = 443; + SSLClient cli(host, port); +#else + auto port = 80; + Client cli(host, port); +#endif + cli.set_connection_timeout(1); + + { + auto res = cli.Get(path); + ASSERT_FALSE(res); + } + + std::this_thread::sleep_for(std::chrono::seconds(8)); +} + TEST(ConnectionErrorTest, InvalidHost) { auto host = "-abcde.com"; From 41be1e24e3a58c17390002aa50a1afc8b1fad284 Mon Sep 17 00:00:00 2001 From: yhirose Date: Mon, 15 Sep 2025 07:59:53 -0400 Subject: [PATCH 27/29] Code cleanup --- httplib.h | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/httplib.h b/httplib.h index 054e37a7c2..db55d07e25 100644 --- a/httplib.h +++ b/httplib.h @@ -3800,32 +3800,19 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service, // Fallback implementation using thread-based timeout for other Unix systems struct GetAddrInfoState { - std::mutex mutex{}; - std::condition_variable result_cv{}; - bool completed{false}; - int result{0}; - std::string node{}; - std::string service{}; - struct addrinfo hints {}; - struct addrinfo *info{nullptr}; + std::mutex mutex; + std::condition_variable result_cv; + bool completed = false; + int result = EAI_SYSTEM; + std::string node = node; + std::string service = service; + struct addrinfo hints = hints; + struct addrinfo *info = nullptr; }; // Allocate on the heap, so the resolver thread can keep using the data. auto state = std::make_shared(); - state->result = EAI_SYSTEM; - state->node = node; - state->service = service; - state->hints.ai_flags = hints->ai_flags; - state->hints.ai_family = hints->ai_family; - state->hints.ai_socktype = hints->ai_socktype; - state->hints.ai_protocol = hints->ai_protocol; - // The remaining fields of "hints" must be zeroed, so do not copy them. - assert(hints->ai_addrlen == 0); - assert(hints->ai_addr == nullptr); - assert(hints->ai_canonname == nullptr); - assert(hints->ai_next == nullptr); - std::thread resolve_thread([=]() { auto thread_result = getaddrinfo( state->node.c_str(), state->service.c_str(), hints, &state->info); From 35c52c1ab9b8ade2e66548fcec5e417092d8e583 Mon Sep 17 00:00:00 2001 From: Andrea Pappacoda Date: Sat, 20 Sep 2025 21:06:49 +0200 Subject: [PATCH 28/29] build(meson): use C++17 for gtest >= 1.17.0 (#2241) --- test/meson.build | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/meson.build b/test/meson.build index 80621d30aa..745236b9e3 100644 --- a/test/meson.build +++ b/test/meson.build @@ -108,9 +108,11 @@ subdir('www') subdir('www2'/'dir') subdir('www3'/'dir') -# GoogleTest 1.13.0 requires C++14 +# New GoogleTest versions require new C++ standards test_options = [] -if gtest_dep.version().version_compare('>=1.13.0') +if gtest_dep.version().version_compare('>=1.17.0') + test_options += 'cpp_std=c++17' +elif gtest_dep.version().version_compare('>=1.13.0') test_options += 'cpp_std=c++14' endif From db561f5552cff751cf70393b6f236e720b12271b Mon Sep 17 00:00:00 2001 From: crueter Date: Thu, 16 Oct 2025 09:59:15 -0400 Subject: [PATCH 29/29] [cmake] FindBrotli: do not add `Brotli::` targets if they already exist (#2249) Not checking for this is terrible practice. --- cmake/FindBrotli.cmake | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmake/FindBrotli.cmake b/cmake/FindBrotli.cmake index 2048e55499..de167ed959 100644 --- a/cmake/FindBrotli.cmake +++ b/cmake/FindBrotli.cmake @@ -86,7 +86,9 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc") # Check if the target was already found by Pkgconf if(TARGET PkgConfig::Brotli_${_component_name}${_brotli_stat_str}) # ALIAS since we don't want the PkgConfig namespace on the Cmake library (for end-users) - add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str}) + if (NOT TARGET Brotli::${_component_name}) + add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str}) + endif() # Tells HANDLE_COMPONENTS we found the component set(Brotli_${_component_name}_FOUND TRUE) @@ -139,7 +141,10 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc") # Tells HANDLE_COMPONENTS we found the component set(Brotli_${_component_name}_FOUND TRUE) - add_library("Brotli::${_component_name}" UNKNOWN IMPORTED) + if (NOT TARGET Brotli::${_component_name}) + add_library("Brotli::${_component_name}" UNKNOWN IMPORTED) + endif() + # Attach the literal library and include dir to the IMPORTED target for the end-user set_target_properties("Brotli::${_component_name}" PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${Brotli_INCLUDE_DIR}"