From a636a094bf2110333b4ce177b79faf6844746bdc Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 5 Jul 2025 20:22:57 -0400 Subject: [PATCH] Fix #1656 --- README.md | 12 +++++ httplib.h | 9 ++++ test/test.cc | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) diff --git a/README.md b/README.md index cd933be..4cf22de 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,18 @@ svr.set_logger([](const auto& req, const auto& res) { }); ``` +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: + +```cpp +svr.set_pre_compression_logger([](const auto& req, const auto& 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. + ### Error handler ```cpp diff --git a/httplib.h b/httplib.h index c2d9860..244e055 100644 --- a/httplib.h +++ b/httplib.h @@ -1059,6 +1059,7 @@ public: Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); Server &set_logger(Logger logger); + Server &set_pre_compression_logger(Logger logger); Server &set_address_family(int family); Server &set_tcp_nodelay(bool on); @@ -1202,6 +1203,7 @@ private: Expect100ContinueHandler expect_100_continue_handler_; Logger logger_; + Logger pre_compression_logger_; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; @@ -6913,6 +6915,11 @@ inline Server &Server::set_logger(Logger logger) { return *this; } +inline Server &Server::set_pre_compression_logger(Logger logger) { + pre_compression_logger_ = std::move(logger); + return *this; +} + inline Server & Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { expect_100_continue_handler_ = std::move(handler); @@ -7647,6 +7654,8 @@ inline void Server::apply_ranges(const Request &req, Response &res, } if (type != detail::EncodingType::None) { + if (pre_compression_logger_) { pre_compression_logger_(req, res); } + std::unique_ptr compressor; std::string content_encoding; diff --git a/test/test.cc b/test/test.cc index 34a875d..86a47b7 100644 --- a/test/test.cc +++ b/test/test.cc @@ -5901,6 +5901,143 @@ TEST_F(ServerTest, PutWithContentProviderWithZstd) { EXPECT_EQ("PUT", res->body); } +// Pre-compression logging tests +TEST_F(ServerTest, PreCompressionLogging) { + // Test data for compression (matches the actual /compress endpoint content) + const std::string test_content = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + + // Variables to capture logging data + std::string pre_compression_body; + std::string pre_compression_content_type; + std::string pre_compression_content_encoding; + + std::string post_compression_body; + std::string post_compression_content_type; + std::string post_compression_content_encoding; + + // Set up pre-compression logger + svr_.set_pre_compression_logger([&](const Request &req, const Response &res) { + pre_compression_body = res.body; + pre_compression_content_type = res.get_header_value("Content-Type"); + pre_compression_content_encoding = res.get_header_value("Content-Encoding"); + }); + + // Set up post-compression logger + svr_.set_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 = res.get_header_value("Content-Encoding"); + }); + + // Test with gzip compression + Headers headers; + headers.emplace("Accept-Encoding", "gzip"); + + auto res = cli_.Get("/compress", headers); + + // Verify response was compressed + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("gzip", res->get_header_value("Content-Encoding")); + + // Verify pre-compression logger captured uncompressed content + EXPECT_EQ(test_content, pre_compression_body); + EXPECT_EQ("text/plain", pre_compression_content_type); + EXPECT_TRUE(pre_compression_content_encoding.empty()); // No encoding header before compression + + // Verify post-compression logger captured compressed content + EXPECT_NE(test_content, post_compression_body); // Should be different after compression + EXPECT_EQ("text/plain", post_compression_content_type); + EXPECT_EQ("gzip", post_compression_content_encoding); + + // Verify compressed content is smaller + EXPECT_LT(post_compression_body.size(), pre_compression_body.size()); +} + +TEST_F(ServerTest, PreCompressionLoggingWithBrotli) { + const std::string test_content = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + + 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_logger([&](const Request &req, const Response &res) { + post_compression_body = res.body; + }); + + Headers headers; + headers.emplace("Accept-Encoding", "br"); + + auto res = cli_.Get("/compress", headers); + + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("br", res->get_header_value("Content-Encoding")); + + // Verify pre-compression content is uncompressed + EXPECT_EQ(test_content, pre_compression_body); + + // Verify post-compression content is compressed + EXPECT_NE(test_content, post_compression_body); + EXPECT_LT(post_compression_body.size(), pre_compression_body.size()); +} + +TEST_F(ServerTest, PreCompressionLoggingWithoutCompression) { + const std::string test_content = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + + 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_logger([&](const Request &req, const Response &res) { + post_compression_body = res.body; + }); + + // Request without compression (use /nocompress endpoint) + Headers headers; + auto res = cli_.Get("/nocompress", headers); + + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_TRUE(res->get_header_value("Content-Encoding").empty()); + + // Pre-compression logger should not be called when no compression is applied + EXPECT_TRUE(pre_compression_body.empty()); // Pre-compression logger not called + EXPECT_EQ(test_content, post_compression_body); // Post-compression logger captures final content +} + +TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) { + const std::string test_content = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; + + std::string pre_compression_body; + 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; + }); + + Headers headers; + headers.emplace("Accept-Encoding", "gzip"); + + auto res = cli_.Get("/compress", headers); + + ASSERT_TRUE(res); + EXPECT_EQ(StatusCode::OK_200, res->status); + EXPECT_EQ("gzip", res->get_header_value("Content-Encoding")); + + // Verify pre-compression logger was called + EXPECT_TRUE(pre_logger_called); + EXPECT_EQ(test_content, pre_compression_body); +} + TEST(ZstdDecompressor, ChunkedDecompression) { std::string data; for (size_t i = 0; i < 32 * 1024; ++i) {