1
0

13 Коммитууд 2e61fd3e6e ... 1c6b3ea5a0

Эзэн SHA1 Мессеж Огноо
  yhirose 1c6b3ea5a0 Add status: "draft" to multiple documentation pages and enhance navigation sections 2 долоо хоног өмнө
  yhirose 4a1e9443ee Update deprecation messages to indicate removal in v1.0.0 2 долоо хоног өмнө
  yhirose 6f2717e623 Release v0.38.0 2 долоо хоног өмнө
  yhirose 257b266190 Add runtime configuration for WebSocket ping interval and related tests 2 долоо хоног өмнө
  yhirose ba0d0b82db Add benchmark tests and related configurations for performance evaluation 2 долоо хоног өмнө
  yhirose 5ecba74a99 Remove large data tests for GzipDecompressor and SSLClientServerTest due to memory issues 2 долоо хоног өмнө
  yhirose ec1ffbc27d Add Brotli compression support and corresponding tests 2 долоо хоног өмнө
  yhirose 4978f26f86 Fix port number in OpenStreamMalformedContentLength test to avoid conflicts 2 долоо хоног өмнө
  yhirose bb7c7ab075 Add quality parameter parsing for Accept-Encoding header and enhance encoding type selection logic 2 долоо хоног өмнө
  yhirose 1c3d35f83c Update comment to clarify requirements for safe handling in ClientImpl::handle_request 2 долоо хоног өмнө
  yhirose b1bb2b7ecc Implement setup_proxy_connection method for SSLClient and refactor proxy handling in open_stream 2 долоо хоног өмнө
  yhirose f6ed5fc60f Add SSL support for proxy connections in open_stream and corresponding test 2 долоо хоног өмнө
  yhirose 69d468f4d9 Enable BindDualStack test and remove disabled large content test due to memory issues 2 долоо хоног өмнө

+ 1 - 4
.github/workflows/test.yaml

@@ -228,7 +228,7 @@ jobs:
         for ($i = 0; $i -lt $shards; $i++) {
           $log = "shard_${i}.log"
           $procs += Start-Process -FilePath ./Release/httplib-test.exe `
-            -ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}-*BenchmarkTest*" `
+            -ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}" `
             -NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
             -Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
         }
@@ -248,9 +248,6 @@ jobs:
         }
         if ($failed) { exit 1 }
         Write-Host "All shards passed."
-    - name: Run benchmark tests with retry ${{ matrix.config.name }}
-      if: ${{ matrix.config.run_tests }}
-      run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5
 
     env:
       VCPKG_ROOT: "C:/vcpkg"

+ 79 - 0
.github/workflows/test_benchmark.yaml

