13 Commits 2e61fd3e6e ... 1c6b3ea5a0

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

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

@@ -228,7 +228,7 @@ jobs:
         for ($i = 0; $i -lt $shards; $i++) {
         for ($i = 0; $i -lt $shards; $i++) {
           $log = "shard_${i}.log"
           $log = "shard_${i}.log"
           $procs += Start-Process -FilePath ./Release/httplib-test.exe `
           $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" `
             -NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
             -Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
             -Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
         }
         }
@@ -248,9 +248,6 @@ jobs:
         }
         }
         if ($failed) { exit 1 }
         if ($failed) { exit 1 }
         Write-Host "All shards passed."
         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:
     env:
       VCPKG_ROOT: "C:/vcpkg"
       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_split_no_tls
 test/test_websocket_heartbeat
 test/test_websocket_heartbeat
 test/test_thread_pool
 test/test_thread_pool
+test/test_benchmark
 test/test.xcodeproj/xcuser*
 test/test.xcodeproj/xcuser*
 test/test.xcodeproj/*/xcuser*
 test/test.xcodeproj/*/xcuser*
 test/*.o
 test/*.o

+ 2 - 2
README-stream.md

@@ -45,7 +45,7 @@ cpp-httplib provides multiple API layers for different use cases:
 
 
 ```text
 ```text
 ┌─────────────────────────────────────────────┐
 ┌─────────────────────────────────────────────┐
-│  SSEClient (planned)                        │  ← SSE-specific, parsed events
+│  SSEClient                        │  ← SSE-specific, parsed events
 │  - on_message(), on_event()                 │
 │  - on_message(), on_event()                 │
 │  - Auto-reconnect, Last-Event-ID            │
 │  - Auto-reconnect, Last-Event-ID            │
 ├─────────────────────────────────────────────┤
 ├─────────────────────────────────────────────┤
@@ -61,7 +61,7 @@ cpp-httplib provides multiple API layers for different use cases:
 
 
 | Use Case | Recommended API |
 | 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()` |
 | LLM streaming (JSON Lines) | `stream::Get()` |
 | Large file download | `stream::Get()` or `open_stream()` |
 | Large file download | `stream::Get()` or `open_stream()` |
 | Reverse proxy | `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_CLOSE_TIMEOUT_SECOND` | `5`               | Timeout for waiting peer's Close response (seconds)      |
 | `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30`              | Automatic Ping interval for heartbeat (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
 ## Threading Model
 
 
 WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
 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]
 [site]
 title = "cpp-httplib"
 title = "cpp-httplib"
-version = "0.37.2"
+version = "0.38.0"
 hostname = "https://yhirose.github.io"
 hostname = "https://yhirose.github.io"
 base_path = "/cpp-httplib"
 base_path = "/cpp-httplib"
 footer_message = "© 2026 Yuji Hirose. All rights reserved."
 footer_message = "© 2026 Yuji Hirose. All rights reserved."

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

@@ -1,6 +1,7 @@
 ---
 ---
 title: "Cookbook"
 title: "Cookbook"
 order: 0
 order: 0
+status: "draft"
 ---
 ---
 
 
 A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need.
 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
 ## Documentation
 
 
 - [A Tour of cpp-httplib](tour/) — A step-by-step tutorial covering the basics. Start here if you're new
 - [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
 - [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
 - [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"
 title: "Building a Desktop LLM App with cpp-httplib"
 order: 0
 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.
 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"
 title: "Cookbook"
 order: 0
 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/) — 基本を順を追って学べるチュートリアル。初めての方はここから
 - [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
+
+## お楽しみに
+
 - [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
 - [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
 - [Building a Desktop LLM App](llm-app/) — llama.cpp を組み込んだデスクトップアプリを段階的に構築する実践ガイド
 - [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"
 title: "Building a Desktop LLM App with cpp-httplib"
 order: 0
 order: 0
+status: "draft"
 ---
 ---
 
 
 llama.cpp を組み込んだ LLM 翻訳デスクトップアプリを段階的に構築しながら、cpp-httplib のサーバー・クライアント両面の使い方を実践的に学びます。翻訳は一例であり、この部分を差し替えることで要約・コード生成・チャットボットなど自分のアプリに応用できます。
 llama.cpp を組み込んだ LLM 翻訳デスクトップアプリを段階的に構築しながら、cpp-httplib のサーバー・クライアント両面の使い方を実践的に学びます。翻訳は一例であり、この部分を差し替えることで要約・コード生成・チャットボットなど自分のアプリに応用できます。

+ 259 - 137
httplib.h

@@ -8,8 +8,8 @@
 #ifndef CPPHTTPLIB_HTTPLIB_H
 #ifndef CPPHTTPLIB_HTTPLIB_H
 #define 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
 #ifdef _WIN32
 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -1670,6 +1670,11 @@ public:
 
 
   Server &set_payload_max_length(size_t length);
   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);
   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);
   int bind_to_any_port(const std::string &host, int socket_flags = 0);
   bool listen_after_bind();
   bool listen_after_bind();
@@ -1704,6 +1709,8 @@ protected:
   time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
   time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
   time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
   time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
   size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
   size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+  time_t websocket_ping_interval_sec_ =
+      CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
 
 
 private:
 private:
   using Handlers =
   using Handlers =
@@ -1879,7 +1886,8 @@ private:
 
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
 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 {
   uint64_t ssl_openssl_error() const {
     return ssl_backend_error_;
     return ssl_backend_error_;
   }
   }
@@ -2191,6 +2199,10 @@ protected:
 
 
   virtual bool create_and_connect_socket(Socket &socket, Error &error);
   virtual bool create_and_connect_socket(Socket &socket, Error &error);
   virtual bool ensure_socket_connection(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:
   // All of:
   //   shutdown_ssl
   //   shutdown_ssl
@@ -2355,13 +2367,16 @@ protected:
 
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
 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);
   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;
   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(
   virtual void set_server_certificate_verifier(
       std::function<SSLVerifierResponse(SSL *ssl)> verifier);
       std::function<SSLVerifierResponse(SSL *ssl)> verifier);
 #endif
 #endif
@@ -2590,14 +2605,17 @@ private:
 
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
 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;
   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(
   void set_server_certificate_verifier(
       std::function<SSLVerifierResponse(SSL *ssl)> 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;
   long get_verify_result() const;
 #endif
 #endif
 };
 };
@@ -2649,18 +2667,22 @@ private:
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
 public:
   [[deprecated("Use SSLServer(PemMemory) or "
   [[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,
   SSLServer(X509 *cert, EVP_PKEY *private_key,
             X509_STORE *client_ca_cert_store = nullptr);
             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(
   SSLServer(
       const std::function<bool(SSL_CTX &ssl_ctx)> &setup_ssl_ctx_callback);
       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;
   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,
   void update_certs(X509 *cert, EVP_PKEY *private_key,
                     X509_STORE *client_ca_cert_store = nullptr);
                     X509_STORE *client_ca_cert_store = nullptr);
 #endif
 #endif
@@ -2717,6 +2739,10 @@ private:
                  std::function<bool(Stream &strm)> callback) override;
                  std::function<bool(Stream &strm)> callback) override;
   bool is_ssl() const 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(
   bool connect_with_proxy(
       Socket &sock,
       Socket &sock,
       std::chrono::time_point<std::chrono::steady_clock> start_time,
       std::chrono::time_point<std::chrono::steady_clock> start_time,
@@ -2741,18 +2767,22 @@ private:
 
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 public:
 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,
   explicit SSLClient(const std::string &host, int port, X509 *client_cert,
                      EVP_PKEY *client_key,
                      EVP_PKEY *client_key,
                      const std::string &private_key_password = std::string());
                      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;
   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;
   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(
   void set_server_certificate_verifier(
       std::function<SSLVerifierResponse(SSL *ssl)> verifier) override;
       std::function<SSLVerifierResponse(SSL *ssl)> verifier) override;
 
 
@@ -3721,15 +3751,19 @@ private:
   friend class httplib::Server;
   friend class httplib::Server;
   friend class WebSocketClient;
   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();
     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),
       : 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();
     start_heartbeat();
   }
   }
 
 
@@ -3740,6 +3774,7 @@ private:
   std::unique_ptr<Stream> owned_strm_;
   std::unique_ptr<Stream> owned_strm_;
   Request req_;
   Request req_;
   bool is_server_;
   bool is_server_;
+  time_t ping_interval_sec_;
   std::atomic<bool> closed_{false};
   std::atomic<bool> closed_{false};
   std::mutex write_mutex_;
   std::mutex write_mutex_;
   std::thread ping_thread_;
   std::thread ping_thread_;
@@ -3768,6 +3803,7 @@ public:
   const std::string &subprotocol() const;
   const std::string &subprotocol() const;
   void set_read_timeout(time_t sec, time_t usec = 0);
   void set_read_timeout(time_t sec, time_t usec = 0);
   void set_write_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
 #ifdef CPPHTTPLIB_SSL_ENABLED
   void set_ca_cert_path(const std::string &path);
   void set_ca_cert_path(const std::string &path);
@@ -3791,6 +3827,8 @@ private:
   time_t read_timeout_usec_ = 0;
   time_t read_timeout_usec_ = 0;
   time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
   time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
   time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
   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
 #ifdef CPPHTTPLIB_SSL_ENABLED
   bool is_ssl_ = false;
   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) {
 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");
   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
 #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
 #endif
-
 #ifdef CPPHTTPLIB_ZLIB_SUPPORT
 #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
 #endif
-
 #ifdef CPPHTTPLIB_ZSTD_SUPPORT
 #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
 #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,
 inline bool nocompressor::compress(const char *data, size_t data_length,
@@ -6773,6 +6892,21 @@ create_decompressor(const std::string &encoding) {
   return decompressor;
   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) {
 inline bool is_prohibited_header_name(const std::string &name) {
   using udl::operator""_t;
   using udl::operator""_t;
 
 
@@ -7605,7 +7739,7 @@ inline bool parse_accept_header(const std::string &s,
   struct AcceptEntry {
   struct AcceptEntry {
     std::string media_type;
     std::string media_type;
     double quality;
     double quality;
-    int order; // Original order in header
+    int order;
   };
   };
 
 
   std::vector<AcceptEntry> entries;
   std::vector<AcceptEntry> entries;
@@ -7623,48 +7757,12 @@ inline bool parse_accept_header(const std::string &s,
     }
     }
 
 
     AcceptEntry accept_entry;
     AcceptEntry accept_entry;
-    accept_entry.quality = 1.0; // Default quality
     accept_entry.order = order++;
     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
     // Remove additional parameters from media type
@@ -10746,6 +10844,20 @@ inline Server &Server::set_payload_max_length(size_t length) {
   return *this;
   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,
 inline bool Server::bind_to_port(const std::string &host, int port,
                                  int socket_flags) {
                                  int socket_flags) {
   auto ret = bind_internal(host, port, 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
           // Use WebSocket-specific read timeout instead of HTTP timeout
           strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
           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);
           entry.handler(req, ws);
         }
         }
         return true;
         return true;
@@ -12140,6 +12252,13 @@ inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
   return create_and_connect_socket(socket, 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*/,
 inline void ClientImpl::shutdown_ssl(Socket & /*socket*/,
                                      bool /*shutdown_gracefully*/) {
                                      bool /*shutdown_gracefully*/) {
   // If there are any requests in flight from threads other than us, then it's
   // 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;
         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
     // 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;
         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);
     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" ||
   if (res.get_header_value("Connection") == "close" ||
       (res.version == "HTTP/1.0" && res.reason != "Connection established")) {
       (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.
     // for this to be safe.
 
 
     // This is safe to call because handle_request is only called by send_
     // 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; };
   auto is_shutting_down = []() { return false; };
 
 
   if (req.is_chunked_content_provider_) {
   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>();
       compressor = detail::make_unique<detail::nocompressor>();
     }
     }
 
 
@@ -13208,14 +13307,15 @@ ClientImpl::send_with_content_provider_and_receiver(
     Error &error) {
     Error &error) {
   if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
   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) {
     if (content_provider) {
       auto ok = true;
       auto ok = true;
@@ -13226,7 +13326,7 @@ ClientImpl::send_with_content_provider_and_receiver(
         if (ok) {
         if (ok) {
           auto last = offset + data_len == content_length;
           auto last = offset + data_len == content_length;
 
 
-          auto ret = compressor.compress(
+          auto ret = compressor->compress(
               data, data_len, last,
               data, data_len, last,
               [&](const char *compressed_data, size_t compressed_data_len) {
               [&](const char *compressed_data, size_t compressed_data_len) {
                 req.body.append(compressed_data, compressed_data_len);
                 req.body.append(compressed_data, compressed_data_len);
@@ -13250,19 +13350,17 @@ ClientImpl::send_with_content_provider_and_receiver(
         }
         }
       }
       }
     } else {
     } 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;
         error = Error::Compression;
         output_error_log(error, &req);
         output_error_log(error, &req);
         return nullptr;
         return nullptr;
       }
       }
     }
     }
-  } else
-#endif
-  {
+  } else {
     if (content_provider) {
     if (content_provider) {
       req.content_length_ = content_length;
       req.content_length_ = content_length;
       req.content_provider_ = std::move(content_provider);
       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); }
 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) {
 inline void Client::set_url_encode(bool on) {
   cli_->set_path_encode(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);
   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
 // Assumes that socket_mutex_ is locked and that there are no requests in
 // flight
 // flight
 inline bool SSLClient::connect_with_proxy(
 inline bool SSLClient::connect_with_proxy(
@@ -19945,11 +20062,11 @@ inline WebSocket::~WebSocket() {
 }
 }
 
 
 inline void WebSocket::start_heartbeat() {
 inline void WebSocket::start_heartbeat() {
+  if (ping_interval_sec_ == 0) { return; }
   ping_thread_ = std::thread([this]() {
   ping_thread_ = std::thread([this]() {
     std::unique_lock<std::mutex> lock(ping_mutex_);
     std::unique_lock<std::mutex> lock(ping_mutex_);
     while (!closed_) {
     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; }
       if (closed_) { break; }
       lock.unlock();
       lock.unlock();
       if (!send_frame(Opcode::Ping, nullptr, 0)) {
       if (!send_frame(Opcode::Ping, nullptr, 0)) {
@@ -20087,7 +20204,8 @@ inline bool WebSocketClient::connect() {
   Request req;
   Request req;
   req.method = "GET";
   req.method = "GET";
   req.path = path_;
   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;
   return true;
 }
 }
 
 
@@ -20127,6 +20245,10 @@ inline void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
   write_timeout_usec_ = usec;
   write_timeout_usec_ = usec;
 }
 }
 
 
+inline void WebSocketClient::set_websocket_ping_interval(time_t sec) {
+  websocket_ping_interval_sec_ = sec;
+}
+
 #ifdef CPPHTTPLIB_SSL_ENABLED
 #ifdef CPPHTTPLIB_SSL_ENABLED
 
 
 inline void WebSocketClient::set_ca_cert_path(const std::string &path) {
 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 fuzz_test)
     @(cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat)
     @(cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat)
     @(cd test && make test_thread_pool && ./test_thread_pool)
     @(cd test && make test_thread_pool && ./test_thread_pool)
+    @(cd test && make test_benchmark && ./test_benchmark)
 
 
 build:
 build:
     @(cd test && make test_split)
     @(cd test && make test_split)

+ 11 - 1
test/Makefile

@@ -219,6 +219,16 @@ style_check: $(STYLE_CHECK_FILES)
 		echo "All files are properly formatted."; \
 		echo "All files are properly formatted."; \
 	fi
 	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
 test_websocket_heartbeat : test_websocket_heartbeat.cc ../httplib.h Makefile
 	$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
 	$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
 	@file $@
 	@file $@
@@ -254,5 +264,5 @@ cert.pem:
 	./gen-certs.sh
 	./gen-certs.sh
 
 
 clean:
 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));
   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 {
 class UnixSocketTest : public ::testing::Test {
 protected:
 protected:
   void TearDown() override { std::remove(pathname_.c_str()); }
   void TearDown() override { std::remove(pathname_.c_str()); }
@@ -1267,6 +1202,89 @@ TEST(ParseAcceptEncoding3, AcceptEncoding) {
 #endif
 #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) {
 TEST(BufferStreamTest, read) {
   detail::BufferStream strm1;
   detail::BufferStream strm1;
   Stream &strm = strm1;
   Stream &strm = strm1;
@@ -2569,7 +2587,7 @@ TEST(PathUrlEncodeTest, IncludePercentEncodingLF) {
   }
   }
 }
 }
 
 
-TEST(BindServerTest, DISABLED_BindDualStack) {
+TEST(BindServerTest, BindDualStack) {
   Server svr;
   Server svr;
 
 
   svr.Get("/1", [&](const Request & /*req*/, Response &res) {
   svr.Get("/1", [&](const Request & /*req*/, Response &res) {
@@ -5662,7 +5680,13 @@ TEST_F(ServerTest, PutLargeFileWithGzip2) {
   // depending on the zlib library.
   // depending on the zlib library.
   EXPECT_LT(res.get_request_header_value_u64("Content-Length"),
   EXPECT_LT(res.get_request_header_value_u64("Content-Length"),
             static_cast<uint64_t>(10 * 1024 * 1024));
             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"));
   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) {
 TEST_F(ServerTest, PutContentWithDeflate) {
@@ -5803,53 +5827,6 @@ TEST(GzipDecompressor, DeflateDecompressionTrailingBytes) {
   ASSERT_EQ(original_text, decompressed_data);
   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
 #endif
 
 
 #ifdef CPPHTTPLIB_BROTLI_SUPPORT
 #ifdef CPPHTTPLIB_BROTLI_SUPPORT
@@ -5872,6 +5849,47 @@ TEST_F(ServerTest, GetStreamedChunkedWithBrotli2) {
   EXPECT_EQ(StatusCode::OK_200, res->status);
   EXPECT_EQ(StatusCode::OK_200, res->status);
   EXPECT_EQ(std::string("123456789"), res->body);
   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
 #endif
 
 
 TEST_F(ServerTest, Patch) {
 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
 #ifdef CPPHTTPLIB_SSL_ENABLED
 TEST(YahooRedirectTest2, SimpleInterface_Online) {
 TEST(YahooRedirectTest2, SimpleInterface_Online) {
   Client cli("http://yahoo.com");
   Client cli("http://yahoo.com");
@@ -13917,7 +13903,7 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
 #endif
 #endif
 
 
   auto server_thread = serve_single_response(
   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-Type: text/plain\r\n"
                 "Content-Length: 99999999999999999999999999\r\n"
                 "Content-Length: 99999999999999999999999999\r\n"
                 "Connection: close\r\n"
                 "Connection: close\r\n"
@@ -13930,7 +13916,7 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
   // crash the process. After the fix, strtoull silently clamps to
   // crash the process. After the fix, strtoull silently clamps to
   // ULLONG_MAX so the stream opens without crashing. The important thing
   // ULLONG_MAX so the stream opens without crashing. The important thing
   // is that the process does NOT terminate.
   // 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", "/");
   auto handle = cli.open_stream("GET", "/");
   EXPECT_TRUE(handle.is_valid());
   EXPECT_TRUE(handle.is_valid());
 
 
@@ -16165,48 +16151,6 @@ TEST(SSLClientServerTest, FilePathConstructorSetsClientCAList) {
   EXPECT_GT(sk_X509_NAME_num(ca_list), 0);
   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
 #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);
   KeepAliveTest(cli, false);
 }
 }
 #endif
 #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();
   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
 // Verify that multiple heartbeat cycles work
 TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
 TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");