From 1f4fd9fec2ae57091d2e9b786a62b9bf23aeebbd Mon Sep 17 00:00:00 2001 From: yhirose Date: Sun, 6 Jul 2025 22:24:15 -0400 Subject: [PATCH 1/3] Fix #870 --- README.md | 73 ++++++++++++++- example/Makefile | 7 +- example/nginxish_server.cc | 174 ++++++++++++++++++++++++++++++++++ example/server.cc | 2 +- example/server_and_client.cc | 2 +- example/simplesvr.cc | 2 +- httplib.h | 77 ++++++++++----- test/test.cc | 175 +++++++++++++++++++++++++++++++---- 8 files changed, 463 insertions(+), 49 deletions(-) create mode 100644 example/nginxish_server.cc diff --git a/README.md b/README.md index 66348cc52b..92b94a19d8 100644 --- a/README.md +++ b/README.md @@ -269,23 +269,44 @@ 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_access_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 Request and Error information, as errors typically occur before a meaningful Response can be generated. + +```cpp +svr.set_error_logger([](const httplib::Request& req, const httplib::Error& err) { + std::cerr << httplib::to_string(err) << " while processing request" + << ", client: " << req.get_header_value("X-Forwarded-For") + << ", request: '" << req.method << " " << req.path << " " << req.version << "'" + << ", host: " << req.get_header_value("Host") << std::endl; +}); +``` ### Error handler @@ -718,6 +739,48 @@ enum Error { }; ``` +### Client Logging + +#### Access Logging + +```cpp +cli.set_access_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::Request& req, const httplib::Error& err) { + std::cerr << "✗ " << req.method << " " << req.path + << " 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++ diff --git a/example/Makefile b/example/Makefile index 3082b88014..c8a6a4a608 100644 --- a/example/Makefile +++ b/example/Makefile @@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz BROTLI_DIR = $(PREFIX)/opt/brotli BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec -all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header +all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header nginxish_server server : server.cc ../httplib.h Makefile $(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) @@ -59,9 +59,12 @@ server_and_client : server_and_client.cc ../httplib.h Makefile accept_header : accept_header.cc ../httplib.h Makefile $(CXX) -o accept_header $(CXXFLAGS) accept_header.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) +nginxish_server : nginxish_server.cc ../httplib.h Makefile + $(CXX) -o nginxish_server $(CXXFLAGS) nginxish_server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) + pem: openssl genrsa 2048 > key.pem openssl req -new -key key.pem | openssl x509 -days 3650 -req -signkey key.pem > cert.pem clean: - rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header *.pem + rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header nginxish_server *.pem diff --git a/example/nginxish_server.cc b/example/nginxish_server.cc new file mode 100644 index 0000000000..cd075cf66b --- /dev/null +++ b/example/nginxish_server.cc @@ -0,0 +1,174 @@ +// +// nginxish_server.cc +// +// Copyright (c) 2025 Yuji Hirose. All rights reserved. +// MIT License +// + +#include +#include +#include +#include +#include +#include + +using namespace httplib; + +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(&time_t), "%d/%b/%Y:%H:%M:%S %z"); + 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"; +} + +// 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 = req.method + " " + req.path + " HTTP/" + 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 << remote_addr << " - " << remote_user << " [" << time_local << "] " + << "\"" << request << "\" " << status << " " << body_bytes_sent + << " " + << "\"" << http_referer << "\" \"" << http_user_agent << "\"" + << std::endl; +} + +// NGINX Error log format: +// [time] [level] pid#tid: *cid message, client: client_ip, server: server_name, +// request: "request", host: "host" +void nginx_error_logger(const Request &req, const Error &err) { + std::string time_local = get_nginx_time_format(); + std::string level = "error"; + std::string client_ip = get_client_ip(req); + std::string server_name = req.get_header_value("Host"); + if (server_name.empty()) server_name = "-"; + std::string request = req.method + " " + req.path + " HTTP/" + req.version; + std::string host = req.get_header_value("Host"); + if (host.empty()) host = "-"; + + std::cerr << "[" << time_local << "] [" << level << "] " << to_string(err) + << ", client: " << client_ip << ", server: " << server_name + << ", request: \"" << request << "\"" + << ", host: \"" << host << "\"" << std::endl; +} + +void print_usage(const char *program_name) { + std::cerr << "Usage: " << program_name + << " " + << std::endl; + std::cerr << "Example: " << program_name << " localhost 8080 /var/www/html ." + << std::endl; +} + +int main(int argc, char *argv[]) { + if (argc != 5) { + print_usage(argv[0]); + return 1; + } + + std::string hostname = argv[1]; + int port = std::atoi(argv[2]); + std::string mount_point = argv[3]; + std::string document_root = argv[4]; + + if (port <= 0 || port > 65535) { + std::cerr << "Error: Invalid port number. Must be between 1 and 65535." + << std::endl; + return 1; + } + + Server svr; + + // Set up NGINX-style logging + svr.set_access_logger(nginx_access_logger); + svr.set_error_logger(nginx_error_logger); + + // Set up static file serving + auto ret = svr.set_mount_point(mount_point, document_root); + if (!ret) { + std::cerr << "Error: Cannot mount '" << mount_point << "' to '" + << document_root << "'. Directory may not exist." << std::endl; + return 1; + } + + // Add some common MIME types (similar to NGINX) + 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"); + + // Custom error handler for 404s + svr.set_error_handler([](const Request & /*req*/, Response &res) { + if (res.status == 404) { + res.set_content( + "404 Not Found" + "