@@ -0,0 +1,79 @@
+name: benchmark
+
+on:
+  push:
+  pull_request:
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  ubuntu:
+    runs-on: ubuntu-latest
+    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: build and run
+        run: cd test && make test_benchmark && ./test_benchmark
+
+  macos:
+    runs-on: macos-latest
+    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: build and run
+        run: cd test && make test_benchmark && ./test_benchmark
+
+  windows:
+    runs-on: windows-latest
+    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+    steps:
+      - name: Prepare Git for Checkout on Windows
+        run: |
+          git config --global core.autocrlf false
+          git config --global core.eol lf
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Export GitHub Actions cache environment variables
+        uses: actions/github-script@v7
+        with:
+          script: |
+            core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
+            core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
+      - name: Cache vcpkg packages
+        id: vcpkg-cache
+        uses: actions/cache@v4
+        with:
+          path: C:/vcpkg/installed
+          key: vcpkg-installed-windows-gtest
+      - name: Install vcpkg dependencies
+        if: steps.vcpkg-cache.outputs.cache-hit != 'true'
+        run: vcpkg install gtest
+      - name: Configure and build
+        shell: pwsh
+        run: |
+          $cmake_content = @"
+          cmake_minimum_required(VERSION 3.14)
+          project(httplib-benchmark CXX)
+          find_package(GTest REQUIRED)
+          add_executable(httplib-benchmark test/test_benchmark.cc)
+          target_include_directories(httplib-benchmark PRIVATE .)
+          target_link_libraries(httplib-benchmark PRIVATE GTest::gtest_main)
+          target_compile_options(httplib-benchmark PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
+          "@
+          New-Item -ItemType Directory -Force -Path build_bench/test | Out-Null
+          Set-Content -Path build_bench/CMakeLists.txt -Value $cmake_content
+          Copy-Item -Path httplib.h -Destination build_bench/
+          Copy-Item -Path test/test_benchmark.cc -Destination build_bench/test/
+          cmake -B build_bench/build -S build_bench `
+            -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
+          cmake --build build_bench/build --config Release
+      - name: Run with retry
+        run: ctest --output-on-failure --test-dir build_bench/build -C Release --repeat until-pass:5
+    env:
+      VCPKG_ROOT: "C:/vcpkg"
+      VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"

+ 1 - 0
.gitignore

@@ -51,6 +51,7 @@ test/test_split_wolfssl
 test/test_split_no_tls
 test/test_websocket_heartbeat
 test/test_thread_pool
+test/test_benchmark
 test/test.xcodeproj/xcuser*
 test/test.xcodeproj/*/xcuser*
 test/*.o

+ 2 - 2
README-stream.md

@@ -45,7 +45,7 @@ cpp-httplib provides multiple API layers for different use cases:
 
 ```text
 ┌─────────────────────────────────────────────┐
-│  SSEClient (planned)                        │  ← SSE-specific, parsed events
+│  SSEClient                        │  ← SSE-specific, parsed events
 │  - on_message(), on_event()                 │
 │  - Auto-reconnect, Last-Event-ID            │
 ├─────────────────────────────────────────────┤
@@ -61,7 +61,7 @@ cpp-httplib provides multiple API layers for different use cases:
 
 | Use Case | Recommended API |
 |----------|----------------|
-| SSE with auto-reconnect | SSEClient (planned) or `ssecli-stream.cc` example |
+| SSE with auto-reconnect | `SSEClient` (see [README-sse.md](README-sse.md)) |
 | LLM streaming (JSON Lines) | `stream::Get()` |
 | Large file download | `stream::Get()` or `open_stream()` |
 | Reverse proxy | `open_stream()` |

+ 20 - 0
README-websocket.md

@@ -353,6 +353,26 @@ if (ws.connect()) {
 | `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5`               | Timeout for waiting peer's Close response (seconds)      |
 | `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30`              | Automatic Ping interval for heartbeat (seconds)          |
 
+### Runtime Ping Interval
+
+You can override the ping interval at runtime instead of changing the compile-time macro. Set it to `0` to disable automatic pings entirely.
+
+```cpp
+// Server side
+httplib::Server svr;
+svr.set_websocket_ping_interval(10);  // 10 seconds
+
+// Or using std::chrono
+svr.set_websocket_ping_interval(std::chrono::seconds(10));
+
+// Client side
+httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
+ws.set_websocket_ping_interval(10);  // 10 seconds
+
+// Disable automatic pings
+ws.set_websocket_ping_interval(0);
+```
+
 ## Threading Model
 
 WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.

+ 1 - 1
docs-src/config.toml

@@ -4,7 +4,7 @@ langs = ["en", "ja"]
 
 [site]
 title = "cpp-httplib"
-version = "0.37.2"
+version = "0.38.0"
 hostname = "https://yhirose.github.io"
 base_path = "/cpp-httplib"
 footer_message = "© 2026 Yuji Hirose. All rights reserved."

+ 1 - 0
docs-src/pages/en/cookbook/index.md

@@ -1,6 +1,7 @@
 ---
 title: "Cookbook"
 order: 0
+status: "draft"
 ---
 
 A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need.

+ 3 - 0
docs-src/pages/en/index.md

@@ -18,5 +18,8 @@ Under the hood, it uses blocking I/O with a thread pool. It's not built for hand
 ## Documentation
 
 - [A Tour of cpp-httplib](tour/) — A step-by-step tutorial covering the basics. Start here if you're new
+
+## Stay Tuned
+
 - [Cookbook](cookbook/) — A collection of recipes organized by topic. Jump to whatever you need
 - [Building a Desktop LLM App](llm-app/) — A hands-on guide to building a desktop app with llama.cpp, step by step

+ 1 - 0
docs-src/pages/en/llm-app/index.md

@@ -1,6 +1,7 @@
 ---
 title: "Building a Desktop LLM App with cpp-httplib"
 order: 0
+status: "draft"
 ---
 
 Build an LLM-powered translation desktop app step by step, learning both the server and client sides of cpp-httplib along the way. Translation is just an example — swap it out to build your own summarizer, code generator, chatbot, or any other LLM application.

+ 1 - 0
docs-src/pages/ja/cookbook/index.md

@@ -1,6 +1,7 @@
 ---
 title: "Cookbook"
 order: 0
+status: "draft"
 ---
 
 「〇〇をするには?」という問いに答えるレシピ集です。各レシピは独立しているので、必要なページだけ読めます。

+ 3 - 0
docs-src/pages/ja/index.md

@@ -18,5 +18,8 @@ HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバ
 ## ドキュメント
 
 - [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
+
+## お楽しみに
+
 - [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
 - [Building a Desktop LLM App](llm-app/) — llama.cpp を組み込んだデスクトップアプリを段階的に構築する実践ガイド

+ 1 - 0
docs-src/pages/ja/llm-app/index.md

@@ -1,6 +1,7 @@
 ---
 title: "Building a Desktop LLM App with cpp-httplib"
 order: 0
+status: "draft"
 ---
 
 llama.cpp を組み込んだ LLM 翻訳デスクトップアプリを段階的に構築しながら、cpp-httplib のサーバー・クライアント両面の使い方を実践的に学びます。翻訳は一例であり、この部分を差し替えることで要約・コード生成・チャットボットなど自分のアプリに応用できます。

+ 259 - 137
httplib.h

@@ -8,8 +8,8 @@
 #ifndef CPPHTTPLIB_HTTPLIB_H
 #define CPPHTTPLIB_HTTPLIB_H
 
-#define CPPHTTPLIB_VERSION "0.37.2"
-#define CPPHTTPLIB_VERSION_NUM "0x002502"
+#define CPPHTTPLIB_VERSION "0.38.0"
+#define CPPHTTPLIB_VERSION_NUM "0x002600"
 
 #ifdef _WIN32
 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -1670,6 +1670,11 @@ public:
 
   Server &set_payload_max_length(size_t length);
 
+  Server &set_websocket_ping_interval(time_t sec);
+  template <class Rep, class Period>
+  Server &set_websocket_ping_interval(
+      const std::chrono::duration<Rep, Period> &duration);
+
   bool bind_to_port(const std::string &host, int port, int socket_flags = 0);
   int bind_to_any_port(const std::string &host, int socket_flags = 0);
   bool listen_after_bind();
@@ -1704,6 +1709,8 @@ 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;
+  time_t websocket_ping_interval_sec_ =
+      CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
 
 private:
   using Handlers =
@@ -1879,7 +1886,8 @@ private:
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
-  [[deprecated("Use ssl_backend_error() instead")]]
+  [[deprecated("Use ssl_backend_error() instead. "
+               "This function will be removed by v1.0.0.")]]
   uint64_t ssl_openssl_error() const {
     return ssl_backend_error_;
   }
@@ -2191,6 +2199,10 @@ protected:
 
   virtual bool create_and_connect_socket(Socket &socket, Error &error);
   virtual bool ensure_socket_connection(Socket &socket, Error &error);
+  virtual bool setup_proxy_connection(
+      Socket &socket,
+      std::chrono::time_point<std::chrono::steady_clock> start_time,
+      Response &res, bool &success, Error &error);
 
   // All of:
   //   shutdown_ssl
@@ -2355,13 +2367,16 @@ protected:
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
-  [[deprecated("Use load_ca_cert_store() instead")]]
+  [[deprecated("Use load_ca_cert_store() instead. "
+               "This function will be removed by v1.0.0.")]]
   void set_ca_cert_store(X509_STORE *ca_cert_store);
 
-  [[deprecated("Use tls::create_ca_store() instead")]]
+  [[deprecated("Use tls::create_ca_store() instead. "
+               "This function will be removed by v1.0.0.")]]
   X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size) const;
 
-  [[deprecated("Use set_server_certificate_verifier(VerifyCallback) instead")]]
+  [[deprecated("Use set_server_certificate_verifier(VerifyCallback) instead. "
+               "This function will be removed by v1.0.0.")]]
   virtual void set_server_certificate_verifier(
       std::function<SSLVerifierResponse(SSL *ssl)> verifier);
 #endif
@@ -2590,14 +2605,17 @@ private:
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
-  [[deprecated("Use tls_context() instead")]]
+  [[deprecated("Use tls_context() instead. "
+               "This function will be removed by v1.0.0.")]]
   SSL_CTX *ssl_context() const;
 
-  [[deprecated("Use set_session_verifier(session_t) instead")]]
+  [[deprecated("Use set_session_verifier(session_t) instead. "
+               "This function will be removed by v1.0.0.")]]
   void set_server_certificate_verifier(
       std::function<SSLVerifierResponse(SSL *ssl)> verifier);
 
-  [[deprecated("Use Result::ssl_backend_error() instead")]]
+  [[deprecated("Use Result::ssl_backend_error() instead. "
+               "This function will be removed by v1.0.0.")]]
   long get_verify_result() const;
 #endif
 };
@@ -2649,18 +2667,22 @@ private:
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
   [[deprecated("Use SSLServer(PemMemory) or "
-               "SSLServer(ContextSetupCallback) instead")]]
+               "SSLServer(ContextSetupCallback) instead. "
+               "This constructor will be removed by v1.0.0.")]]
   SSLServer(X509 *cert, EVP_PKEY *private_key,
             X509_STORE *client_ca_cert_store = nullptr);
 
-  [[deprecated("Use SSLServer(ContextSetupCallback) instead")]]
+  [[deprecated("Use SSLServer(ContextSetupCallback) instead. "
+               "This constructor will be removed by v1.0.0.")]]
   SSLServer(
       const std::function<bool(SSL_CTX &ssl_ctx)> &setup_ssl_ctx_callback);
 
