Compare commits

..

15 Commits

Author SHA1 Message Date
yhirose 85b18a9c64 Release v0.35.0 2026-02-27 22:14:27 -05:00
yhirose 0853ce7753 Fix #2379 2026-02-27 22:08:27 -05:00
yhirose c99d7472b5 Merge commit from fork 2026-02-27 21:36:04 -05:00
yhirose defd907c74 Merge commit from fork 2026-02-27 21:35:32 -05:00
yhirose c6c75e4c69 Remove tag-latest.yml 2026-02-23 00:01:58 -05:00
yhirose c2002f6e06 Make loading system certificates from the Keychain on macOS an opt-out feature (#2377) 2026-02-22 19:18:40 -05:00
yhirose 7c33fd47bf Release v0.34.0 2026-02-22 17:27:02 -05:00
yhirose 21243b3c9e Fix problem caused by the recent performance improvement 2026-02-22 13:02:40 -05:00
yhirose e068da4f6b Fix race condittion with logging 2026-02-22 08:46:30 -05:00
yhirose f6aec98145 Fix SocketStream.wait_writable_INET test 2026-02-22 08:39:08 -05:00
yhirose f29bb15f9d Performance improvement! 2026-02-22 07:53:23 -05:00
yhirose c53d93d145 Add make_file_body 2026-02-22 07:53:23 -05:00
yhirose b4d16a582d Update latest tag when new release 2026-02-21 23:27:16 -05:00
yhirose e61a8bcec7 Release v0.33.1 2026-02-21 08:55:06 -05:00
yhirose 33dbe00cce Fix compiple problem with C++11 compiler 2026-02-21 08:52:50 -05:00
10 changed files with 11224 additions and 11485 deletions
+9 -6
View File
@@ -10,7 +10,7 @@
* HTTPLIB_REQUIRE_ZLIB (default off)
* HTTPLIB_REQUIRE_BROTLI (default off)
* HTTPLIB_REQUIRE_ZSTD (default off)
* HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN (default on)
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
* HTTPLIB_COMPILE (default off)
* HTTPLIB_INSTALL (default on)
@@ -51,7 +51,7 @@
* HTTPLIB_IS_USING_ZLIB - a bool for if ZLIB support is enabled.
* HTTPLIB_IS_USING_BROTLI - a bool for if Brotli support is enabled.
* HTTPLIB_IS_USING_ZSTD - a bool for if ZSTD support is enabled.
* HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN - a bool for if support of loading system certs from the Apple Keychain is enabled.
* HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES - a bool for if support of loading system certs from the Apple Keychain is enabled.
* HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO - a bool for if nonblocking getaddrinfo is enabled.
* HTTPLIB_IS_COMPILED - a bool for if the library is compiled, or otherwise header-only.
* HTTPLIB_INCLUDE_DIR - the root path to httplib's header (e.g. /usr/include).
@@ -107,7 +107,7 @@ option(HTTPLIB_INSTALL "Enables the installation target" ON)
option(HTTPLIB_TEST "Enables testing and builds tests" OFF)
option(HTTPLIB_REQUIRE_BROTLI "Requires Brotli to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_BROTLI_IF_AVAILABLE "Uses Brotli (if available) to enable Brotli decompression support." ON)
option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system certs from the Apple Keychain." ON)
option(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES "Disable loading system certs from the Apple Keychain on macOS." OFF)
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
@@ -152,7 +152,10 @@ endif()
# Set some variables that are used in-tree and while building based on our options
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
set(HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN ${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN})
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES TRUE)
if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
endif()
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
@@ -316,7 +319,7 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Windows>:crypt32>
# Needed for API from MacOS Security framework
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
# Needed for non-blocking getaddrinfo on MacOS
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
# Can't put multiple targets in a single generator expression or it bugs out.
@@ -336,7 +339,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
$<$<BOOL:${HTTPLIB_IS_USING_ZLIB}>:CPPHTTPLIB_ZLIB_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_ZSTD}>:CPPHTTPLIB_ZSTD_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN}>>:CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN>
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
)
+11 -2
View File
@@ -79,7 +79,7 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()` or `load_ca_cert_store(pem, size)`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable with these backends.
> [!TIP]
> For macOS: cpp-httplib can use system certs with `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`. `CoreFoundation` and `Security` should be linked with `-framework`.
> For macOS: cpp-httplib automatically loads system certs from the Keychain when a TLS backend is enabled. `CoreFoundation` and `Security` must be linked with `-framework`. To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
```c++
// Use either OpenSSL, Mbed TLS, or wolfSSL
@@ -989,7 +989,7 @@ httplib::UploadFormDataItems items = {
auto res = cli.Post("/multipart", items);
```
To upload files from disk without loading them entirely into memory, use `make_file_provider`. The file is read and sent in chunks with a correct `Content-Length` header.
To upload files from disk without loading them entirely into memory, use `make_file_provider`. The file is sent with chunked transfer encoding.
```cpp
httplib::FormDataProviderItems providers = {
@@ -1000,6 +1000,15 @@ httplib::FormDataProviderItems providers = {
auto res = cli.Post("/upload", {}, {}, providers);
```
### POST with a file body
To POST a file as a raw binary body with `Content-Length`, use `make_file_body`.
```cpp
auto [size, provider] = httplib::make_file_body("/path/to/data.bin");
auto res = cli.Post("/upload", size, provider, "application/octet-stream");
```
### PUT
```c++
+8 -5
View File
@@ -1,4 +1,7 @@
CXXFLAGS = -std=c++11 -O2 -I..
CXXFLAGS = -O2 -I..
CPPHTTPLIB_CXXFLAGS = -std=c++11
CROW_CXXFLAGS = -std=c++17
CPPHTTPLIB_FLAGS = -DCPPHTTPLIB_THREAD_POOL_COUNT=16
@@ -18,11 +21,11 @@ run : server
@./server
server : cpp-httplib/main.cpp ../httplib.h
@g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib/main.cpp
@g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib/main.cpp
# crow
bench-crow: server-crow
@echo "-------------\n Crow v1.2.0\n-------------\n"
@echo "-------------\n Crow v1.3.1\n-------------\n"
@./server-crow & export PID=$$!; $(BENCH); kill $${PID}
@echo ""
@@ -32,8 +35,8 @@ monitor-crow: server-crow
run-crow : server-crow
@./server-crow
server-crow : crow/main.cpp
@g++ -o $@ $(CXXFLAGS) crow/main.cpp
server-crow : crow/main.cpp crow/crow_all.h
@g++ -o $@ $(CXXFLAGS) $(CROW_CXXFLAGS) crow/main.cpp
# misc
build: server server-crow
+10819 -11339
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -2,7 +2,7 @@
class CustomLogger : public crow::ILogHandler {
public:
void log(std::string, crow::LogLevel) {}
void log(const std::string &, crow::LogLevel) {}
};
int main() {
+232 -113
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.33.0"
#define CPPHTTPLIB_VERSION_NUM "0x002100"
#define CPPHTTPLIB_VERSION "0.35.0"
#define CPPHTTPLIB_VERSION_NUM "0x002300"
/*
* Platform compatibility check
@@ -357,14 +357,32 @@ using socket_t = int;
#include <any>
#endif
// On macOS with a TLS backend, enable Keychain root certificates by default
// unless the user explicitly opts out.
#if defined(__APPLE__) && \
!defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \
(defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \
defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \
defined(CPPHTTPLIB_WOLFSSL_SUPPORT))
#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#endif
#endif
// On Windows, enable Schannel certificate verification by default
// unless the user explicitly opts out.
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#define CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
#endif
#if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) || \
defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
#if TARGET_OS_MAC
#include <CFNetwork/CFHost.h>
#include <CoreFoundation/CoreFoundation.h>
#endif
#endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO or
// CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#endif
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef _WIN32
@@ -382,11 +400,11 @@ using socket_t = int;
#endif
#endif // _WIN32
#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#if TARGET_OS_MAC
#include <Security/Security.h>
#endif
#endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
#endif
#include <openssl/err.h>
#include <openssl/evp.h>
@@ -430,11 +448,11 @@ using socket_t = int;
#pragma comment(lib, "crypt32.lib")
#endif
#endif // _WIN32
#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#if TARGET_OS_MAC
#include <Security/Security.h>
#endif
#endif // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#endif
// Mbed TLS 3.x API compatibility
#if MBEDTLS_VERSION_MAJOR >= 3
@@ -473,11 +491,11 @@ using socket_t = int;
#pragma comment(lib, "crypt32.lib")
#endif
#endif // _WIN32
#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#if TARGET_OS_MAC
#include <Security/Security.h>
#endif
#endif // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#endif
#endif // CPPHTTPLIB_WOLFSSL_SUPPORT
// Define CPPHTTPLIB_SSL_ENABLED if any SSL backend is available
@@ -1042,6 +1060,32 @@ make_file_provider(const std::string &name, const std::string &filepath,
return fdp;
}
inline std::pair<size_t, ContentProvider>
make_file_body(const std::string &filepath) {
std::ifstream f(filepath, std::ios::binary | std::ios::ate);
if (!f) { return {0, ContentProvider{}}; }
auto size = static_cast<size_t>(f.tellg());
ContentProvider provider = [filepath](size_t offset, size_t length,
DataSink &sink) -> bool {
std::ifstream f(filepath, std::ios::binary);
if (!f) { return false; }
f.seekg(static_cast<std::streamoff>(offset));
if (!f.good()) { return false; }
char buf[8192];
while (length > 0) {
auto to_read = (std::min)(sizeof(buf), length);
f.read(buf, static_cast<std::streamsize>(to_read));
auto n = static_cast<size_t>(f.gcount());
if (n == 0) { break; }
if (!sink.write(buf, n)) { return false; }
length -= n;
}
return true;
};
return {size, std::move(provider)};
}
using ContentReceiverWithProgress = std::function<bool(
const char *data, size_t data_length, size_t offset, size_t total_length)>;
@@ -1356,6 +1400,7 @@ public:
virtual bool is_readable() const = 0;
virtual bool wait_readable() const = 0;
virtual bool wait_writable() const = 0;
virtual bool is_peer_alive() const { return wait_writable(); }
virtual ssize_t read(char *ptr, size_t size) = 0;
virtual ssize_t write(const char *ptr, size_t size) = 0;
@@ -2534,8 +2579,7 @@ public:
tls::ctx_t tls_context() const;
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
void enable_windows_certificate_verification(bool enabled);
#endif
@@ -2656,8 +2700,7 @@ public:
tls::ctx_t tls_context() const { return ctx_; }
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
void enable_windows_certificate_verification(bool enabled);
#endif
@@ -2689,8 +2732,7 @@ private:
std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
bool enable_windows_cert_verification_ = true;
#endif
@@ -5427,6 +5469,7 @@ public:
bool is_readable() const override;
bool wait_readable() const override;
bool wait_writable() const override;
bool is_peer_alive() const override;
ssize_t read(char *ptr, size_t size) override;
ssize_t write(const char *ptr, size_t size) override;
void get_remote_ip_and_port(std::string &ip, int &port) const override;
@@ -6337,10 +6380,46 @@ find_content_type(const std::string &path,
}
}
inline std::string
extract_media_type(const std::string &content_type,
std::map<std::string, std::string> *params = nullptr) {
// Extract type/subtype from Content-Type value (RFC 2045)
// e.g. "application/json; charset=utf-8" -> "application/json"
auto media_type = content_type;
auto semicolon_pos = media_type.find(';');
if (semicolon_pos != std::string::npos) {
auto param_str = media_type.substr(semicolon_pos + 1);
media_type = media_type.substr(0, semicolon_pos);
if (params) {
// Parse parameters: key=value pairs separated by ';'
split(param_str.data(), param_str.data() + param_str.size(), ';',
[&](const char *b, const char *e) {
std::string key;
std::string val;
split(b, e, '=', [&](const char *b2, const char *e2) {
if (key.empty()) {
key.assign(b2, e2);
} else {
val.assign(b2, e2);
}
});
if (!key.empty()) {
params->emplace(trim_copy(key), trim_double_quotes_copy(val));
}
});
}
}
// Trim whitespace from media type
return trim_copy(media_type);
}
inline bool can_compress_content_type(const std::string &content_type) {
using udl::operator""_t;
auto tag = str2tag(content_type);
auto mime_type = extract_media_type(content_type);
auto tag = str2tag(mime_type);
switch (tag) {
case "image/svg+xml"_t:
@@ -6352,7 +6431,7 @@ inline bool can_compress_content_type(const std::string &content_type) {
case "text/event-stream"_t: return false;
default: return !content_type.rfind("text/", 0);
default: return !mime_type.rfind("text/", 0);
}
}
@@ -6907,7 +6986,8 @@ inline bool is_chunked_transfer_encoding(const Headers &headers) {
template <typename T, typename U>
bool prepare_content_receiver(T &x, int &status,
ContentReceiverWithProgress receiver,
bool decompress, U callback) {
bool decompress, size_t payload_max_length,
bool &exceed_payload_max_length, U callback) {
if (decompress) {
std::string encoding = x.get_header_value("Content-Encoding");
std::unique_ptr<decompressor> decompressor;
@@ -6923,12 +7003,22 @@ bool prepare_content_receiver(T &x, int &status,
if (decompressor) {
if (decompressor->is_valid()) {
size_t decompressed_size = 0;
ContentReceiverWithProgress out = [&](const char *buf, size_t n,
size_t off, size_t len) {
return decompressor->decompress(buf, n,
[&](const char *buf2, size_t n2) {
return receiver(buf2, n2, off, len);
});
return decompressor->decompress(
buf, n, [&](const char *buf2, size_t n2) {
// Guard against zip-bomb: check
// decompressed size against limit.
if (payload_max_length > 0 &&
(decompressed_size >= payload_max_length ||
n2 > payload_max_length - decompressed_size)) {
exceed_payload_max_length = true;
return false;
}
decompressed_size += n2;
return receiver(buf2, n2, off, len);
});
};
return callback(std::move(out));
} else {
@@ -6949,11 +7039,14 @@ template <typename T>
bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
DownloadProgress progress,
ContentReceiverWithProgress receiver, bool decompress) {
bool exceed_payload_max_length = false;
return prepare_content_receiver(
x, status, std::move(receiver), decompress,
[&](const ContentReceiverWithProgress &out) {
x, status, std::move(receiver), decompress, payload_max_length,
exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) {
auto ret = true;
auto exceed_payload_max_length = false;
// Note: exceed_payload_max_length may also be set by the decompressor
// wrapper in prepare_content_receiver when the decompressed payload
// size exceeds the limit.
if (is_chunked_transfer_encoding(x.headers)) {
auto result = read_content_chunked(strm, x, payload_max_length, out);
@@ -7080,10 +7173,10 @@ inline bool write_content_with_progress(Stream &strm,
return ok;
};
data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); };
data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
while (offset < end_offset && !is_shutting_down()) {
if (!strm.wait_writable()) {
if (!strm.wait_writable() || !strm.is_peer_alive()) {
error = Error::Write;
return false;
} else if (!content_provider(offset, end_offset - offset, data_sink)) {
@@ -7095,6 +7188,11 @@ inline bool write_content_with_progress(Stream &strm,
}
}
if (offset < end_offset) { // exited due to is_shutting_down(), not completion
error = Error::Write;
return false;
}
error = Error::Success;
return true;
}
@@ -7134,12 +7232,12 @@ write_content_without_length(Stream &strm,
return ok;
};
data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); };
data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
data_sink.done = [&](void) { data_available = false; };
while (data_available && !is_shutting_down()) {
if (!strm.wait_writable()) {
if (!strm.wait_writable() || !strm.is_peer_alive()) {
return false;
} else if (!content_provider(offset, 0, data_sink)) {
return false;
@@ -7147,7 +7245,8 @@ write_content_without_length(Stream &strm,
return false;
}
}
return true;
return !data_available; // true only if done() was called, false if shutting
// down
}
template <typename T, typename U>
@@ -7183,7 +7282,7 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
return ok;
};
data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); };
data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
auto done_with_trailer = [&](const Headers *trailer) {
if (!ok) { return; }
@@ -7233,7 +7332,7 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
};
while (data_available && !is_shutting_down()) {
if (!strm.wait_writable()) {
if (!strm.wait_writable() || !strm.is_peer_alive()) {
error = Error::Write;
return false;
} else if (!content_provider(offset, 0, data_sink)) {
@@ -7245,6 +7344,11 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
}
}
if (data_available) { // exited due to is_shutting_down(), not done()
error = Error::Write;
return false;
}
error = Error::Success;
return true;
}
@@ -7358,12 +7462,11 @@ inline std::string normalize_query_string(const std::string &query) {
inline bool parse_multipart_boundary(const std::string &content_type,
std::string &boundary) {
auto boundary_keyword = "boundary=";
auto pos = content_type.find(boundary_keyword);
if (pos == std::string::npos) { return false; }
auto end = content_type.find(';', pos);
auto beg = pos + strlen(boundary_keyword);
boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg));
std::map<std::string, std::string> params;
extract_media_type(content_type, &params);
auto it = params.find("boundary");
if (it == params.end()) { return false; }
boundary = it->second;
return !boundary.empty();
}
@@ -7531,11 +7634,7 @@ inline bool parse_accept_header(const std::string &s,
}
// Remove additional parameters from media type
auto param_pos = accept_entry.media_type.find(';');
if (param_pos != std::string::npos) {
accept_entry.media_type =
trim_copy(accept_entry.media_type.substr(0, param_pos));
}
accept_entry.media_type = extract_media_type(accept_entry.media_type);
// Basic validation of media type format
if (accept_entry.media_type.empty()) {
@@ -7951,10 +8050,19 @@ make_multipart_content_provider(const UploadFormDataItems &items,
}
segs.push_back({owned.back().data(), owned.back().size()});
return [owned = std::move(owned), segs = std::move(segs)](
size_t offset, size_t length, DataSink &sink) -> bool {
struct MultipartState {
std::vector<std::string> owned;
std::vector<MultipartSegment> segs;
};
auto state = std::make_shared<MultipartState>();
state->owned = std::move(owned);
// `segs` holds raw pointers into owned strings; std::string move preserves
// the data pointer, so these pointers remain valid after the move above.
state->segs = std::move(segs);
return [state](size_t offset, size_t length, DataSink &sink) -> bool {
size_t pos = 0;
for (const auto &seg : segs) {
for (const auto &seg : state->segs) {
// Loop invariant: pos <= offset (proven by advancing pos only when
// offset - pos >= seg.size, i.e., the segment doesn't contain offset)
if (seg.size > 0 && offset - pos < seg.size) {
@@ -8404,6 +8512,7 @@ public:
bool is_readable() const override;
bool wait_readable() const override;
bool wait_writable() const override;
bool is_peer_alive() const override;
ssize_t read(char *ptr, size_t size) override;
ssize_t write(const char *ptr, size_t size) override;
void get_remote_ip_and_port(std::string &ip, int &port) const override;
@@ -9355,7 +9464,7 @@ inline size_t Request::get_param_value_count(const std::string &key) const {
inline bool Request::is_multipart_form_data() const {
const auto &content_type = get_header_value("Content-Type");
return !content_type.rfind("multipart/form-data", 0);
return detail::extract_media_type(content_type) == "multipart/form-data";
}
// Multipart FormData implementation
@@ -9827,8 +9936,11 @@ inline bool SocketStream::wait_readable() const {
}
inline bool SocketStream::wait_writable() const {
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&
is_socket_alive(sock_);
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
}
inline bool SocketStream::is_peer_alive() const {
return detail::is_socket_alive(sock_);
}
inline ssize_t SocketStream::read(char *ptr, size_t size) {
@@ -10159,7 +10271,11 @@ inline bool SSLSocketStream::wait_readable() const {
inline bool SSLSocketStream::wait_writable() const {
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&
is_socket_alive(sock_) && !tls::is_peer_closed(session_, sock_);
!tls::is_peer_closed(session_, sock_);
}
inline bool SSLSocketStream::is_peer_alive() const {
return !tls::is_peer_closed(session_, sock_);
}
inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
@@ -10683,35 +10799,33 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
if (post_routing_handler_) { post_routing_handler_(req, res); }
// Response line and headers
{
detail::BufferStream bstrm;
if (!detail::write_response_line(bstrm, res.status)) { return false; }
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
detail::BufferStream bstrm;
if (!detail::write_response_line(bstrm, res.status)) { return false; }
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
// Flush buffer
auto &data = bstrm.get_buffer();
detail::write_data(strm, data.data(), data.size());
// Combine small body with headers to reduce write syscalls
if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) {
bstrm.write(res.body.data(), res.body.size());
}
// Body
// Log before writing to avoid race condition with client-side code that
// accesses logger-captured data immediately after receiving the response.
output_log(req, res);
// Flush buffer
auto &data = bstrm.get_buffer();
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
// Streaming body
auto ret = true;
if (req.method != "HEAD") {
if (!res.body.empty()) {
if (!detail::write_data(strm, res.body.data(), res.body.size())) {
ret = false;
}
} else if (res.content_provider_) {
if (write_content_with_provider(strm, req, res, boundary, content_type)) {
res.content_provider_success_ = true;
} else {
ret = false;
}
if (req.method != "HEAD" && res.content_provider_) {
if (write_content_with_provider(strm, req, res, boundary, content_type)) {
res.content_provider_success_ = true;
} else {
ret = false;
}
}
// Log
output_log(req, res);
return ret;
}
@@ -10832,7 +10946,8 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) {
return true;
})) {
const auto &content_type = req.get_header_value("Content-Type");
if (!content_type.find("application/x-www-form-urlencoded")) {
if (detail::extract_media_type(content_type) ==
"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);
@@ -11219,45 +11334,63 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
if (detail::expect_content(req)) {
// Content reader handler
{
// Track whether the ContentReader was aborted due to the decompressed
// payload exceeding `payload_max_length_`.
// The user handler runs after the lambda returns, so we must restore the
// 413 status if the handler overwrites it.
bool content_reader_payload_too_large = false;
ContentReader reader(
[&](ContentReceiver receiver) {
auto result = read_content_with_content_receiver(
strm, req, res, std::move(receiver), nullptr, nullptr);
if (!result) { output_error_log(Error::Read, &req); }
if (!result) {
output_error_log(Error::Read, &req);
if (res.status == StatusCode::PayloadTooLarge_413) {
content_reader_payload_too_large = true;
}
}
return result;
},
[&](FormDataHeader header, ContentReceiver 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); }
if (!result) {
output_error_log(Error::Read, &req);
if (res.status == StatusCode::PayloadTooLarge_413) {
content_reader_payload_too_large = true;
}
}
return result;
});
bool dispatched = false;
if (req.method == "POST") {
if (dispatch_request_for_content_reader(
req, res, std::move(reader),
post_handlers_for_content_reader_)) {
return true;
}
dispatched = dispatch_request_for_content_reader(
req, res, std::move(reader), post_handlers_for_content_reader_);
} else if (req.method == "PUT") {
if (dispatch_request_for_content_reader(
req, res, std::move(reader),
put_handlers_for_content_reader_)) {
return true;
}
dispatched = dispatch_request_for_content_reader(
req, res, std::move(reader), put_handlers_for_content_reader_);
} else if (req.method == "PATCH") {
if (dispatch_request_for_content_reader(
req, res, std::move(reader),
patch_handlers_for_content_reader_)) {
return true;
}
dispatched = dispatch_request_for_content_reader(
req, res, std::move(reader), patch_handlers_for_content_reader_);
} else if (req.method == "DELETE") {
if (dispatch_request_for_content_reader(
req, res, std::move(reader),
delete_handlers_for_content_reader_)) {
return true;
dispatched = dispatch_request_for_content_reader(
req, res, std::move(reader), delete_handlers_for_content_reader_);
}
if (dispatched) {
if (content_reader_payload_too_large) {
// Enforce the limit: override any status the handler may have set
// and return false so the error path sends a plain 413 response.
res.status = StatusCode::PayloadTooLarge_413;
res.body.clear();
res.content_length_ = 0;
res.content_provider_ = nullptr;
return false;
}
return true;
}
}
@@ -11670,16 +11803,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
routed = true;
} else {
res.status = StatusCode::InternalServerError_500;
std::string val;
auto s = e.what();
for (size_t i = 0; s[i]; i++) {
switch (s[i]) {
case '\r': val += "\\r"; break;
case '\n': val += "\\n"; break;
default: val += s[i]; break;
}
}
res.set_header("EXCEPTION_WHAT", val);
}
} catch (...) {
if (exception_handler_) {
@@ -11688,7 +11811,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
routed = true;
} else {
res.status = StatusCode::InternalServerError_500;
res.set_header("EXCEPTION_WHAT", "UNKNOWN");
}
}
#endif
@@ -15369,8 +15491,7 @@ inline void SSLClient::set_session_verifier(
session_verifier_ = std::move(verifier);
}
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
enable_windows_cert_verification_ = enabled;
}
@@ -15528,8 +15649,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
}
}
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
// Additional Windows Schannel verification.
// This provides real-time certificate validation with Windows Update
// integration, working with both OpenSSL and MbedTLS backends.
@@ -15575,8 +15695,7 @@ inline void Client::enable_server_hostname_verification(bool enabled) {
cli_->enable_server_hostname_verification(enabled);
}
#if defined(_WIN32) && \
!defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
inline void Client::enable_windows_certificate_verification(bool enabled) {
if (is_ssl_) {
static_cast<SSLClient &>(*cli_).enable_windows_certificate_verification(
@@ -15699,7 +15818,7 @@ inline bool enumerate_windows_system_certs(Callback cb) {
}
#endif
#if defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
// Enumerate macOS Keychain certificates and call callback with DER data
template <typename Callback>
inline bool enumerate_macos_keychain_certs(Callback cb) {
+6 -4
View File
@@ -62,11 +62,13 @@ if get_option('tls').allowed()
endif
if tls_found and host_machine.system() == 'darwin'
macosx_keychain_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
if macosx_keychain_dep.found()
disable_macosx_certs = get_option('disable_macosx_automatic_root_certificates')
if disable_macosx_certs
args += '-DCPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES'
else
macosx_keychain_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation', 'Security'], required: true)
deps += macosx_keychain_dep
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
endif
endif
endif
endif
+2 -1
View File
@@ -7,7 +7,7 @@ option('tls_backend', type: 'combo', choices: ['openssl', 'mbedtls'], value: 'op
option('zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
option('brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
option('zstd', type: 'feature', value: 'auto', description: 'Enable zstd support')
option('macosx_keychain', type: 'feature', value: 'auto', description: 'Enable loading certs from the Keychain on Apple devices')
option('disable_macosx_automatic_root_certificates', type: 'boolean', value: false, description: 'Disable loading system certs from the Apple Keychain on macOS')
option('non_blocking_getaddrinfo', type: 'feature', value: 'auto', description: 'Enable asynchronous name lookup')
option('compile', type: 'boolean', value: false, description: 'Split the header into a compilable header & source file (requires python3)')
option('test', type: 'boolean', value: false, description: 'Build tests')
@@ -17,6 +17,7 @@ option('openssl', type: 'feature', deprecated: 'tls')
option('cpp-httplib_openssl', type: 'feature', deprecated: 'openssl')
option('cpp-httplib_zlib', type: 'feature', deprecated: 'zlib')
option('cpp-httplib_brotli', type: 'feature', deprecated: 'brotli')
option('macosx_keychain', type: 'feature', deprecated: true, description: 'Deprecated: use disable_macosx_automatic_root_certificates instead')
option('cpp-httplib_macosx_keychain', type: 'feature', deprecated: 'macosx_keychain')
option('cpp-httplib_non_blocking_getaddrinfo', type: 'feature', deprecated: 'non_blocking_getaddrinfo')
option('cpp-httplib_compile', type: 'boolean', value: false, deprecated: 'compile')
+3 -3
View File
@@ -7,13 +7,13 @@ ifneq ($(OS), Windows_NT)
PREFIX ?= $(shell brew --prefix)
OPENSSL_DIR = $(PREFIX)/opt/openssl@3
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
OPENSSL_SUPPORT += -framework CoreFoundation -framework Security
MBEDTLS_DIR ?= $(shell brew --prefix mbedtls@3)
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib -lmbedtls -lmbedx509 -lmbedcrypto
MBEDTLS_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
MBEDTLS_SUPPORT += -framework CoreFoundation -framework Security
WOLFSSL_DIR ?= $(shell brew --prefix wolfssl)
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -I$(WOLFSSL_DIR)/include -I$(WOLFSSL_DIR)/include/wolfssl -L$(WOLFSSL_DIR)/lib -lwolfssl
WOLFSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
WOLFSSL_SUPPORT += -framework CoreFoundation -framework Security
else
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
+133 -11
View File
@@ -323,9 +323,10 @@ TEST(SocketStream, wait_writable_UNIX) {
asSocketStream(fds[0], [&](Stream &s0) {
EXPECT_EQ(s0.socket(), fds[0]);
EXPECT_TRUE(s0.wait_writable());
EXPECT_TRUE(s0.is_peer_alive());
EXPECT_EQ(0, close(fds[1]));
EXPECT_FALSE(s0.wait_writable());
EXPECT_FALSE(s0.is_peer_alive());
return true;
});
@@ -367,7 +368,9 @@ TEST(SocketStream, wait_writable_INET) {
};
asSocketStream(disconnected_svr_sock, [&](Stream &ss) {
EXPECT_EQ(ss.socket(), disconnected_svr_sock);
EXPECT_FALSE(ss.wait_writable());
// wait_writable() returns true because select_write() only checks if the
// send buffer has space. Peer disconnection is detected later by send().
EXPECT_TRUE(ss.wait_writable());
return true;
});
@@ -2698,16 +2701,14 @@ TEST(ExceptionTest, WithoutExceptionHandler) {
auto res = cli.Get("/exception");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::InternalServerError_500, res->status);
ASSERT_TRUE(res->has_header("EXCEPTION_WHAT"));
EXPECT_EQ("exception...", res->get_header_value("EXCEPTION_WHAT"));
EXPECT_FALSE(res->has_header("EXCEPTION_WHAT"));
}
{
auto res = cli.Get("/unknown");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::InternalServerError_500, res->status);
ASSERT_TRUE(res->has_header("EXCEPTION_WHAT"));
EXPECT_EQ("exception\\r\\n...", res->get_header_value("EXCEPTION_WHAT"));
EXPECT_FALSE(res->has_header("EXCEPTION_WHAT"));
}
}
@@ -3886,6 +3887,13 @@ protected:
"01234567890123456789012345678901234567890",
"text/plain");
})
.Get("/compress-with-charset",
[&](const Request & /*req*/, Response &res) {
res.set_content(
"12345678901234567890123456789012345678901234567890123456789"
"01234567890123456789012345678901234567890",
"application/json; charset=utf-8");
})
.Get("/nocompress",
[&](const Request & /*req*/, Response &res) {
res.set_content(
@@ -4802,12 +4810,9 @@ TEST_F(ServerTest, HeaderCountSecurityTest) {
if (res) {
// If we get a response, it should be 400 Bad Request
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
} else {
// Request failed, which is the expected behavior for DoS protection
EXPECT_FALSE(res);
EXPECT_EQ("close", res->get_header_value("Connection"));
}
EXPECT_EQ("close", res->get_header_value("Connection"));
EXPECT_FALSE(cli_.is_socket_open());
}
@@ -6249,6 +6254,21 @@ TEST_F(ServerTest, Gzip) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST_F(ServerTest, GzipWithContentTypeParameters) {
Headers headers;
headers.emplace("Accept-Encoding", "gzip, deflate");
auto res = cli_.Get("/compress-with-charset", headers);
ASSERT_TRUE(res);
EXPECT_EQ("gzip", res->get_header_value("Content-Encoding"));
EXPECT_EQ("application/json; charset=utf-8",
res->get_header_value("Content-Type"));
EXPECT_EQ("123456789012345678901234567890123456789012345678901234567890123456"
"7890123456789012345678901234567890",
res->body);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST_F(ServerTest, GzipWithoutAcceptEncoding) {
Headers headers;
headers.emplace("Accept-Encoding", "");
@@ -8651,6 +8671,68 @@ TEST_F(LargePayloadMaxLengthTest, NoContentLengthExceeds10MB) {
}
}
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
// `payload_max_length` is not enforced on decompressed body in ContentReader
// path.
TEST(PayloadLimitBypassTest, StreamingGzipDecompression) {
Server svr;
const size_t LIMIT = 64 * 1024; // 64KB
svr.set_payload_max_length(LIMIT);
size_t total = 0;
svr.Post("/stream", [&](const Request & /*req*/, Response &res,
const ContentReader &content_reader) {
content_reader([&](const char * /*data*/, size_t len) {
total += len;
return true;
});
res.status = 200;
res.set_content("stream_ok", "text/plain");
});
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
// Prepare 256KB raw data and gzip-compress it
std::string raw(256 * 1024, 'A');
std::string gz;
{
z_stream zs{};
deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8,
Z_DEFAULT_STRATEGY);
zs.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(raw.data()));
zs.avail_in = static_cast<uInt>(raw.size());
char outbuf[4096];
int ret;
do {
zs.next_out = reinterpret_cast<Bytef *>(outbuf);
zs.avail_out = sizeof(outbuf);
ret = deflate(&zs, Z_FINISH);
gz.append(outbuf, sizeof(outbuf) - zs.avail_out);
} while (ret != Z_STREAM_END);
deflateEnd(&zs);
}
Client cli(HOST, PORT);
cli.set_connection_timeout(std::chrono::seconds(5));
Headers headers = {{"Content-Encoding", "gzip"}};
auto res = cli.Post("/stream", headers, gz.data(), gz.size(),
"application/octet-stream");
ASSERT_TRUE(res);
// Server must reject oversized decompressed payloads with 413.
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
// Decompressed bytes delivered to the handler must not exceed LIMIT.
EXPECT_LE(total, LIMIT);
}
#endif
// Regression test for DoS vulnerability: a malicious server sending a response
// without Content-Length header must not cause unbounded memory consumption on
// the client side. The client should stop reading after a reasonable limit,
@@ -11821,6 +11903,46 @@ TEST(MultipartFormDataTest, MakeFileProvider) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST(MakeFileBodyTest, Basic) {
const std::string file_content(4096, 'Z');
const std::string tmp_path = "/tmp/httplib_test_make_file_body.bin";
{
std::ofstream ofs(tmp_path, std::ios::binary);
ofs.write(file_content.data(),
static_cast<std::streamsize>(file_content.size()));
}
auto handled = false;
Server svr;
svr.Post("/upload", [&](const Request &req, Response &res) {
EXPECT_EQ(file_content, req.body);
handled = true;
res.status = StatusCode::OK_200;
});
auto port = svr.bind_to_any_port(HOST);
auto t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_TRUE(handled);
std::remove(tmp_path.c_str());
});
svr.wait_until_ready();
auto fb = make_file_body(tmp_path);
ASSERT_GT(fb.first, 0u);
Client cli(HOST, port);
auto res =
cli.Post("/upload", fb.first, fb.second, "application/octet-stream");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST(TaskQueueTest, IncreaseAtomicInteger) {
static constexpr unsigned int number_of_tasks{1000000};
std::atomic_uint count{0};
@@ -15620,7 +15742,7 @@ TEST(ZipBombProtectionTest, DecompressedSizeExceedsLimit) {
// Server should reject because decompressed size (8KB) exceeds limit (1KB)
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
}
#endif