404 Not Found

" + "

The requested resource was not found on this server.

" + "

nginxish_server/1.0

", + "text/html"); + } + }); + + // Set server header to mimic NGINX + svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) { + res.set_header("Server", "nginxish_server/1.0"); + return Server::HandlerResponse::Unhandled; + }); + + std::cout << "Starting nginxish_server on " << hostname << ":" << port + << std::endl; + std::cout << "Document root: " << document_root << std::endl; + std::cout << "Mount point: " << mount_point << " -> " << document_root + << std::endl; + std::cout << "Press Ctrl+C to stop the server" << std::endl; + + // Start the server + svr.listen(hostname, port); + + return 0; +} diff --git a/example/server.cc b/example/server.cc index 1c347f5f9c..9893d062ef 100644 --- a/example/server.cc +++ b/example/server.cc @@ -103,7 +103,7 @@ int main(void) { res.set_content(buf, "text/html"); }); - svr.set_logger([](const Request &req, const Response &res) { + svr.set_access_logger([](const Request &req, const Response &res) { printf("%s", log(req, res).c_str()); }); diff --git a/example/server_and_client.cc b/example/server_and_client.cc index 34bf852714..9ab6f63229 100644 --- a/example/server_and_client.cc +++ b/example/server_and_client.cc @@ -64,7 +64,7 @@ void logger(const Request &req, const Response &res) { int main(void) { // Server Server svr; - svr.set_logger(logger); + svr.set_access_logger(logger); svr.Post("/post", [&](const Request & /*req*/, Response &res) { res.set_content("POST", "text/plain"); diff --git a/example/simplesvr.cc b/example/simplesvr.cc index e45e360646..ae865af431 100644 --- a/example/simplesvr.cc +++ b/example/simplesvr.cc @@ -126,7 +126,7 @@ int main(int argc, const char **argv) { res.set_content(buf, "text/html"); }); - svr.set_logger( + svr.set_access_logger( [](const Request &req, const Response &res) { cout << log(req, res); }); auto port = 8080; diff --git a/httplib.h b/httplib.h index eaea3d85ae..9a611c9daa 100644 --- a/httplib.h +++ b/httplib.h @@ -926,7 +926,11 @@ class ThreadPool final : public TaskQueue { std::mutex mutex_; }; -using Logger = std::function; +using AccessLogger = std::function; + +// Forward declaration for Error type +enum class Error; +using ErrorLogger = std::function; using SocketOptions = std::function; @@ -1086,8 +1090,9 @@ class Server { Server &set_pre_request_handler(HandlerWithResponse handler); Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); - Server &set_logger(Logger logger); - Server &set_pre_compression_logger(Logger logger); + Server &set_access_logger(AccessLogger access_logger); + Server &set_pre_compression_logger(AccessLogger logger); + Server &set_error_logger(ErrorLogger error_logger); Server &set_address_family(int family); Server &set_tcp_nodelay(bool on); @@ -1230,8 +1235,9 @@ class Server { HandlerWithResponse pre_request_handler_; Expect100ContinueHandler expect_100_continue_handler_; - Logger logger_; - Logger pre_compression_logger_; + AccessLogger access_logger_; + AccessLogger pre_compression_logger_; + ErrorLogger error_logger_; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; @@ -1503,7 +1509,8 @@ class ClientImpl { std::function verifier); #endif - void set_logger(Logger logger); + void set_access_logger(AccessLogger access_logger); + void set_error_logger(ErrorLogger error_logger); protected: struct Socket { @@ -1620,7 +1627,8 @@ class ClientImpl { std::function server_certificate_verifier_; #endif - Logger logger_; + AccessLogger access_logger_; + ErrorLogger error_logger_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT int last_ssl_error_ = 0; @@ -1846,7 +1854,8 @@ class Client { std::function verifier); #endif - void set_logger(Logger logger); + void set_access_logger(AccessLogger access_logger); + void set_error_logger(ErrorLogger error_logger); // SSL #ifdef CPPHTTPLIB_OPENSSL_SUPPORT @@ -7050,13 +7059,18 @@ inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) { return *this; } -inline Server &Server::set_logger(Logger logger) { - logger_ = std::move(logger); +inline Server &Server::set_pre_compression_logger(AccessLogger logger) { + pre_compression_logger_ = std::move(logger); return *this; } -inline Server &Server::set_pre_compression_logger(Logger logger) { - pre_compression_logger_ = std::move(logger); +inline Server &Server::set_access_logger(AccessLogger access_logger) { + access_logger_ = std::move(access_logger); + return *this; +} + +inline Server &Server::set_error_logger(ErrorLogger error_logger) { + error_logger_ = std::move(error_logger); return *this; } @@ -7303,7 +7317,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection, } // Log - if (logger_) { logger_(req, res); } + if (access_logger_) { access_logger_(req, res); } return ret; } @@ -8136,7 +8150,8 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) { server_hostname_verification_ = rhs.server_hostname_verification_; server_certificate_verifier_ = rhs.server_certificate_verifier_; #endif - logger_ = rhs.logger_; + access_logger_ = rhs.access_logger_; + error_logger_ = rhs.error_logger_; } inline socket_t ClientImpl::create_client_socket(Error &error) const { @@ -8279,7 +8294,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)) { + if (error_logger_) { error_logger_(req, error); } + return false; + } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // TODO: refactoring @@ -8289,11 +8307,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 && error_logger_) { error_logger_(req, error); } return success; } } - if (!scli.initialize_ssl(socket_, error)) { return false; } + if (!scli.initialize_ssl(socket_, error)) { + if (error_logger_) { error_logger_(req, error); } + return false; + } } #endif } @@ -8340,6 +8362,8 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { if (!ret) { if (error == Error::Success) { error = Error::Unknown; } + + if (error_logger_) { error_logger_(req, error); } } return ret; @@ -8616,7 +8640,8 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { if (!interface_.empty()) { client.set_interface(interface_); } // Copy logging and headers - if (logger_) { client.set_logger(logger_); } + if (access_logger_) { client.set_access_logger(access_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 @@ -9009,7 +9034,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, } // Log - if (logger_) { logger_(req, res); } + if (access_logger_) { access_logger_(req, res); } return true; } @@ -9909,8 +9934,12 @@ inline void ClientImpl::set_server_certificate_verifier( } #endif -inline void ClientImpl::set_logger(Logger logger) { - logger_ = std::move(logger); +inline void ClientImpl::set_access_logger(AccessLogger access_logger) { + access_logger_ = std::move(access_logger); +} + +inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { + error_logger_ = std::move(error_logger); } /* @@ -11323,8 +11352,12 @@ inline void Client::set_server_certificate_verifier( } #endif -inline void Client::set_logger(Logger logger) { - cli_->set_logger(std::move(logger)); +inline void Client::set_access_logger(AccessLogger access_logger) { + cli_->set_access_logger(std::move(access_logger)); +} + +inline void Client::set_error_logger(ErrorLogger error_logger) { + cli_->set_error_logger(std::move(error_logger)); } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT diff --git a/test/test.cc b/test/test.cc index 8e8299b76f..98ca15e582 100644 --- a/test/test.cc +++ b/test/test.cc @@ -6092,7 +6092,7 @@ TEST_F(ServerTest, PreCompressionLogging) { }); // Set up post-compression logger - svr_.set_logger([&](const Request & /*req*/, const Response &res) { + svr_.set_access_logger([&](const Request &req, const Response &res) { post_compression_body = res.body; post_compression_content_type = res.get_header_value("Content-Type"); post_compression_content_encoding = @@ -6134,12 +6134,11 @@ TEST_F(ServerTest, PreCompressionLoggingWithBrotli) { std::string pre_compression_body; std::string post_compression_body; - svr_.set_pre_compression_logger( - [&](const Request & /*req*/, const Response &res) { - pre_compression_body = res.body; - }); + svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { + pre_compression_body = res.body; + }); - svr_.set_logger([&](const Request & /*req*/, const Response &res) { + svr_.set_access_logger([&](const Request &req, const Response &res) { post_compression_body = res.body; }); @@ -6168,12 +6167,11 @@ TEST_F(ServerTest, PreCompressionLoggingWithoutCompression) { std::string pre_compression_body; std::string post_compression_body; - svr_.set_pre_compression_logger( - [&](const Request & /*req*/, const Response &res) { - pre_compression_body = res.body; - }); + svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { + pre_compression_body = res.body; + }); - svr_.set_logger([&](const Request & /*req*/, const Response &res) { + svr_.set_access_logger([&](const Request &req, const Response &res) { post_compression_body = res.body; }); @@ -6202,11 +6200,10 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) { bool pre_logger_called = false; // Set only pre-compression logger - svr_.set_pre_compression_logger( - [&](const Request & /*req*/, const Response &res) { - pre_compression_body = res.body; - pre_logger_called = true; - }); + svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { + pre_compression_body = res.body; + pre_logger_called = true; + }); Headers headers; headers.emplace("Accept-Encoding", "gzip"); @@ -6222,6 +6219,150 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) { EXPECT_EQ(test_content, pre_compression_body); } +TEST_F(ServerTest, ErrorLogging) { + bool error_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + // Set error logger + svr_.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + // Create a client that will fail to connect (invalid port) + httplib::Client cli("localhost", 9999); // Non-existent server + cli.set_connection_timeout(1, 0); // 1 second timeout + + auto res = cli.Get("/test"); + + // Verify the client request failed + EXPECT_FALSE(res); + EXPECT_EQ(httplib::Error::Connection, res.error()); + + // Note: Server error logger won't be called for client connection failures + // This test demonstrates the client-side error scenario +} + +TEST(ClientTest, ErrorLogging) { + bool error_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + // Create a client that will definitely fail - use an invalid host + httplib::Client cli("255.255.255.255", 1); // Invalid host and port + cli.set_connection_timeout(1, 0); // 1 second timeout + + // Set error logger on client + cli.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + auto res = cli.Get("/test"); + + // Verify the request failed + EXPECT_FALSE(res); + EXPECT_TRUE(res.error() == httplib::Error::Connection || + res.error() == httplib::Error::ConnectionTimeout); + + // Verify error logger was called with correct information + EXPECT_TRUE(error_logger_called); + EXPECT_EQ("GET", error_request_method); + EXPECT_EQ("/test", error_request_path); + EXPECT_TRUE(error_type == httplib::Error::Connection || + error_type == httplib::Error::ConnectionTimeout); +} + +TEST(ClientTest, ErrorLoggingTimeout) { + bool error_logger_called = false; + httplib::Error error_type; + + // Create a client that will definitely timeout + httplib::Client cli("10.0.0.1", 80); // Non-routable IP for timeout + cli.set_connection_timeout(0, 100000); // 100ms timeout + + cli.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_type = err; + }); + + auto res = cli.Get("/test"); + + // Should fail with timeout or connection error + EXPECT_FALSE(res); + EXPECT_TRUE(res.error() == httplib::Error::ConnectionTimeout || + res.error() == httplib::Error::Connection); + + // Verify error logger was called + EXPECT_TRUE(error_logger_called); + EXPECT_TRUE(error_type == httplib::Error::ConnectionTimeout || + error_type == httplib::Error::Connection); +} + +TEST(ClientTest, ErrorLoggingSSL) { + bool error_logger_called = false; + httplib::Error error_type; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Try to connect to an invalid SSL endpoint + httplib::Client cli("https://invalid-ssl-host.example.com"); + cli.set_connection_timeout(2, 0); + + cli.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_type = err; + }); + + auto res = cli.Get("/"); + + // Should fail with connection or SSL error + EXPECT_FALSE(res); + + if (error_logger_called) { + // Verify we got some kind of connection-related error + EXPECT_TRUE(error_type == httplib::Error::Connection || + error_type == httplib::Error::SSLConnection || + error_type == httplib::Error::ConnectionTimeout); + } +#else + // Skip SSL test if OpenSSL not available + GTEST_SKIP() << "SSL support not available"; +#endif +} + +TEST(ClientTest, AccessLoggerNotCalledOnError) { + bool access_logger_called = false; + bool error_logger_called = false; + + httplib::Client cli("localhost", 9999); // Non-existent server + cli.set_connection_timeout(1, 0); + + // Set both loggers + cli.set_access_logger([&](const Request &req, const Response &res) { + access_logger_called = true; + }); + + cli.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + }); + + auto res = cli.Get("/test"); + + // Verify the request failed + EXPECT_FALSE(res); + + // Verify only error logger was called, not access logger + EXPECT_FALSE(access_logger_called); + EXPECT_TRUE(error_logger_called); +} + TEST(ZstdDecompressor, ChunkedDecompression) { std::string data; for (size_t i = 0; i < 32 * 1024; ++i) { @@ -9650,7 +9791,7 @@ TEST(VulnerabilityTest, CRLFInjection) { res.set_content("Hello 4", "text/plain"); }); - svr.set_logger([](const Request &req, const Response & /*res*/) { + svr.set_access_logger([](const Request &req, const Response & /*res*/) { for (const auto &x : req.headers) { auto key = x.first; EXPECT_STRNE("evil", key.c_str()); From caf9a08a6843efb35e382bb489ebca53f68228cb Mon Sep 17 00:00:00 2001 From: yhirose Date: Mon, 7 Jul 2025 20:36:46 -0400 Subject: [PATCH 2/3] Update --- httplib.h | 50 +++--- test/test.cc | 431 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 445 insertions(+), 36 deletions(-) diff --git a/httplib.h b/httplib.h index 9a611c9daa..8424a46956 100644 --- a/httplib.h +++ b/httplib.h @@ -1182,12 +1182,13 @@ class Server { void apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) const; bool write_response(Stream &strm, bool close_connection, Request &req, - Response &res); + Response &res, Error error); bool write_response_with_content(Stream &strm, bool close_connection, - const Request &req, Response &res); + const Request &req, Response &res, + Error error); bool write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, - bool need_apply_ranges); + bool need_apply_ranges, Error error); bool write_content_with_provider(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type); @@ -7234,23 +7235,23 @@ inline bool Server::parse_request_line(const char *s, Request &req) const { } inline bool Server::write_response(Stream &strm, bool close_connection, - Request &req, Response &res) { + Request &req, Response &res, Error error) { // NOTE: `req.ranges` should be empty, otherwise it will be applied // incorrectly to the error content. req.ranges.clear(); - return write_response_core(strm, close_connection, req, res, false); + return write_response_core(strm, close_connection, req, res, false, error); } inline bool Server::write_response_with_content(Stream &strm, bool close_connection, const Request &req, - Response &res) { - return write_response_core(strm, close_connection, req, res, true); + Response &res, Error error) { + return write_response_core(strm, close_connection, req, res, true, error); } inline bool Server::write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, - bool need_apply_ranges) { + bool need_apply_ranges, Error error) { assert(res.status != -1); if (400 <= res.status && error_handler_ && @@ -7319,6 +7320,11 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection, // Log if (access_logger_) { access_logger_(req, res); } + if ((error != Error::Success || !ret) && error_logger_) { + Error final_error = error != Error::Success ? error : Error::Write; + error_logger_(req, final_error); + } + return ret; } @@ -7892,7 +7898,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, if (!parse_request_line(line_reader.ptr(), req) || !detail::read_headers(strm, req.headers)) { res.status = StatusCode::BadRequest_400; - return write_response(strm, close_connection, req, res); + return write_response(strm, close_connection, req, res, Error::Read); } // Check if the request URI doesn't exceed the limit @@ -7900,7 +7906,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, Headers dummy; detail::read_headers(strm, dummy); res.status = StatusCode::UriTooLong_414; - return write_response(strm, close_connection, req, res); + return write_response(strm, close_connection, req, res, Error::Read); } if (req.get_header_value("Connection") == "close") { @@ -7926,7 +7932,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; - return write_response(strm, close_connection, req, res); + return write_response(strm, close_connection, req, res, Error::Success); } } @@ -7934,7 +7940,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; - return write_response(strm, close_connection, req, res); + return write_response(strm, close_connection, req, res, Error::Success); } } @@ -7953,7 +7959,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, break; default: connection_closed = true; - return write_response(strm, true, req, res); + return write_response(strm, true, req, res, Error::Success); } } @@ -8014,7 +8020,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr, res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::NotFound_404; - return write_response(strm, close_connection, req, res); + return write_response(strm, close_connection, req, res, Error::Success); } auto content_type = res.file_content_content_type_; @@ -8036,14 +8042,20 @@ Server::process_request(Stream &strm, const std::string &remote_addr, res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::RangeNotSatisfiable_416; - return write_response(strm, close_connection, req, res); + return write_response(strm, close_connection, req, res, Error::Success); } - return write_response_with_content(strm, close_connection, req, res); + return write_response_with_content(strm, close_connection, req, res, + Error::Success); } else { - if (res.status == -1) { res.status = StatusCode::NotFound_404; } - - return write_response(strm, close_connection, req, res); + if (res.status == -1) { + res.status = StatusCode::NotFound_404; + return write_response(strm, close_connection, req, res, Error::Success); + } else { + // Status was set by routing (e.g., read error), preserve it and log as + // read error + return write_response(strm, close_connection, req, res, Error::Read); + } } } diff --git a/test/test.cc b/test/test.cc index 98ca15e582..8411db3ba9 100644 --- a/test/test.cc +++ b/test/test.cc @@ -6092,7 +6092,7 @@ TEST_F(ServerTest, PreCompressionLogging) { }); // Set up post-compression logger - svr_.set_access_logger([&](const Request &req, const Response &res) { + svr_.set_access_logger([&](const Request & /*req*/, const Response &res) { post_compression_body = res.body; post_compression_content_type = res.get_header_value("Content-Type"); post_compression_content_encoding = @@ -6134,11 +6134,12 @@ TEST_F(ServerTest, PreCompressionLoggingWithBrotli) { std::string pre_compression_body; std::string post_compression_body; - svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { - pre_compression_body = res.body; - }); + svr_.set_pre_compression_logger( + [&](const Request & /*req*/, const Response &res) { + pre_compression_body = res.body; + }); - svr_.set_access_logger([&](const Request &req, const Response &res) { + svr_.set_access_logger([&](const Request & /*req*/, const Response &res) { post_compression_body = res.body; }); @@ -6167,11 +6168,12 @@ TEST_F(ServerTest, PreCompressionLoggingWithoutCompression) { std::string pre_compression_body; std::string post_compression_body; - svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { - pre_compression_body = res.body; - }); + svr_.set_pre_compression_logger( + [&](const Request & /*req*/, const Response &res) { + pre_compression_body = res.body; + }); - svr_.set_access_logger([&](const Request &req, const Response &res) { + svr_.set_access_logger([&](const Request & /*req*/, const Response &res) { post_compression_body = res.body; }); @@ -6200,10 +6202,11 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) { bool pre_logger_called = false; // Set only pre-compression logger - svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { - pre_compression_body = res.body; - pre_logger_called = true; - }); + svr_.set_pre_compression_logger( + [&](const Request & /*req*/, const Response &res) { + pre_compression_body = res.body; + pre_logger_called = true; + }); Headers headers; headers.emplace("Accept-Encoding", "gzip"); @@ -6247,6 +6250,400 @@ TEST_F(ServerTest, ErrorLogging) { // This test demonstrates the client-side error scenario } +TEST(ServerErrorLoggingTest, ErrorLoggingOnSocketClose) { + bool error_logger_called = false; + bool access_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + const int test_port = 8765; // Use a different port to avoid conflicts + + httplib::Server svr; + + // Set error logger + svr.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + // Set access logger to verify request processing + svr.set_access_logger([&](const Request & /*req*/, const Response & /*res*/) { + access_logger_called = true; + }); + + // Add a handler that returns a large response + svr.Get("/large", [](const Request &, Response &res) { + // Return a very large response that definitely takes time to send + std::string large_content(5 * 1024 * 1024, 'A'); // 5MB of data + res.set_content(large_content, "text/plain"); + }); + + // Start server in a separate thread + auto server_thread = + std::thread([&]() { svr.listen("localhost", test_port); }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Create a raw socket connection to manually control disconnection + httplib::Error error; + auto sock = httplib::detail::create_client_socket( + "localhost", std::string(), test_port, AF_UNSPEC, false, false, nullptr, + 5, 0, 0, 0, 0, 0, std::string(), error); + + if (sock != INVALID_SOCKET) { + // Send HTTP request for large content + std::string request = + "GET /large HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + send(sock, request.c_str(), request.length(), 0); + + // Read a small part of the response to ensure server starts sending + char buffer[1024]; + std::this_thread::sleep_for(std::chrono::milliseconds( + 100)); // Give server more time to start processing + recv(sock, buffer, sizeof(buffer), MSG_DONTWAIT); // Non-blocking read + + // Close socket abruptly during transfer to trigger write error + httplib::detail::close_socket(sock); + } + + // Give server time to detect the error and call error logger + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Stop the server + svr.stop(); + server_thread.join(); + + // Verify that the request was processed (access logger called) + EXPECT_TRUE(access_logger_called); + + // Verify that the server error logger was called + EXPECT_TRUE(error_logger_called); + EXPECT_EQ("GET", error_request_method); + EXPECT_EQ("/large", error_request_path); + EXPECT_EQ(httplib::Error::Write, error_type); +} + +TEST(ServerErrorLoggingTest, ErrorLoggingSimpleCase) { + bool error_logger_called = false; + httplib::Error error_type; + + const int test_port = 8766; // Use a different port to avoid conflicts + + httplib::Server svr; + + // Set error logger + svr.set_error_logger([&](const Request & /*req*/, const Error &err) { + error_logger_called = true; + error_type = err; + }); + + // Add a handler that returns normal response + svr.Get("/test", [](const Request & /*req*/, Response &res) { + res.set_content("Hello", "text/plain"); + }); + + // Start server in a separate thread + auto server_thread = + std::thread([&]() { svr.listen("localhost", test_port); }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Create normal client request (should NOT trigger error logger) + httplib::Client cli("localhost", test_port); + auto res = cli.Get("/test"); + + // Stop the server + svr.stop(); + server_thread.join(); + + // Verify normal request does NOT call error logger + EXPECT_TRUE(res); + EXPECT_EQ(200, res->status); + EXPECT_FALSE(error_logger_called); // Error logger should NOT be called for + // successful requests +} + +TEST(ServerErrorLoggingTest, ErrorLoggingOnPostContentInterruption) { + bool error_logger_called = false; + bool access_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + const int test_port = 8767; // Use a different port to avoid conflicts + + httplib::Server svr; + + // Set error logger + svr.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + // Set access logger to verify request processing started + svr.set_access_logger([&](const Request & /*req*/, const Response & /*res*/) { + access_logger_called = true; + }); + + // Add a POST handler that expects content + svr.Post("/upload", [](const Request &req, Response &res) { + res.set_content("Received: " + req.body, "text/plain"); + }); + + // Start server in a separate thread + auto server_thread = + std::thread([&]() { svr.listen("localhost", test_port); }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Create a raw socket connection to manually control the POST request + httplib::Error error; + auto sock = httplib::detail::create_client_socket( + "localhost", std::string(), test_port, AF_UNSPEC, false, false, nullptr, + 5, 0, 0, 0, 0, 0, std::string(), error); + + if (sock != INVALID_SOCKET) { + // Send POST request headers with Content-Length but incomplete body + std::string large_content(1024 * 1024, 'A'); // 1MB of data + std::string headers = + "POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: " + + std::to_string(large_content.length()) + + "\r\nContent-Type: text/plain\r\n\r\n"; + send(sock, headers.c_str(), headers.length(), 0); + + // Send only partial content then close socket abruptly + std::string partial_content = + large_content.substr(0, 1024); // Send only 1KB out of 1MB + send(sock, partial_content.c_str(), partial_content.length(), 0); + + // Wait a bit for server to start reading, then close socket abruptly + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + httplib::detail::close_socket(sock); + } + + // Give server time to detect the error and call error logger + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Stop the server + svr.stop(); + server_thread.join(); + + // Verify that the server error logger was called for read error + EXPECT_TRUE(error_logger_called); + EXPECT_EQ("POST", error_request_method); + EXPECT_EQ("/upload", error_request_path); + EXPECT_EQ(httplib::Error::Read, error_type); +} + +TEST(ServerErrorLoggingTest, ErrorLoggingOnMalformedMultipartBoundary) { + bool error_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + const int test_port = 8768; // Use a different port to avoid conflicts + + httplib::Server svr; + + // Set error logger + svr.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + // Add a POST handler for multipart data + svr.Post("/upload", [](const Request & /*req*/, Response &res) { + res.set_content("Upload received", "text/plain"); + }); + + // Start server in a separate thread + auto server_thread = + std::thread([&]() { svr.listen("localhost", test_port); }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Create a raw socket connection to send malformed multipart request + httplib::Error error; + auto sock = httplib::detail::create_client_socket( + "localhost", std::string(), test_port, AF_UNSPEC, false, false, nullptr, + 5, 0, 0, 0, 0, 0, std::string(), error); + + if (sock != INVALID_SOCKET) { + // Send POST request with malformed Content-Type (missing boundary) + std::string malformed_request = + "POST /upload HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: multipart/form-data\r\n" // Missing boundary parameter + "Content-Length: 50\r\n" + "\r\n" + "--boundary\r\n" + "Content-Disposition: form-data; name=\"test\"\r\n" + "\r\n" + "value\r\n" + "--boundary--\r\n"; + + send(sock, malformed_request.c_str(), malformed_request.length(), 0); + httplib::detail::close_socket(sock); + } + + // Give server time to process the malformed request and call error logger + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Stop the server + svr.stop(); + server_thread.join(); + + // Verify that the server error logger was called for malformed multipart + // boundary + EXPECT_TRUE(error_logger_called); + EXPECT_EQ("POST", error_request_method); + EXPECT_EQ("/upload", error_request_path); + EXPECT_EQ(httplib::Error::Read, error_type); +} + +TEST(ServerErrorLoggingTest, ErrorLoggingOnInvalidMultipartData) { + bool error_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + const int test_port = 8769; // Use a different port to avoid conflicts + + httplib::Server svr; + + // Set error logger + svr.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + // Add a POST handler for multipart data + svr.Post("/upload", [](const Request & /*req*/, Response &res) { + res.set_content("Upload received", "text/plain"); + }); + + // Start server in a separate thread + auto server_thread = + std::thread([&]() { svr.listen("localhost", test_port); }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Create a raw socket connection to send invalid multipart data + httplib::Error error; + auto sock = httplib::detail::create_client_socket( + "localhost", std::string(), test_port, AF_UNSPEC, false, false, nullptr, + 5, 0, 0, 0, 0, 0, std::string(), error); + + if (sock != INVALID_SOCKET) { + // Send POST request with valid boundary but invalid multipart structure + std::string invalid_multipart_request = + "POST /upload HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Type: multipart/form-data; boundary=test123\r\n" + "Content-Length: 40\r\n" + "\r\n" + "Invalid multipart data without proper format"; // Invalid multipart + // structure + + send(sock, invalid_multipart_request.c_str(), + invalid_multipart_request.length(), 0); + httplib::detail::close_socket(sock); + } + + // Give server time to process the invalid multipart data and call error + // logger + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Stop the server + svr.stop(); + server_thread.join(); + + // Verify that the server error logger was called for invalid multipart data + EXPECT_TRUE(error_logger_called); + EXPECT_EQ("POST", error_request_method); + EXPECT_EQ("/upload", error_request_path); + EXPECT_EQ(httplib::Error::Read, error_type); +} + +TEST(ServerErrorLoggingTest, ErrorLoggingOnUriTooLong) { + bool error_logger_called = false; + std::string error_request_method; + std::string error_request_path; + httplib::Error error_type; + + const int test_port = 8770; // Use a different port to avoid conflicts + + httplib::Server svr; + + // Set error logger + svr.set_error_logger([&](const Request &req, const Error &err) { + error_logger_called = true; + error_request_method = req.method; + error_request_path = req.path; + error_type = err; + }); + + // Add a GET handler + svr.Get("/.*", [](const Request & /*req*/, Response &res) { + res.set_content("OK", "text/plain"); + }); + + // Start server in a separate thread + auto server_thread = + std::thread([&]() { svr.listen("localhost", test_port); }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Create a raw socket connection to send a request with very long URI + httplib::Error error; + auto sock = httplib::detail::create_client_socket( + "localhost", std::string(), test_port, AF_UNSPEC, false, false, nullptr, + 5, 0, 0, 0, 0, 0, std::string(), error); + + if (sock != INVALID_SOCKET) { + // Send GET request with URI that exceeds CPPHTTPLIB_REQUEST_URI_MAX_LENGTH + // (8192) + std::string very_long_path(10000, 'a'); // 10KB path to exceed 8KB URI limit + std::string request = + "GET /" + very_long_path + " HTTP/1.1\r\nHost: localhost\r\n\r\n"; + + send(sock, request.c_str(), request.length(), 0); + + // Read the response to ensure server processed the request + char buffer[1024]; + recv(sock, buffer, sizeof(buffer), 0); + + httplib::detail::close_socket(sock); + } + + // Give server time to process the long URI request and call error logger + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Stop the server + svr.stop(); + server_thread.join(); + + // Verify that the server error logger was called for URI too long + EXPECT_TRUE(error_logger_called); + EXPECT_EQ("GET", error_request_method); + EXPECT_EQ(httplib::Error::Read, error_type); +} + TEST(ClientTest, ErrorLogging) { bool error_logger_called = false; std::string error_request_method; @@ -6288,7 +6685,7 @@ TEST(ClientTest, ErrorLoggingTimeout) { httplib::Client cli("10.0.0.1", 80); // Non-routable IP for timeout cli.set_connection_timeout(0, 100000); // 100ms timeout - cli.set_error_logger([&](const Request &req, const Error &err) { + cli.set_error_logger([&](const Request & /*req*/, const Error &err) { error_logger_called = true; error_type = err; }); @@ -6315,7 +6712,7 @@ TEST(ClientTest, ErrorLoggingSSL) { httplib::Client cli("https://invalid-ssl-host.example.com"); cli.set_connection_timeout(2, 0); - cli.set_error_logger([&](const Request &req, const Error &err) { + cli.set_error_logger([&](const Request & /*req*/, const Error &err) { error_logger_called = true; error_type = err; }); @@ -6345,11 +6742,11 @@ TEST(ClientTest, AccessLoggerNotCalledOnError) { cli.set_connection_timeout(1, 0); // Set both loggers - cli.set_access_logger([&](const Request &req, const Response &res) { + cli.set_access_logger([&](const Request & /*req*/, const Response & /*res*/) { access_logger_called = true; }); - cli.set_error_logger([&](const Request &req, const Error &err) { + cli.set_error_logger([&](const Request & /*req*/, const Error & /*err*/) { error_logger_called = true; }); From 73c048dda76d0862cac121046b77eda18c6873fc Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 8 Jul 2025 23:08:49 -0400 Subject: [PATCH 3/3] Fix #2111 --- httplib.h | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/httplib.h b/httplib.h index 8424a46956..88aec04afe 100644 --- a/httplib.h +++ b/httplib.h @@ -3218,6 +3218,23 @@ inline int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) { template inline ssize_t select_impl(socket_t sock, time_t sec, time_t usec) { +#ifdef __APPLE__ + if (sock >= FD_SETSIZE) { return -1; } + + fd_set fds, *rfds, *wfds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + rfds = (Read ? &fds : nullptr); + wfds = (Read ? nullptr : &fds); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + return handle_EINTR([&]() { + return select(static_cast(sock + 1), rfds, wfds, nullptr, &tv); + }); +#else struct pollfd pfd; pfd.fd = sock; pfd.events = (Read ? POLLIN : POLLOUT); @@ -3225,6 +3242,7 @@ inline ssize_t select_impl(socket_t sock, time_t sec, time_t usec) { auto timeout = static_cast(sec * 1000 + usec / 1000); return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); }); +#endif } inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) { @@ -3237,6 +3255,36 @@ inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) { inline Error wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { +#ifdef __APPLE__ + if (sock >= FD_SETSIZE) { return Error::Connection; } + + fd_set fdsr, fdsw; + FD_ZERO(&fdsr); + FD_ZERO(&fdsw); + FD_SET(sock, &fdsr); + FD_SET(sock, &fdsw); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + auto ret = handle_EINTR([&]() { + return select(static_cast(sock + 1), &fdsr, &fdsw, nullptr, &tv); + }); + + if (ret == 0) { return Error::ConnectionTimeout; } + + if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { + auto error = 0; + socklen_t len = sizeof(error); + auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&error), &len); + auto successful = res >= 0 && !error; + return successful ? Error::Success : Error::Connection; + } + + return Error::Connection; +#else struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLIN | POLLOUT; @@ -3258,6 +3306,7 @@ inline Error wait_until_socket_is_ready(socket_t sock, time_t sec, } return Error::Connection; +#endif } inline bool is_socket_alive(socket_t sock) { @@ -7894,6 +7943,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr, res.version = "HTTP/1.1"; res.headers = default_headers_; +#ifdef __APPLE__ + // Socket file descriptor exceeded FD_SETSIZE... + if (strm.socket() >= FD_SETSIZE) { + Headers dummy; + detail::read_headers(strm, dummy); + res.status = StatusCode::InternalServerError_500; + return write_response(strm, close_connection, req, res, Error::Read); + } +#endif + // Request line and headers if (!parse_request_line(line_reader.ptr(), req) || !detail::read_headers(strm, req.headers)) {