-  [[deprecated("Use tls_context() instead")]]
+  [[deprecated("Use tls_context() instead. "
+               "This function will be removed by v1.0.0.")]]
   SSL_CTX *ssl_context() const;
 
-  [[deprecated("Use update_certs_pem() instead")]]
+  [[deprecated("Use update_certs_pem() instead. "
+               "This function will be removed by v1.0.0.")]]
   void update_certs(X509 *cert, EVP_PKEY *private_key,
                     X509_STORE *client_ca_cert_store = nullptr);
 #endif
@@ -2717,6 +2739,10 @@ private:
                  std::function<bool(Stream &strm)> callback) override;
   bool is_ssl() const override;
 
+  bool setup_proxy_connection(
+      Socket &socket,
+      std::chrono::time_point<std::chrono::steady_clock> start_time,
+      Response &res, bool &success, Error &error) override;
   bool connect_with_proxy(
       Socket &sock,
       std::chrono::time_point<std::chrono::steady_clock> start_time,
@@ -2741,18 +2767,22 @@ private:
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
-  [[deprecated("Use SSLClient(host, port, PemMemory) instead")]]
+  [[deprecated("Use SSLClient(host, port, PemMemory) instead. "
+               "This constructor will be removed by v1.0.0.")]]
   explicit SSLClient(const std::string &host, int port, X509 *client_cert,
                      EVP_PKEY *client_key,
                      const std::string &private_key_password = std::string());
 
-  [[deprecated("Use Result::ssl_backend_error() instead")]]
+  [[deprecated("Use Result::ssl_backend_error() instead. "
+               "This function will be removed by v1.0.0.")]]
   long get_verify_result() const;
 
-  [[deprecated("Use tls_context() instead")]]
+  [[deprecated("Use tls_context() instead. "
+               "This function will be removed by v1.0.0.")]]
   SSL_CTX *ssl_context() const;
 
-  [[deprecated("Use set_session_verifier(session_t) instead")]]
+  [[deprecated("Use set_session_verifier(session_t) instead. "
+               "This function will be removed by v1.0.0.")]]
   void set_server_certificate_verifier(
       std::function<SSLVerifierResponse(SSL *ssl)> verifier) override;
 
@@ -3721,15 +3751,19 @@ private:
   friend class httplib::Server;
   friend class WebSocketClient;
 
-  WebSocket(Stream &strm, const Request &req, bool is_server)
-      : strm_(strm), req_(req), is_server_(is_server) {
+  WebSocket(
+      Stream &strm, const Request &req, bool is_server,
+      time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND)
+      : strm_(strm), req_(req), is_server_(is_server),
+        ping_interval_sec_(ping_interval_sec) {
     start_heartbeat();
   }
 
-  WebSocket(std::unique_ptr<Stream> &&owned_strm, const Request &req,
-            bool is_server)
+  WebSocket(
+      std::unique_ptr<Stream> &&owned_strm, const Request &req, bool is_server,
+      time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND)
       : strm_(*owned_strm), owned_strm_(std::move(owned_strm)), req_(req),
-        is_server_(is_server) {
+        is_server_(is_server), ping_interval_sec_(ping_interval_sec) {
     start_heartbeat();
   }
 
@@ -3740,6 +3774,7 @@ private:
   std::unique_ptr<Stream> owned_strm_;
   Request req_;
   bool is_server_;
+  time_t ping_interval_sec_;
   std::atomic<bool> closed_{false};
   std::mutex write_mutex_;
   std::thread ping_thread_;
@@ -3768,6 +3803,7 @@ public:
   const std::string &subprotocol() const;
   void set_read_timeout(time_t sec, time_t usec = 0);
   void set_write_timeout(time_t sec, time_t usec = 0);
+  void set_websocket_ping_interval(time_t sec);
 
 #ifdef CPPHTTPLIB_SSL_ENABLED
   void set_ca_cert_path(const std::string &path);
@@ -3791,6 +3827,8 @@ private:
   time_t read_timeout_usec_ = 0;
   time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
   time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
+  time_t websocket_ping_interval_sec_ =
+      CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
 
 #ifdef CPPHTTPLIB_SSL_ENABLED
   bool is_ssl_ = false;
@@ -6463,33 +6501,114 @@ inline bool can_compress_content_type(const std::string &content_type) {
   }
 }
 
+inline bool parse_quality(const char *b, const char *e, std::string &token,
+                          double &quality) {
+  quality = 1.0;
+  token.clear();
+
+  // Split on first ';': left = token name, right = parameters
+  const char *params_b = nullptr;
+  std::size_t params_len = 0;
+
+  divide(
+      b, static_cast<std::size_t>(e - b), ';',
+      [&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) {
+        auto r = trim(lb, lb + llen, 0, llen);
+        if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); }
+        params_b = rb;
+        params_len = rlen;
+      });
+
+  if (token.empty()) { return false; }
+  if (params_len == 0) { return true; }
+
+  // Scan parameters for q= (stops on first match)
+  bool invalid = false;
+  split_find(params_b, params_b + params_len, ';',
+             (std::numeric_limits<size_t>::max)(),
+             [&](const char *pb, const char *pe) -> bool {
+               // Match exactly "q=" or "Q=" (not "query=" etc.)
+               auto len = static_cast<size_t>(pe - pb);
+               if (len < 2) { return false; }
+               if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') {
+                 return false;
+               }
+
+               // Trim the value portion
+               auto r = trim(pb, pe, 2, len);
+               if (r.first >= r.second) {
+                 invalid = true;
+                 return true;
+               }
+
+               double v = 0.0;
+               auto res = from_chars(pb + r.first, pb + r.second, v);
+               if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
+                 invalid = true;
+                 return true;
+               }
+               quality = v;
+               return true;
+             });
+
+  return !invalid;
+}
+
 inline EncodingType encoding_type(const Request &req, const Response &res) {
-  auto ret =
-      detail::can_compress_content_type(res.get_header_value("Content-Type"));
-  if (!ret) { return EncodingType::None; }
+  if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
+    return EncodingType::None;
+  }
 
   const auto &s = req.get_header_value("Accept-Encoding");
-  (void)(s);
+  if (s.empty()) { return EncodingType::None; }
 
+  // Single-pass: iterate tokens and track the best supported encoding.
+  // Server preference breaks ties (br > gzip > zstd).
+  EncodingType best = EncodingType::None;
+  double best_q = 0.0; // q=0 means "not acceptable"
+
+  // Server preference: Brotli > Gzip > Zstd (lower = more preferred)
+  auto priority = [](EncodingType t) -> int {
+    switch (t) {
+    case EncodingType::Brotli: return 0;
+    case EncodingType::Gzip: return 1;
+    case EncodingType::Zstd: return 2;
+    default: return 3;
+    }
+  };
+
+  std::string name;
+  split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
+    double quality = 1.0;
+    if (!parse_quality(b, e, name, quality)) { return; }
+    if (quality <= 0.0) { return; }
+
+    EncodingType type = EncodingType::None;
 #ifdef CPPHTTPLIB_BROTLI_SUPPORT
