Add Server::set_compress() to turn off handler response compression

A server built with compression support compressed every handler
response the client accepted, with no way to turn it off. Over HTTPS
the length of a compressed response can give away a secret in it when
the same response also carries text an attacker chooses (BREACH).
set_compress(false) sends handler responses, buffered or streamed, as
they are; compression stays on by default, and static file compression
keeps its own setting. The README and the cookbook describe the switch
and when to use it.
This commit is contained in:
yhirose committed 2026-10-21 23:34:48 -04:00
1 parent 3b8b9cf9d5
commit fc37d3eae1
5 files changed
+67 -1

No files matched your search

+2
View File
@@ -1590,6 +1590,8 @@ This holds for every kind of response, including the file-backed ones below.
`Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
Turn this compression off with `svr.set_compress(false)`. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). A server whose responses mix the two should turn compression off, or keep such secrets out of those responses. Static file compression, below, is set separately.
### Static file compression
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
});
```
## Turning it off
```cpp
svr.set_compress(false);
```
Handler responses, buffered or streamed, are then sent as they are. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). Static file compression is set separately.
## Static files need to be opted in
Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:
@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
});
```
## 圧縮を止める
```cpp
svr.set_compress(false);
```
ハンドラのレスポンスは、通常のボディもストリーミングも、そのまま送られます。HTTPSでは、攻撃者が選べる文字列とCSRFトークンなどの秘密が同じレスポンスに載ると、圧縮後の長さから秘密が漏れることがあります(BREACH攻撃)。静的ファイルの圧縮は別に設定します。
## 静的ファイルは明示的に有効にする
`set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
+10 -1
View File
@@ -2357,6 +2357,8 @@ public:
Server &set_payload_max_length(size_t length);
Server &set_compress(bool on);
Server &set_static_file_compression(bool on);
Server &set_static_file_compression_min_length(size_t length);
Server &set_static_file_compression_max_length(size_t length);
@@ -2431,6 +2433,7 @@ protected:
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
bool compress_ = true;
bool static_file_compression_ = false;
size_t static_file_compression_min_length_ =
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
@@ -13854,6 +13857,11 @@ inline Server &Server::set_payload_max_length(size_t length) {
return *this;
}
inline Server &Server::set_compress(bool on) {
compress_ = on;
return *this;
}
inline Server &Server::set_static_file_compression(bool on) {
static_file_compression_ = on;
return *this;
@@ -14882,7 +14890,8 @@ inline void Server::apply_ranges(const Request &req, Response &res,
"multipart/byteranges; boundary=" + boundary);
}
auto type = detail::encoding_type(req, res);
auto type =
compress_ ? detail::encoding_type(req, res) : detail::EncodingType::None;
if (res.body.empty()) {
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
+39
View File
@@ -14908,6 +14908,45 @@ TEST(ContentEncodingTest, PreEncodedResponseIsNotCompressedAgain) {
EXPECT_EQ("Hello World!", res->body);
}
// set_compress(false) sends every handler response as it is, buffered or
// streamed
TEST(ContentEncodingTest, SetCompressFalseSendsIdentity) {
const std::string text(1000, 'a');
Server svr;
svr.set_compress(false);
svr.Get("/buffered", [&](const Request &, Response &res) {
res.set_content(text, "text/plain");
});
svr.Get("/streamed", [&](const Request &, Response &res) {
res.set_chunked_content_provider("text/plain", [&](size_t, DataSink &sink) {
sink.write(text.data(), text.size());
sink.done();
return true;
});
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
cli.set_decompress(false);
for (auto path : {"/buffered", "/streamed"}) {
SCOPED_TRACE(path);
auto res = cli.Get(path, Headers{{"Accept-Encoding", "gzip"}});
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_FALSE(res->has_header("Content-Encoding"));
EXPECT_EQ(text, res->body);
}
}
// A chunked provider settles its coding where the headers are written and
// reuses it at write time. Both ends of that have to hold: a handler's own
// Content-Encoding suppresses the coding, and a response without one is still