From b52d7d8411f92ab82588ce20f14559062788c71b Mon Sep 17 00:00:00 2001
From: yhirose <yhirose@users.noreply.github.com>
Date: Wed, 6 Aug 2025 17:38:18 -0400
Subject: [PATCH 1/3] 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 <atomic>
 #include <chrono>
 #include <ctime>
 #include <format>
 #include <iomanip>
 #include <iostream>
+#include <signal.h>
 #include <sstream>
 
 #include <httplib.h>
 
-constexpr auto error_html = R"(<html>
-<head><title>{} {}</title></head>
-<body>
-<center><h1>404 Not Found</h1></center>
-<hr><center>cpp-httplib/{}</center>
-</body>
-</html>
-)";
+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: {} <hostname> <port> <mount_point> "
+                           "<document_root_directory>",
+                           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(
+              "<html><head><title>404 Not Found</title></head>"
+              "<body><h1>404 Not Found</h1>"
+              "<p>The requested resource was not found on this server.</p>"
+              "<hr><p>{}</p></body></html>",
+              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<void(const Request &, const Response &)>;
 
+// Forward declaration for Error type
+enum class Error;
+using ErrorLogger = std::function<void(const Error &, const Request *)>;
+
 using SocketOptions = std::function<void(socket_t sock)>;
 
 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<bool> is_running_{false};
   std::atomic<bool> 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<SSLVerifierResponse(SSL *ssl)> 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<Rep, Period> &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<detail::mmap>(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<socklen_t>(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<struct sockaddr *>(&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<struct sockaddr_in6 *>(&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<detail::compressor> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<Response> 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<Response> 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<std::mutex> 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<std::mutex> 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<ContentReceiverWithProgress>(
@@ -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<size_t>(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<unsigned long>(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 <yuji.hirose.bug@gmail.com>
Date: Wed, 6 Aug 2025 17:41:03 -0400
Subject: [PATCH 2/3] 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::milliseconds>(
+    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 <yuji.hirose.bug@gmail.com>
Date: Wed, 6 Aug 2025 17:54:08 -0400
Subject: [PATCH 3/3] 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 .