-  // TODO: 'Accept-Encoding' has br, not br;q=0
-  ret = s.find("br") != std::string::npos;
-  if (ret) { return EncodingType::Brotli; }
+    if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; }
 #endif
-
 #ifdef CPPHTTPLIB_ZLIB_SUPPORT
-  // TODO: 'Accept-Encoding' has gzip, not gzip;q=0
-  ret = s.find("gzip") != std::string::npos;
-  if (ret) { return EncodingType::Gzip; }
+    if (type == EncodingType::None && case_ignore::equal(name, "gzip")) {
+      type = EncodingType::Gzip;
+    }
 #endif
-
 #ifdef CPPHTTPLIB_ZSTD_SUPPORT
-  // TODO: 'Accept-Encoding' has zstd, not zstd;q=0
-  ret = s.find("zstd") != std::string::npos;
-  if (ret) { return EncodingType::Zstd; }
+    if (type == EncodingType::None && case_ignore::equal(name, "zstd")) {
+      type = EncodingType::Zstd;
+    }
 #endif
 
-  return EncodingType::None;
+    if (type == EncodingType::None) { return; }
+
+    // Higher q-value wins; for equal q, server preference breaks ties
+    if (quality > best_q ||
+        (quality == best_q && priority(type) < priority(best))) {
+      best_q = quality;
+      best = type;
+    }
+  });
+
+  return best;
 }
 
 inline bool nocompressor::compress(const char *data, size_t data_length,
@@ -6773,6 +6892,21 @@ create_decompressor(const std::string &encoding) {
   return decompressor;
 }
 
+// Returns the best available compressor and its Content-Encoding name.
+// Priority: Brotli > Gzip > Zstd (matches server-side preference).
+inline std::pair<std::unique_ptr<compressor>, const char *>
+create_compressor() {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+  return {detail::make_unique<brotli_compressor>(), "br"};
+#elif defined(CPPHTTPLIB_ZLIB_SUPPORT)
+  return {detail::make_unique<gzip_compressor>(), "gzip"};
+#elif defined(CPPHTTPLIB_ZSTD_SUPPORT)
+  return {detail::make_unique<zstd_compressor>(), "zstd"};
+#else
+  return {nullptr, nullptr};
+#endif
+}
+
 inline bool is_prohibited_header_name(const std::string &name) {
   using udl::operator""_t;
 
@@ -7605,7 +7739,7 @@ inline bool parse_accept_header(const std::string &s,
   struct AcceptEntry {
     std::string media_type;
     double quality;
-    int order; // Original order in header
+    int order;
   };
 
   std::vector<AcceptEntry> entries;
@@ -7623,48 +7757,12 @@ inline bool parse_accept_header(const std::string &s,
     }
 
     AcceptEntry accept_entry;
-    accept_entry.quality = 1.0; // Default quality
     accept_entry.order = order++;
 
-    // Find q= parameter
-    auto q_pos = entry.find(";q=");
-    if (q_pos == std::string::npos) { q_pos = entry.find("; q="); }
-
-    if (q_pos != std::string::npos) {
-      // Extract media type (before q parameter)
-      accept_entry.media_type = trim_copy(entry.substr(0, q_pos));
-
-      // Extract quality value
-      auto q_start = entry.find('=', q_pos) + 1;
-      auto q_end = entry.find(';', q_start);
-      if (q_end == std::string::npos) { q_end = entry.length(); }
-
-      std::string quality_str =
-          trim_copy(entry.substr(q_start, q_end - q_start));
-      if (quality_str.empty()) {
-        has_invalid_entry = true;
-        return;
-      }
-
-      {
-        double v = 0.0;
-        auto res = detail::from_chars(
-            quality_str.data(), quality_str.data() + quality_str.size(), v);
-        if (res.ec == std::errc{}) {
-          accept_entry.quality = v;
-        } else {
-          has_invalid_entry = true;
-          return;
-        }
-      }
-      // Check if quality is in valid range [0.0, 1.0]
-      if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {
-        has_invalid_entry = true;
-        return;
-      }
-    } else {
-      // No quality parameter, use entire entry as media type
-      accept_entry.media_type = entry;
+    if (!parse_quality(entry.data(), entry.data() + entry.size(),
+                       accept_entry.media_type, accept_entry.quality)) {
+      has_invalid_entry = true;
+      return;
     }
 
     // Remove additional parameters from media type
@@ -10746,6 +10844,20 @@ inline Server &Server::set_payload_max_length(size_t length) {
   return *this;
 }
 
+inline Server &Server::set_websocket_ping_interval(time_t sec) {
+  websocket_ping_interval_sec_ = sec;
+  return *this;
+}
+
+template <class Rep, class Period>
+inline Server &Server::set_websocket_ping_interval(
+    const std::chrono::duration<Rep, Period> &duration) {
+  detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) {
+    set_websocket_ping_interval(sec);
+  });
+  return *this;
+}
+
 inline bool Server::bind_to_port(const std::string &host, int port,
                                  int socket_flags) {
   auto ret = bind_internal(host, port, socket_flags);
@@ -11896,7 +12008,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
         {
           // Use WebSocket-specific read timeout instead of HTTP timeout
           strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
-          ws::WebSocket ws(strm, req, true);
+          ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_);
           entry.handler(req, ws);
         }
         return true;
@@ -12140,6 +12252,13 @@ inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
   return create_and_connect_socket(socket, error);
 }
 
+inline bool ClientImpl::setup_proxy_connection(
+    Socket & /*socket*/,
+    std::chrono::time_point<std::chrono::steady_clock> /*start_time*/,
+    Response & /*res*/, bool & /*success*/, Error & /*error*/) {
+  return true;
+}
+
 inline void ClientImpl::shutdown_ssl(Socket & /*socket*/,
                                      bool /*shutdown_gracefully*/) {
   // If there are any requests in flight from threads other than us, then it's
@@ -12261,27 +12380,14 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
         return false;
       }
 
-#ifdef CPPHTTPLIB_SSL_ENABLED
-      // TODO: refactoring
-      if (is_ssl()) {
-        auto &scli = static_cast<SSLClient &>(*this);
-        if (!proxy_host_.empty() && proxy_port_ != -1) {
-          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 (!proxy_host_.empty() && proxy_port_ != -1) {
-          if (!scli.initialize_ssl(socket_, error)) {
-            output_error_log(error, &req);
-            return false;
-          }
+      {
+        auto success = true;
+        if (!setup_proxy_connection(socket_, req.start_time_, res, success,
+                                    error)) {
+          if (!success) { output_error_log(error, &req); }
+          return success;
         }
       }
-#endif
     }
 
     // Mark the current socket as being in use so that it cannot be closed by
@@ -12442,17 +12548,15 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
         return handle;
       }
 
-#ifdef CPPHTTPLIB_SSL_ENABLED
-      if (is_ssl()) {
-        auto &scli = static_cast<SSLClient &>(*this);
-        if (!proxy_host_.empty() && proxy_port_ != -1) {
-          if (!scli.initialize_ssl(socket_, handle.error)) {
-            handle.response.reset();
-            return handle;
-          }
+      {
+        auto success = true;
+        auto start_time = std::chrono::steady_clock::now();
+        if (!setup_proxy_connection(socket_, start_time, *handle.response,
+                                    success, handle.error)) {
+          if (!success) { handle.response.reset(); }
+          return handle;
         }
       }
-#endif
     }
 
     transfer_socket_ownership_to_handle(handle);
@@ -12731,7 +12835,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
 
   if (res.get_header_value("Connection") == "close" ||
       (res.version == "HTTP/1.0" && res.reason != "Connection established")) {
-    // TODO this requires a not-entirely-obvious chain of calls to be correct
+    // NOTE: this requires a not-entirely-obvious chain of calls to be correct
     // for this to be safe.
 
     // This is safe to call because handle_request is only called by send_
@@ -12970,14 +13074,9 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
   auto is_shutting_down = []() { return false; };
 
   if (req.is_chunked_content_provider_) {
-    // TODO: Brotli support
-    std::unique_ptr<detail::compressor> compressor;
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
-    if (compress_) {
-      compressor = detail::make_unique<detail::gzip_compressor>();
-    } else
-#endif
-    {
+    auto compressor = compress_ ? detail::create_compressor().first
+                                : std::unique_ptr<detail::compressor>();
+    if (!compressor) {
       compressor = detail::make_unique<detail::nocompressor>();
     }
 
@@ -13208,14 +13307,15 @@ ClientImpl::send_with_content_provider_and_receiver(
     Error &error) {
   if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
 
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
-  if (compress_) { req.set_header("Content-Encoding", "gzip"); }
-#endif
+  auto enc = compress_
+                 ? detail::create_compressor()
+                 : std::pair<std::unique_ptr<detail::compressor>, const char *>(
+                       nullptr, nullptr);
 
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
-  if (compress_ && !content_provider_without_length) {
-    // TODO: Brotli support
-    detail::gzip_compressor compressor;
+  if (enc.second) { req.set_header("Content-Encoding", enc.second); }
+
+  if (enc.first && !content_provider_without_length) {
+    auto &compressor = enc.first;
 
     if (content_provider) {
       auto ok = true;
@@ -13226,7 +13326,7 @@ ClientImpl::send_with_content_provider_and_receiver(
         if (ok) {
           auto last = offset + data_len == content_length;
 
-          auto ret = compressor.compress(
+          auto ret = compressor->compress(
               data, data_len, last,
               [&](const char *compressed_data, size_t compressed_data_len) {
                 req.body.append(compressed_data, compressed_data_len);
@@ -13250,19 +13350,17 @@ ClientImpl::send_with_content_provider_and_receiver(
         }
       }
     } else {
-      if (!compressor.compress(body, content_length, true,
-                               [&](const char *data, size_t data_len) {
-                                 req.body.append(data, data_len);
-                                 return true;
-                               })) {
+      if (!compressor->compress(body, content_length, true,
+                                [&](const char *data, size_t data_len) {
+                                  req.body.append(data, data_len);
+                                  return true;
+                                })) {
         error = Error::Compression;
         output_error_log(error, &req);
         return nullptr;
       }
     }
-  } else
-#endif
-  {
+  } else {
     if (content_provider) {
       req.content_length_ = content_length;
       req.content_provider_ = std::move(content_provider);
@@ -15177,7 +15275,8 @@ inline void Client::set_follow_location(bool on) {
 
 inline void Client::set_path_encode(bool on) { cli_->set_path_encode(on); }
 
-[[deprecated("Use set_path_encode instead")]]
+[[deprecated("Use set_path_encode() instead. "
+             "This function will be removed by v1.0.0.")]]
 inline void Client::set_url_encode(bool on) {
   cli_->set_path_encode(on);
 }
@@ -15429,6 +15528,24 @@ inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) {
   return ClientImpl::create_and_connect_socket(socket, error);
 }
 
+inline bool SSLClient::setup_proxy_connection(
+    Socket &socket,
+    std::chrono::time_point<std::chrono::steady_clock> start_time,
+    Response &res, bool &success, Error &error) {
+  if (proxy_host_.empty() || proxy_port_ == -1) { return true; }
+
+  if (!connect_with_proxy(socket, start_time, res, success, error)) {
+    return false;
+  }
+
+  if (!initialize_ssl(socket, error)) {
+    success = false;
+    return false;
+  }
+
+  return true;
+}
+
 // Assumes that socket_mutex_ is locked and that there are no requests in
 // flight
 inline bool SSLClient::connect_with_proxy(
@@ -19945,11 +20062,11 @@ inline WebSocket::~WebSocket() {
 }
 
 inline void WebSocket::start_heartbeat() {
+  if (ping_interval_sec_ == 0) { return; }
   ping_thread_ = std::thread([this]() {
     std::unique_lock<std::mutex> lock(ping_mutex_);
     while (!closed_) {
-      ping_cv_.wait_for(lock, std::chrono::seconds(
-                                  CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND));
+      ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
       if (closed_) { break; }
       lock.unlock();
       if (!send_frame(Opcode::Ping, nullptr, 0)) {
@@ -20087,7 +20204,8 @@ inline bool WebSocketClient::connect() {
   Request req;
   req.method = "GET";
   req.path = path_;
-  ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false));
+  ws_ = std::unique_ptr<WebSocket>(
+      new WebSocket(std::move(strm), req, false, websocket_ping_interval_sec_));
   return true;
 }
 
@@ -20127,6 +20245,10 @@ inline void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
   write_timeout_usec_ = usec;
 }
 
+inline void WebSocketClient::set_websocket_ping_interval(time_t sec) {
+  websocket_ping_interval_sec_ = sec;
+}
+
 #ifdef CPPHTTPLIB_SSL_ENABLED
 
 inline void WebSocketClient::set_ca_cert_path(const std::string &path) {

+ 1 - 0
justfile

@@ -36,6 +36,7 @@ others:
     @(cd test && make fuzz_test)
     @(cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat)
     @(cd test && make test_thread_pool && ./test_thread_pool)
+    @(cd test && make test_benchmark && ./test_benchmark)
 
 build:
     @(cd test && make test_split)

+ 11 - 1
test/Makefile

@@ -219,6 +219,16 @@ style_check: $(STYLE_CHECK_FILES)
 		echo "All files are properly formatted."; \
 	fi
 
+BENCHMARK_LIBS = -lpthread
+ifneq ($(OS), Windows_NT)
+	ifeq ($(shell uname -s), Darwin)
+		BENCHMARK_LIBS += -framework CoreFoundation -framework CFNetwork
+	endif
+endif
+
+test_benchmark : test_benchmark.cc ../httplib.h Makefile
+	$(CXX) -o $@ -I.. $(CXXFLAGS) test_benchmark.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(BENCHMARK_LIBS)
+
 test_websocket_heartbeat : test_websocket_heartbeat.cc ../httplib.h Makefile
 	$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
 	@file $@
@@ -254,5 +264,5 @@ cert.pem:
 	./gen-certs.sh
 
 clean:
-	rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
+	rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl test_benchmark server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
 

+ 133 - 189
test/test.cc

@@ -102,71 +102,6 @@ static void read_file(const std::string &path, std::string &out) {
   fs.read(&out[0], static_cast<std::streamsize>(size));
 }
 
-void performance_test(const char *host) {
-  Server svr;
-
-  svr.Get("/benchmark", [&](const Request & /*req*/, Response &res) {
-    res.set_content("Benchmark Response", "text/plain");
-  });
-
-  auto listen_thread = std::thread([&]() { svr.listen(host, PORT); });
-  auto se = detail::scope_exit([&] {
-    svr.stop();
-    listen_thread.join();
-    ASSERT_FALSE(svr.is_running());
-  });
-
-  svr.wait_until_ready();
-
-  Client cli(host, PORT);
-
-  // Warm-up request to establish connection and resolve DNS
-  auto warmup_res = cli.Get("/benchmark");
-  ASSERT_TRUE(warmup_res); // Ensure server is responding correctly
-
-  // Run multiple trials and collect timings
-  const int num_trials = 20;
-  std::vector<int64_t> timings;
-  timings.reserve(num_trials);
-
-  for (int i = 0; i < num_trials; i++) {
-    auto start = std::chrono::high_resolution_clock::now();
-    auto res = cli.Get("/benchmark");
-    auto end = std::chrono::high_resolution_clock::now();
-
-    auto elapsed =
-        std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
-            .count();
-
-    // Assertions after timing measurement to avoid overhead
-    ASSERT_TRUE(res);
-    EXPECT_EQ(StatusCode::OK_200, res->status);
-
-    timings.push_back(elapsed);
-  }
-
-  // Calculate 25th percentile (lower quartile)
-  std::sort(timings.begin(), timings.end());
-  auto p25 = timings[num_trials / 4];
-
-  // Format timings for output
-  std::ostringstream timings_str;
-  timings_str << "[";
-  for (size_t i = 0; i < timings.size(); i++) {
-    if (i > 0) timings_str << ", ";
-    timings_str << timings[i];
-  }
-  timings_str << "]";
-
-  // Localhost HTTP GET should be fast even in CI environments
-  EXPECT_LE(p25, 5) << "25th percentile performance is too slow: " << p25
-                    << "ms (Issue #1777). Timings: " << timings_str.str();
-}
-
-TEST(BenchmarkTest, localhost) { performance_test("localhost"); }
-
-TEST(BenchmarkTest, v6) { performance_test("::1"); }
-
 class UnixSocketTest : public ::testing::Test {
 protected:
   void TearDown() override { std::remove(pathname_.c_str()); }
@@ -1267,6 +1202,89 @@ TEST(ParseAcceptEncoding3, AcceptEncoding) {
 #endif
 }
 
+TEST(ParseAcceptEncoding4, AcceptEncodingQZero) {
+  // All supported encodings rejected with q=0 should return None
+  Request req;
+  req.set_header("Accept-Encoding", "gzip;q=0, br;q=0, zstd;q=0, deflate");
+
+  Response res;
+  res.set_header("Content-Type", "text/plain");
+
+  auto ret = detail::encoding_type(req, res);
+
+  EXPECT_TRUE(ret == detail::EncodingType::None);
+}
+
+TEST(ParseAcceptEncoding5, AcceptEncodingQZeroVariants) {
+  // q=0.0, q=0.00, q=0.000 should also be treated as rejected
+  Request req;
+  req.set_header("Accept-Encoding", "gzip;q=0.000, br;q=0.0, zstd;q=0.00");
+
+  Response res;
+  res.set_header("Content-Type", "text/plain");
+
+  auto ret = detail::encoding_type(req, res);
+
+  EXPECT_TRUE(ret == detail::EncodingType::None);
+}
+
+TEST(ParseAcceptEncoding6, AcceptEncodingXGzipQZero) {
+  // x-gzip;q=0 should not cause "gzip" to be incorrectly detected
+  Request req;
+  req.set_header("Accept-Encoding", "x-gzip;q=0");
+
+  Response res;
+  res.set_header("Content-Type", "text/plain");
+
+  auto ret = detail::encoding_type(req, res);
+
+  EXPECT_TRUE(ret == detail::EncodingType::None);
+}
+
+TEST(ParseAcceptEncoding7, AcceptEncodingCaseInsensitive) {
+  // RFC 7231: Accept-Encoding values are case-insensitive
+  Request req;
+  req.set_header("Accept-Encoding", "GZIP, BR, ZSTD");
+
+  Response res;
+  res.set_header("Content-Type", "text/plain");
+
+  auto ret = detail::encoding_type(req, res);
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+  EXPECT_TRUE(ret == detail::EncodingType::Brotli);
+#elif CPPHTTPLIB_ZLIB_SUPPORT
+  EXPECT_TRUE(ret == detail::EncodingType::Gzip);
+#elif CPPHTTPLIB_ZSTD_SUPPORT
+  EXPECT_TRUE(ret == detail::EncodingType::Zstd);
+#else
+  EXPECT_TRUE(ret == detail::EncodingType::None);
+#endif
+}
+
+TEST(ParseAcceptEncoding8, AcceptEncodingQValuePriority) {
+  // q value should determine priority, not hardcoded order
+  Request req;
+  req.set_header("Accept-Encoding", "br;q=0.5, gzip;q=1.0, zstd;q=0.8");
+
+  Response res;
+  res.set_header("Content-Type", "text/plain");
+
+  auto ret = detail::encoding_type(req, res);
+
+  // gzip has highest q=1.0, so it should be selected even though
+  // br and zstd are also supported
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+  EXPECT_TRUE(ret == detail::EncodingType::Gzip);
+#elif CPPHTTPLIB_ZSTD_SUPPORT
+  EXPECT_TRUE(ret == detail::EncodingType::Zstd);
+#elif CPPHTTPLIB_BROTLI_SUPPORT
+  EXPECT_TRUE(ret == detail::EncodingType::Brotli);
+#else
+  EXPECT_TRUE(ret == detail::EncodingType::None);
+#endif
+}
+
 TEST(BufferStreamTest, read) {
   detail::BufferStream strm1;
   Stream &strm = strm1;
@@ -2569,7 +2587,7 @@ TEST(PathUrlEncodeTest, IncludePercentEncodingLF) {
   }
 }
 
-TEST(BindServerTest, DISABLED_BindDualStack) {
+TEST(BindServerTest, BindDualStack) {
   Server svr;
 
   svr.Get("/1", [&](const Request & /*req*/, Response &res) {
@@ -5662,7 +5680,13 @@ TEST_F(ServerTest, PutLargeFileWithGzip2) {
   // depending on the zlib library.
   EXPECT_LT(res.get_request_header_value_u64("Content-Length"),
             static_cast<uint64_t>(10 * 1024 * 1024));
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+  EXPECT_EQ("br", res.get_request_header_value("Content-Encoding"));
+#elif defined(CPPHTTPLIB_ZLIB_SUPPORT)
   EXPECT_EQ("gzip", res.get_request_header_value("Content-Encoding"));
+#elif defined(CPPHTTPLIB_ZSTD_SUPPORT)
+  EXPECT_EQ("zstd", res.get_request_header_value("Content-Encoding"));
+#endif
 }
 
 TEST_F(ServerTest, PutContentWithDeflate) {
@@ -5803,53 +5827,6 @@ TEST(GzipDecompressor, DeflateDecompressionTrailingBytes) {
   ASSERT_EQ(original_text, decompressed_data);
 }
 
-#ifdef _WIN32
-TEST(GzipDecompressor, LargeRandomData) {
-
-  // prepare large random data that is difficult to be compressed and is
-  // expected to have large size even when compressed
-  std::random_device seed_gen;
-  std::mt19937 random(seed_gen());
-  constexpr auto large_size_byte = 4294967296UL;            // 4GiB
-  constexpr auto data_size = large_size_byte + 134217728UL; // + 128MiB
-  std::vector<std::uint32_t> data(data_size / sizeof(std::uint32_t));
-  std::generate(data.begin(), data.end(), [&]() { return random(); });
-
-  // compress data over 4GiB
-  std::string compressed_data;
-  compressed_data.reserve(large_size_byte + 536870912UL); // + 512MiB reserved
-  httplib::detail::gzip_compressor compressor;
-  auto result = compressor.compress(reinterpret_cast<const char *>(data.data()),
-                                    data.size() * sizeof(std::uint32_t), true,
-                                    [&](const char *data, size_t size) {
-                                      compressed_data.insert(
-                                          compressed_data.size(), data, size);
-                                      return true;
-                                    });
-  ASSERT_TRUE(result);
-
-  // FIXME: compressed data size is expected to be greater than 4GiB,
-  // but there is no guarantee
-  // ASSERT_TRUE(compressed_data.size() >= large_size_byte);
-
-  // decompress data over 4GiB
-  std::string decompressed_data;
-  decompressed_data.reserve(data_size);
-  httplib::detail::gzip_decompressor decompressor;
-  result = decompressor.decompress(
-      compressed_data.data(), compressed_data.size(),
-      [&](const char *data, size_t size) {
-        decompressed_data.insert(decompressed_data.size(), data, size);
-        return true;
-      });
-  ASSERT_TRUE(result);
-
-  // compare
-  ASSERT_EQ(data_size, decompressed_data.size());
-  ASSERT_TRUE(std::memcmp(data.data(), decompressed_data.data(), data_size) ==
-              0);
-}
-#endif
 #endif
 
 #ifdef CPPHTTPLIB_BROTLI_SUPPORT
@@ -5872,6 +5849,47 @@ TEST_F(ServerTest, GetStreamedChunkedWithBrotli2) {
   EXPECT_EQ(StatusCode::OK_200, res->status);
   EXPECT_EQ(std::string("123456789"), res->body);
 }
+
+TEST_F(ServerTest, PutWithContentProviderWithBrotli) {
+  cli_.set_compress(true);
+  auto res = cli_.Put(
+      "/put", 3,
+      [](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
+        sink.os << "PUT";
+        return true;
+      },
+      "text/plain");
+
+  ASSERT_TRUE(res);
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  EXPECT_EQ("PUT", res->body);
+}
+
+TEST_F(ServerTest, PutWithContentProviderWithoutLengthWithBrotli) {
+  cli_.set_compress(true);
+  auto res = cli_.Put(
+      "/put",
+      [](size_t /*offset*/, DataSink &sink) {
+        sink.os << "PUT";
+        sink.done();
+        return true;
+      },
+      "text/plain");
+
+  ASSERT_TRUE(res);
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  EXPECT_EQ("PUT", res->body);
+}
+
+TEST_F(ServerTest, PutLargeFileWithBrotli) {
+  cli_.set_compress(true);
+  auto res = cli_.Put("/put-large", LARGE_DATA, "text/plain");
+
+  ASSERT_TRUE(res);
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  EXPECT_EQ(LARGE_DATA, res->body);
+  EXPECT_EQ("br", res.get_request_header_value("Content-Encoding"));
+}
 #endif
 
 TEST_F(ServerTest, Patch) {
@@ -10782,38 +10800,6 @@ TEST(ClientImplMethods, GetSocketTest) {
   }
 }
 
-// Disabled due to out-of-memory problem on GitHub Actions
-#ifdef _WIN64
-TEST(ServerLargeContentTest, DISABLED_SendLargeContent) {
-  // allocate content size larger than 2GB in memory
-  const size_t content_size = 2LL * 1024LL * 1024LL * 1024LL + 1LL;
-  char *content = (char *)malloc(content_size);
-  ASSERT_TRUE(content);
-
-  Server svr;
-  svr.Get("/foo",
-          [=](const httplib::Request & /*req*/, httplib::Response &res) {
-            res.set_content(content, content_size, "application/octet-stream");
-          });
-
-  auto listen_thread = std::thread([&svr]() { svr.listen(HOST, PORT); });
-  auto se = detail::scope_exit([&] {
-    svr.stop();
-    listen_thread.join();
-    if (content) free(content);
-    ASSERT_FALSE(svr.is_running());
-  });
-
-  svr.wait_until_ready();
-
-  Client cli(HOST, PORT);
-  auto res = cli.Get("/foo");
-  ASSERT_TRUE(res);
-  EXPECT_EQ(StatusCode::OK_200, res->status);
-  EXPECT_EQ(content_size, res->body.length());
-}
-#endif
-
 #ifdef CPPHTTPLIB_SSL_ENABLED
 TEST(YahooRedirectTest2, SimpleInterface_Online) {
   Client cli("http://yahoo.com");
@@ -13917,7 +13903,7 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
 #endif
 
   auto server_thread = serve_single_response(
-      PORT + 2, "HTTP/1.1 200 OK\r\n"
+      PORT + 4, "HTTP/1.1 200 OK\r\n"
                 "Content-Type: text/plain\r\n"
                 "Content-Length: 99999999999999999999999999\r\n"
                 "Connection: close\r\n"
@@ -13930,7 +13916,7 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
   // crash the process. After the fix, strtoull silently clamps to
   // ULLONG_MAX so the stream opens without crashing. The important thing
   // is that the process does NOT terminate.
-  Client cli("127.0.0.1", PORT + 2);
+  Client cli("127.0.0.1", PORT + 4);
   auto handle = cli.open_stream("GET", "/");
   EXPECT_TRUE(handle.is_valid());
 
@@ -16165,48 +16151,6 @@ TEST(SSLClientServerTest, FilePathConstructorSetsClientCAList) {
   EXPECT_GT(sk_X509_NAME_num(ca_list), 0);
 }
 
-// Disabled due to the out-of-memory problem on GitHub Actions Workflows
-TEST(SSLClientServerTest, DISABLED_LargeDataTransfer) {
-
-  // prepare large data
-  std::random_device seed_gen;
-  std::mt19937 random(seed_gen());
-  constexpr auto large_size_byte = 2147483648UL + 1048576UL; // 2GiB + 1MiB
-  std::vector<std::uint32_t> binary(large_size_byte / sizeof(std::uint32_t));
-  std::generate(binary.begin(), binary.end(), [&random]() { return random(); });
-
-  // server
-  SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
-  ASSERT_TRUE(svr.is_valid());
-
-  svr.Post("/binary", [&](const Request &req, Response &res) {
-    EXPECT_EQ(large_size_byte, req.body.size());
-    EXPECT_EQ(0, std::memcmp(binary.data(), req.body.data(), large_size_byte));
-    res.set_content(req.body, "application/octet-stream");
-  });
-
-  auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
-  auto se = detail::scope_exit([&] {
-    svr.stop();
-    listen_thread.join();
-    ASSERT_FALSE(svr.is_running());
-  });
-
-  svr.wait_until_ready();
-
-  // client POST
-  SSLClient cli("localhost", PORT);
-  cli.enable_server_certificate_verification(false);
-  cli.set_read_timeout(std::chrono::seconds(100));
-  cli.set_write_timeout(std::chrono::seconds(100));
-  auto res = cli.Post("/binary", reinterpret_cast<char *>(binary.data()),
-                      large_size_byte, "application/octet-stream");
-
-  // compare
-  EXPECT_EQ(StatusCode::OK_200, res->status);
-  EXPECT_EQ(large_size_byte, res->body.size());
-  EXPECT_EQ(0, std::memcmp(binary.data(), res->body.data(), large_size_byte));
-}
 #endif
 
 // ============================================================================

+ 78 - 0
test/test_benchmark.cc

@@ -0,0 +1,78 @@
+#include <httplib.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <chrono>
+#include <sstream>
+#include <thread>
+#include <vector>
+
+using namespace httplib;
+
+static const int PORT = 11134;
+
+static void performance_test(const char *host) {
+  Server svr;
+
+  svr.Get("/benchmark", [&](const Request & /*req*/, Response &res) {
+    res.set_content("Benchmark Response", "text/plain");
+  });
+
+  auto listen_thread = std::thread([&]() { svr.listen(host, PORT); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    listen_thread.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  svr.wait_until_ready();
+
+  Client cli(host, PORT);
+
+  // Warm-up request to establish connection and resolve DNS
+  auto warmup_res = cli.Get("/benchmark");
+  ASSERT_TRUE(warmup_res); // Ensure server is responding correctly
+
+  // Run multiple trials and collect timings
+  const int num_trials = 20;
+  std::vector<int64_t> timings;
+  timings.reserve(num_trials);
+
+  for (int i = 0; i < num_trials; i++) {
+    auto start = std::chrono::high_resolution_clock::now();
+    auto res = cli.Get("/benchmark");
+    auto end = std::chrono::high_resolution_clock::now();
+
+    auto elapsed =
+        std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
+            .count();
+
+    // Assertions after timing measurement to avoid overhead
+    ASSERT_TRUE(res);
+    EXPECT_EQ(StatusCode::OK_200, res->status);
+
+    timings.push_back(elapsed);
+  }
+
+  // Calculate 25th percentile (lower quartile)
+  std::sort(timings.begin(), timings.end());
+  auto p25 = timings[num_trials / 4];
+
+  // Format timings for output
+  std::ostringstream timings_str;
+  timings_str << "[";
+  for (size_t i = 0; i < timings.size(); i++) {
+    if (i > 0) timings_str << ", ";
+    timings_str << timings[i];
+  }
+  timings_str << "]";
+
+  // Localhost HTTP GET should be fast even in CI environments
+  EXPECT_LE(p25, 5) << "25th percentile performance is too slow: " << p25
+                    << "ms (Issue #1777). Timings: " << timings_str.str();
+}
+
+TEST(BenchmarkTest, localhost) { performance_test("localhost"); }
+
+TEST(BenchmarkTest, v6) { performance_test("::1"); }

+ 22 - 0
test/test_proxy.cc

@@ -344,3 +344,25 @@ TEST(KeepAliveTest, SSLWithDigest) {
   KeepAliveTest(cli, false);
 }
 #endif
+
+// ----------------------------------------------------------------------------
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+TEST(ProxyTest, SSLOpenStream) {
+  SSLClient cli("nghttp2.org");
+  cli.set_proxy("localhost", 3128);
+  cli.set_proxy_basic_auth("hello", "world");
+
+  auto handle = cli.open_stream("GET", "/httpbin/get");
+  ASSERT_TRUE(handle.response);
+  EXPECT_EQ(StatusCode::OK_200, handle.response->status);
+
+  std::string body;
+  char buf[8192];
+  ssize_t n;
+  while ((n = handle.read(buf, sizeof(buf))) > 0) {
+    body.append(buf, static_cast<size_t>(n));
+  }
+  EXPECT_FALSE(body.empty());
+}
+#endif

+ 79 - 0
test/test_websocket_heartbeat.cc

@@ -57,6 +57,85 @@ TEST_F(WebSocketHeartbeatTest, IdleConnectionStaysAlive) {
   client.close();
 }
 
+// Verify that set_websocket_ping_interval overrides the compile-time default
+TEST_F(WebSocketHeartbeatTest, RuntimePingIntervalOverride) {
+  // The server is already using the compile-time default (1s).
+  // Create a client with a custom runtime interval.
+  ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
+  client.set_websocket_ping_interval(2);
+  ASSERT_TRUE(client.connect());
+
+  // Sleep longer than read timeout (3s). Client heartbeat at 2s keeps alive.
+  std::this_thread::sleep_for(std::chrono::seconds(5));
+
+  ASSERT_TRUE(client.is_open());
+  ASSERT_TRUE(client.send("runtime interval"));
+  std::string msg;
+  ASSERT_TRUE(client.read(msg));
+  EXPECT_EQ("runtime interval", msg);
+
+  client.close();
+}
+
+// Verify that ping_interval=0 disables heartbeat without breaking basic I/O.
+TEST_F(WebSocketHeartbeatTest, ZeroDisablesHeartbeat) {
+  ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
+  client.set_websocket_ping_interval(0);
+  ASSERT_TRUE(client.connect());
+
+  // Basic send/receive still works with heartbeat disabled
+  ASSERT_TRUE(client.send("no client ping"));
+  std::string msg;
+  ASSERT_TRUE(client.read(msg));
+  EXPECT_EQ("no client ping", msg);
+
+  client.close();
+}
+
+// Verify that Server::set_websocket_ping_interval works at runtime
+class WebSocketServerPingIntervalTest : public ::testing::Test {
+protected:
+  void SetUp() override {
+    svr_.set_websocket_ping_interval(2);
+    svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
+      std::string msg;
+      while (ws.read(msg)) {
+        ws.send(msg);
+      }
+    });
+
+    port_ = svr_.bind_to_any_port("localhost");
+    thread_ = std::thread([this]() { svr_.listen_after_bind(); });
+    svr_.wait_until_ready();
+  }
+
+  void TearDown() override {
+    svr_.stop();
+    thread_.join();
+  }
+
+  Server svr_;
+  int port_;
+  std::thread thread_;
+};
+
+TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
+  ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
+  ASSERT_TRUE(client.connect());
+
+  // Server ping interval is 2s; client uses compile-time default (1s).
+  // Both keep the connection alive.
+  std::this_thread::sleep_for(std::chrono::seconds(5));
+
+  ASSERT_TRUE(client.is_open());
+  ASSERT_TRUE(client.send("server interval"));
+  std::string msg;
+  ASSERT_TRUE(client.read(msg));
+  EXPECT_EQ("server interval", msg);
+
+  client.close();
+}
+
 // Verify that multiple heartbeat cycles work
 TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");