Compare commits

...

47 Commits

Author SHA1 Message Date
yhirose 811dd0b6f2 Release v0.44.0 2026-05-10 21:46:24 +09:00
yhirose e8e652824b Add --minor flag to release.sh for forced minor bumps
Allows forcing a minor version bump even when abidiff passes,
for behavioral breaking changes that don't break ABI.
2026-05-10 21:28:38 +09:00
yhirose fbb031ed85 Stop percent-decoding HTTP request header values
parse_header() applied decode_path_component() to every header value
except Location and Referer, after is_field_value() validation. Wire
sequences like %0D%0A passed the check and expanded into literal CR/LF
inside stored values, enabling response splitting, log injection, and
proxy smuggling. %3D/%2C/%3B also flipped Cookie and X-Forwarded-For
boundaries against WAFs inspecting the wire form.

RFC 9110 §5.5 specifies header values as opaque octets. Drop the
decoding and the Location/Referer special case (originally workarounds
for the same auto-decode misbehavior; redundant once decoding stops).
Applications that need URI semantics should call decode_uri_component()
or decode_path_component() on the result explicitly.

Add regression tests covering CRLF injection, %3D/%2C/%3B boundary
characters, UTF-8 and %uXXXX sequences, browser-style Referer URLs
containing %0A (issue #2033), and the explicit-decode migration
pattern.
2026-05-10 12:59:29 +09:00
yhirose 7d5082cc0e Make ThreadPool ctor exception-safe on partial thread creation (#2445)
* Make ThreadPool ctor exception-safe on partial thread creation

If std::thread construction throws partway through the ThreadPool
constructor (e.g., pthread_create returns EAGAIN under thread-resource
pressure), the partially-built threads_ vector would destruct joinable
std::thread objects, calling std::terminate(). Wrap the spawn loop and,
on failure, signal shutdown to the workers already created, join them,
and rethrow.

Adds a reproducer test in test_thread_pool.cc that interposes
pthread_create at link time to deterministically fail the second call,
gated to POSIX + exceptions-enabled builds.

Fix #2444

* Strip ASAN from test_thread_pool to coexist with pthread_create override

Linux libasan installs its own pthread_create interceptor; our in-binary
symbol override sits on top of it and corrupts ASAN's thread bookkeeping,
which surfaces as "Joining already joined thread" on the very first test.
Disable ASAN for this small unit-test binary -- ThreadPool memory behavior
is still exercised under ASAN by the main `test` binary.
2026-05-09 21:13:40 -04:00
yhirose 600d220c84 Release v0.43.4 2026-05-09 21:29:23 +09:00
yhirose 87d62db46b Reject malformed chunk-size in chunked decoder
strtoul silently accepts a leading "-" and wraps via unsigned
arithmetic, so chunk-size "-2" produced ULONG_MAX-1, bypassing the
ULONG_MAX guard and letting a client drive the server toward unbounded
allocation.

Replace strtoul with a manual hex parser that requires at least one hex
digit, detects size_t overflow per digit, and accepts only chunk-ext or
end-of-line after the digits (RFC 9112 §7.1).
2026-05-09 16:52:32 +09:00
yhirose a1fdc07f34 Guard nullptr res in KeepAliveTest proxy template (#2443)
When the upstream request to httpbingo.org transiently fails, cli.Get()
returns nullptr and the next line dereferences it (res->status / res->body),
producing a SEGV in std::string::begin() under ASan. Sibling templates in
the same file already use ASSERT_TRUE(res != nullptr); apply the same
guard to the four Get() call sites in KeepAliveTest so a flaky network
turns into a clean test failure instead of a crash.
2026-05-06 08:36:38 -04:00
yhirose eb49a304b6 Use vswhere to locate VS install in 32-bit Windows CI (#2442)
The hosted windows-latest runner is migrating from VS 2022 to VS 2026
(NOTICE: windows-2025 -> windows-2025-vs2026 by 2026-05-12). The
hardcoded path C:\Program Files\Microsoft Visual Studio\2022\Enterprise
no longer exists on the new image, so vcvarsall.bat silently fails and
'cl' is not on PATH.

Resolve the install path via vswhere.exe (stable location, version
agnostic) and exit if vcvarsall.bat fails so future breakage surfaces
immediately instead of as a confusing 'cl not recognized' error.
2026-05-06 08:25:56 -04:00
yhirose a9bfe5914b Fix #2441 2026-05-06 18:44:14 +09:00
yhirose ec5ce17929 Release v0.43.3 2026-05-04 16:19:49 +09:00
yhirose f6524c0802 Drop Str2tagTest unit test that broke split / no-exceptions builds
The test referenced detail::can_compress_content_type, which lives below
the split BORDER in httplib.h and is therefore not visible to test.cc in
test_split / Windows-CMake builds. EXPECT_NO_THROW also expanded to a
try/catch that would not compile under -fno-exceptions. The OSS-Fuzz
reproducer in test/fuzzing/corpus already serves as the regression test
for #508087118 and is exercised by make fuzz_test.
2026-05-01 22:20:41 +09:00
yhirose 35c4026c7f Make fuzz_test robust to missing corpus files
When a glob like clusterfuzz-testcase-minimized-foo_fuzzer-* did not
match anything, bash passed the literal pattern through. The standalone
runner then tried to open it, tellg() returned -1, and the resulting
size_t cast (SIZE_MAX) crashed std::vector with length_error. This made
fuzz_test fail loudly during bisects to commits before a corpus file
landed. Filter each glob through a -f test so unmatched patterns are
silently skipped with a "(no XXX corpus)" notice, mirroring what was
already done for url_parser_fuzzer.
2026-05-01 21:50:26 +09:00
yhirose 40e18460bc Document str2tag_core's compile-time-only role 2026-05-01 21:46:13 +09:00
yhirose 92aecf85d8 Fix OSS-Fuzz #508087118: avoid stack overflow in str2tag
str2tag_core is recursive (one frame per character), so a long runtime
input such as a fuzzer-supplied Content-Type would overflow the stack.
Rewrite the runtime entry point str2tag() iteratively while keeping the
recursive constexpr str2tag_core for compile-time UDL evaluation. The
hash output is unchanged for all inputs.
2026-05-01 21:39:46 +09:00
yhirose b223e29778 Add OSS-Fuzz #508370122 reproducer to client_fuzzer corpus
Same root cause as #508342856 (fixed in 2d2efe4): an oversized
Content-Length value (here 4467440718547775) caused res.body.reserve()
to attempt a multi-petabyte allocation. The UBSAN fuzzer job surfaced
it as a std::bad_alloc-driven abort, while the ASAN job for #508342856
reported it as allocation-size-too-big. The payload_max_length_ cap
introduced in 2d2efe4 already addresses both.
2026-05-01 21:34:03 +09:00
yhirose 2d2efe46da Fix OSS-Fuzz #508342856: cap Content-Length reservation by payload_max_length_
A malicious or malformed server response with an enormous Content-Length
header (e.g. 20000000000) caused the client to call res.body.reserve(len)
with the untrusted value, triggering OOM before read_content's
payload_max_length_ check could take effect. Cap the pre-reservation
at payload_max_length_, since reading more than that is never useful.
2026-05-01 21:28:57 +09:00
yhirose cae753425e Run all fuzzers via make fuzz_test 2026-05-01 21:28:45 +09:00
yhirose d412e98c62 Release v0.43.2 2026-04-30 17:47:53 +09:00
yhirose 806fcb8268 Re-enable getaddrinfo_a with worker-completion wait (#2431) (#2439)
* Restore getaddrinfo_a path with proper worker-completion wait (#2431)

5ebbfee dropped the Linux/glibc getaddrinfo_a branch entirely to avoid
the stack-use-after-free reported in #2431. That sidestepped the bug
but lost the asynchronous-resolution capability getaddrinfo_a is meant
to provide.

Bring the getaddrinfo_a branch back with the actual fix on the
cancellation path: after gai_cancel() — which is non-blocking and may
return EAI_NOTCANCELED while the resolver worker is still mid-operation
— call gai_suspend() with no timeout in a loop until gai_error() stops
returning EAI_INPROGRESS. Only then is it safe to destroy the
stack-local gaicb. freeaddrinfo() is also called on any partially
populated ar_result so that error paths do not leak.

This is the approach suggested in the issue body, with gai_suspend
substituted for the busy-poll over gai_error.

The issue-2431 reproducer test (run under ASAN with sinkhole DNS) is
unchanged and continues to drive the cancel path; it now exercises the
restored getaddrinfo_a code rather than the std::thread fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Simplify getaddrinfo_a branch (idiomatic init, scope_exit, fewer comments)

- Value-initialize gaicb / sigevent / timespec with {} instead of memset
- Replace the two manual freeaddrinfo calls with a scope_exit guard, with
  request.ar_result reset to nullptr on the success path to release
  ownership to the caller (matches the addrinfo cleanup pattern used in
  detail::create_socket and friends)
- Inline the single-call wait_for_request_done lambda
- Drop the (const struct gaicb *const *) cast — the array decays without
  it under C++11
- Tighten the leading comment to the one load-bearing fact (#2431) and
  the trade-off about pathological DNS waits; remove a stale claim that
  the inner loop handles EAI_INTR (the loop checks gai_error, not the
  gai_suspend return value)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:03:37 +09:00
yhirose c2678f0186 Fix #2435: allow mmap to open files held open for writing (#2438)
* Add test for #2435 mmap::open with concurrent writer

Verifies that detail::mmap can open a file held open with GENERIC_WRITE
by another handle (e.g. an active log file). Currently fails on Windows
because CreateFile2 omits FILE_SHARE_WRITE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix #2435: allow mmap to open files held open for writing

Add FILE_SHARE_WRITE to the share mode passed to ::CreateFile2 so
detail::mmap can open a file even when another process holds it open
with GENERIC_WRITE (e.g. an active log file). Without this, CreateFile2
fails with ERROR_SHARING_VIOLATION because the new opener's share mode
must permit the existing handle's access mode.

This brings the Windows path's behavior in line with the POSIX path
which uses ::open(O_RDONLY) and is unaffected by other processes'
write handles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:42:38 +09:00
DavidKorczynski 0cbeafe6a4 Add client fuzzing harness (#2437)
Cover client request processing logic. The goal is to enable this
running on OSS-Fuzz.

Signed-off-by: David Korczynski <david@adalogics.com>
2026-04-29 11:05:29 +09:00
yhirose 13e866bdb0 Use SHARDS=1 for macOS mbedTLS to stop residual flakiness
The macos-latest runner is consistently slower than ubuntu-latest for
the ASAN+mbedTLS test binary, and SHARDS=2 still flakes there on the
ServerTest fixture's rapid bind/connect cycle against a fixed port.
Serialize fully (SHARDS=1) on macOS only; ubuntu mbedTLS stays at 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 11:01:10 +09:00
yhirose db6c9ef27b Drop mbedTLS continue-on-error now that the matrix is stable
With the close_notify mid-response fix and SHARDS=2 mitigation, the
mbedTLS legs run reliably on both ubuntu and macos. Drop the
continue-on-error escape hatch so future regressions actually break the
build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:38:45 +09:00
yhirose 887837c65b Run mbedTLS test shards with SHARDS=2 to reduce flakiness
Under ASAN+mbedTLS, the default 4-way sharding loads CI runners enough
that timing-sensitive ServerTest cases (Delete, PostMethod2, GetStreamed,
...) flake on what looks like first-request keep-alive reuse. Reducing
to 2 shards halves contention and historically stabilizes these on local
runs. The total test time goes up roughly 1.5x (still well under the job
budget) which is an acceptable trade for reliability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:27:18 +09:00
yhirose 3d56762d5c Fix mbedTLS close_notify mid-response handling
The mbedTLS backend's read() returned -1 with err.code = PeerClosed when
the peer sent close_notify, while OpenSSL and wolfSSL surface it as 0
(clean EOF). The result was that an SSL response without Content-Length
or chunked Transfer-Encoding — terminated by connection close — was
reported as "Failed to read connection" on mbedTLS, even though the
body had been fully delivered.

Translate PeerClosed into a return value of 0 to match the other
backends. This re-enables SSLTest.ResponseBodyTerminatedByConnectionClose
on mbedTLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:04:10 +09:00
yhirose 109e331068 Exclude *_Online tests from default CI runs
These tests reach out to external services (httpbin, YouTube, ...) and
flake on CI runners whenever those services are slow or unreachable.
The previous shard runner script silently masked these failures; now
that runs report them faithfully, default the filter to -*_Online.

Override via workflow_dispatch + the gtest_filter input to include
them when explicitly desired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:41:39 +09:00
yhirose 2ea632264d Skip mbedTLS-specific SSL test; allow flaky mbedTLS jobs
Skip SSLTest.ResponseBodyTerminatedByConnectionClose under
CPPHTTPLIB_MBEDTLS_SUPPORT until the close_notify-mid-response handling
is brought into parity with the OpenSSL and wolfSSL backends. The test
verifies a successful read past the server's close, which mbedTLS
currently reports as an I/O error.

Mark the mbedTLS matrix legs (ubuntu and macos) as
continue-on-error: true. Several timing-sensitive ServerTest cases
(PostMethod2, GetStreamed, Brotli, ...) flake under ASAN+mbedTLS in
ways unrelated to cpp-httplib code; isolating these into a non-blocking
slot keeps master green while the flakiness is investigated separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:30:36 +09:00
yhirose 511cc02278 Suppress wolfSSL library leaks; remove fail-fast from test matrix
Add a libwolfssl entry to lsan_suppressions.txt to mirror the existing
libcrypto rule: the wolfSSL ECC subsystem caches per-handshake buffers
that are only freed at library shutdown, which the test binaries do
not perform. These are not leaks in cpp-httplib code.

Disable fail-fast on the ubuntu / macos / windows matrices so a failure
in one TLS backend does not cancel the others; with the runner now
detecting failures correctly, we want to see the full picture per run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:55:09 +09:00
yhirose f50bd311fb Fix MakeFileBody/MakeFileProvider tests on Windows
These tests wrote to a hardcoded "/tmp/" path which does not exist on
Windows, causing the file write to silently fail and the subsequent
make_file_body / make_file_provider call to return zero-sized data.
Use a relative path under the test working directory instead so the
test runs identically on every platform.

Also dump the shard log when a shard's process exits non-zero even
when the gtest summary appears clean (e.g. sanitizer report after
the suite, or assertion-based abort) — previously such failures were
detected only via overall rc and showed no diagnostic output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:33:51 +09:00
yhirose b0866cff8f Detect failing tests in parallel shard runner
The previous logic considered a shard "passed" if its log contained any
[  PASSED  ] line, missing the case where some tests pass and some fail
(both [  PASSED  ] N tests. and [  FAILED  ] M tests, listed below:
appear in the gtest summary). Exit codes from the test binaries were
also ignored.

Now require both: an [  PASSED  ] line, no [  FAILED  ] line, and a
zero exit code. Track each shard's PID so wait can surface non-zero
exits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:03:37 +09:00
yhirose 5ebbfeef0b Fix #2431: drop getaddrinfo_a path to eliminate stack-use-after-free (#2436)
The Linux/glibc branch of detail::getaddrinfo_with_timeout used
getaddrinfo_a(GAI_NOWAIT) with a stack-local struct gaicb. On the
connection-timeout branch it called gai_cancel(), which is non-blocking
and may return EAI_NOTCANCELED -- in that case the resolver worker
thread is still alive and writes back to ar_result on the now-destroyed
stack frame after the function has already returned.

Drop the entire #elif _GNU_SOURCE && __GLIBC__ branch and let glibc
fall through to the existing std::thread + std::shared_ptr<State>
implementation that the file already uses for other Unix systems. That
path captures shared ownership in the resolver lambda, so the state
outlives the caller's frame whether or not the worker finishes in
time -- no stack frame is ever referenced after return.

The reproducer added in #2433 (issue-2431 repro CI job) goes from
hanging at job teardown to passing in ~25s with this change.
2026-04-28 18:34:14 +09:00
yhirose d14e4fc05f Reproducer test for #2431 (getaddrinfo_a use-after-free) (#2433)
* Add reproducer for #2431 (getaddrinfo_a use-after-free)

On Linux/glibc, getaddrinfo_with_timeout() runs DNS asynchronously via
getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb. When gai_suspend()
hits the connection timeout, gai_cancel() is called and the function
returns immediately — but gai_cancel() is non-blocking and can return
EAI_NOTCANCELED, leaving the resolver worker thread alive and still
referencing the destroyed stack frame.

Adds three opt-in gtest cases (GetAddrInfoAsyncCancelTest.*) that
exercise the cancel path repeatedly. They are gated on Linux/glibc +
CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO at compile time, and on the
CPPHTTPLIB_TEST_ISSUE_2431=1 env var at runtime, so a normal `make
test` run is unaffected.

Also adds a dedicated CI job (issue-2431-repro) and a Docker-based
local runner (test/run_issue_2431_repro.sh) that sinkhole UDP/53 so
the timeout branch is taken, and run the test under ASAN/LSAN. With
the bug present these runs are expected to fail; with a fix applied
they should pass.

Refs: https://github.com/yhirose/cpp-httplib/issues/2431

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix split build for #2431 reproducer tests

The new GetAddrInfoAsyncCancelTest cases call detail::getaddrinfo_with_timeout
directly. In split builds (make test_split) split.py moves the definition into
httplib.cc and strips `inline`, so the symbol is not declared in the public
httplib.h and test.cc fails to compile -- breaking the ubuntu/test-no-exceptions
CI jobs that the PR description says should be unaffected.

Add a forward declaration in test.cc, gated by the same #if as the tests
themselves, so it links against the split-build symbol without changing the
header-only build.

* Cap issue-2431 repro job at 5 minutes

The bug manifests as orphan getaddrinfo_a resolver workers that keep the
runner from completing job teardown -- the previous run had all steps
succeed in ~1m37s but then hung in "Cleaning up orphan processes" for
~57m before GitHub force-killed the job.

A job-level timeout-minutes makes the failure signal fast and predictable:
bug present -> killed at 5 min, bug fixed -> ~2 min pass. Step-level timeout
isn't enough since the hang is in post-job cleanup, not the test step.

* Enable ASAN detect_stack_use_after_return for #2431 repro

The bug is a textbook stack-use-after-return: a stack-local struct gaicb
is destroyed when getaddrinfo_with_timeout returns after gai_cancel()
yields EAI_NOTCANCELED, then the still-live resolver worker thread writes
back into the freed frame. ASAN's detect_stack_use_after_return is the
direct detector for exactly this pattern -- enabling it lets the failure
surface as a clear ASAN diagnostic during the test run instead of as an
orphan-process hang at job teardown.

* Revert ASAN detect_stack_use_after_return for #2431 repro

The option did not detect the bug in CI -- the resolver worker write
likely lands on the heap (via the gaicb's pai pointer) or happens after
the test process exits, neither of which stack-use-after-return can
catch. Roll back to relying on the job-level timeout: bug present ->
post-cleanup hangs ~8min then job-level timeout cancels at 10min total;
bug fixed -> job completes in ~2min.

* Switch issue-2431 repro to a delayed loopback DNS test fixture

The previous repro setup dropped UDP/53 outright, which made glibc's
resolver hang forever on every lookup -- the worker never actually
received a response and so never reached the buggy write-back path
that #2431 is about. As a result, neither the broken HEAD nor the
fix made any visible difference in CI: both produced "tests pass +
post-cleanup hangs ~10min" because the orphan resolver thread is a
structural property of *any* getaddrinfo path on a hung resolver,
not a property of the bug.

Replace the sinkhole with a small loopback test fixture
(test/dns_test_fixture.py, ~50 lines, stdlib only) that answers DNS
queries after a 3s delay -- longer than the test's 1s timeout. An
iptables NAT rule routes the test job's lookups to the fixture
without touching /etc/resolv.conf, so the rest of the runner's DNS
behaviour is unaffected.

With ASAN's detect_stack_use_after_return enabled, the worker's
late write-back into the destroyed gaicb stack frame is now caught
as a stack-use-after-return diagnostic, so the broken HEAD fails
fast at the test step (clear red) and the fix turns the same job
green in well under a minute.

Same fixture is wired into both the GitHub Actions job and the
docker-based test/run_issue_2431_repro.sh script, so local repro on
macOS and CI repro on Linux exercise the identical path.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:17:19 +09:00
yhirose 33bc1df930 Release v0.43.1 2026-04-20 01:48:27 -04:00
yhirose 02d3825149 Fix Windows build error 2026-04-20 01:39:51 -04:00
yhirose 9f41fc0447 Release v0.43.0 2026-04-19 20:18:52 -04:00
yhirose 3cedf31d4c Fix #2427 (#2428)
* Fix #2427

* Use setarch -R on Linux to fix ASAN crash on WSL2

WSL2 uses high-entropy ASLR which conflicts with ASAN's shadow memory
requirements, causing the ASAN runtime to crash at startup. Running tests
via setarch -R (ADDR_NO_RANDOMIZE) disables ASLR for the test process,
allowing ASAN to initialize correctly.
2026-04-13 23:19:31 -04:00
yhirose cc8f270d4b Fix test style for ResponseBodyTerminatedByConnectionClose
Use HOST/PORT constants and scope_exit cleanup pattern
to match the rest of the SSL test suite.
2026-04-13 20:41:56 -04:00
Kukodam 9f52821be6 fix #2429 (#2430) 2026-04-13 20:32:04 -04:00
yhirose b045ee7f6b Fix #2424 2026-04-12 17:31:32 -04:00
Andrea Pappacoda cb3fce964d fix: cast len to 64 bits before right shift in ws (#2426)
Fixes WebSocketIntegrationTest.LargeMessage and
WebSocketIntegrationTest.MaxPayloadAtLimit on i386
2026-04-12 17:27:16 -04:00
yhirose 7e2a173072 Fix #2425 2026-04-12 17:25:41 -04:00
yhirose ee5d15c842 Let dynamic threads wait for work instead of exiting immediately
Previously, dynamic threads exited as soon as their current task
completed and the queue was empty. This caused excessive thread
creation/destruction under bursty or long-lived workloads (e.g., SSE
streaming), degrading tail latency. Now dynamic threads loop back and
wait for CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT (3s) before exiting,
allowing them to be reused for subsequent tasks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:50:38 -04:00
yhirose 09d00c099c Update README 2026-04-11 22:24:04 -04:00
yhirose 6bdd657713 Enhance WebSocket support with unresponsive-peer detection and documentation updates
- Added `set_websocket_max_missed_pongs` method to configure unresponsive-peer detection.
- Updated README and documentation to clarify WebSocket limitations and features.
- Introduced tests for detecting non-responsive peers and ensuring responsive peers do not trigger timeouts.
2026-04-11 22:17:38 -04:00
yhirose b4eec3ee77 Removed deprecated APIs (#2423) 2026-04-11 20:54:06 -04:00
yhirose c0248ff7fc Add links to other topics in Cookbook documents 2026-04-11 20:40:08 -04:00
yhirose 203e1bf2ac Code cleanup 2026-04-11 20:40:08 -04:00
102 changed files with 1555 additions and 815 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ jobs:
- name: Build (Win32)
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set VSDIR=%%i
call "%VSDIR%\VC\Auxiliary\Build\vcvarsall.bat" x86 || exit /b 1
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
test-arm32:
+106 -6
View File
@@ -25,7 +25,9 @@ concurrency:
cancel-in-progress: true
env:
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '*' }}
# Exclude *_Online tests by default — they hit external services and flake on
# CI runners. Run with workflow_dispatch + a custom filter to include them.
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '-*_Online' }}
jobs:
style-check:
@@ -75,6 +77,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
strategy:
fail-fast: false
matrix:
tls_backend: [openssl, mbedtls, wolfssl]
name: ubuntu (${{ matrix.tls_backend }})
@@ -101,7 +104,10 @@ jobs:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
- name: build and run tests (Mbed TLS)
if: matrix.tls_backend == 'mbedtls'
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
# Run mbedTLS shards with reduced parallelism — under ASAN+mbedTLS the
# default 4 shards overload CI runners enough that timing-sensitive
# ServerTest cases flake on first-request keep-alive reuse.
run: cd test && make test_split_mbedtls && SHARDS=2 make test_mbedtls_parallel
- name: build and run tests (wolfSSL)
if: matrix.tls_backend == 'wolfssl'
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
@@ -114,6 +120,92 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
# When gai_suspend() hits the connection timeout, gai_cancel() is called
# but does not block; the resolver worker can later write back into the
# destroyed stack frame. To make the worker actually reach that write,
# the test job runs a loopback UDP responder (test/dns_test_fixture.py)
# that delays its reply past the test's 1s timeout, and uses an iptables
# NAT rule so glibc's lookups land on that fixture instead of a real
# nameserver. With ASAN's detect_stack_use_after_return enabled, the
# late write-back is reported as a stack-use-after-return.
issue-2431-repro:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
name: issue-2431 repro (Linux + ASAN)
# Bound the whole job in case anything in the test harness hangs
# unexpectedly. With the fixture in place a normal run is well under
# a minute either way (ASAN abort on broken HEAD, clean pass on fix).
timeout-minutes: 5
env:
DNS_FIXTURE_PORT: "15353"
DNS_FIXTURE_DELAY: "3"
steps:
- name: checkout
uses: actions/checkout@v4
- name: install libraries
run: |
sudo apt-get update
sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \
libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2
- name: start loopback DNS test fixture
run: |
# Force glibc through its DNS code path: Ubuntu's default
# nsswitch short-circuits to NOTFOUND through mdns4_minimal,
# which would skip the buggy code entirely.
sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf
# Run the loopback fixture (delayed UDP responder).
python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
>/tmp/dns_fixture.log 2>&1 &
echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null
# Wait for the fixture to start listening.
for _ in $(seq 1 50); do
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
break
fi
sleep 0.1
done
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \
|| { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; }
# Send the test process's DNS lookups to the loopback fixture.
# NAT only the local OUTPUT chain; conntrack handles the reply path.
sudo iptables -t nat -I OUTPUT -p udp --dport 53 \
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
# Sanity check: a query must take at least the fixture delay
# and resolve to NXDOMAIN (proving traffic reaches the fixture).
start=$(date +%s)
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
elapsed=$(( $(date +%s) - start ))
if [ "$elapsed" -lt 2 ]; then
echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2
exit 1
fi
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
- name: build test binary
run: cd test && make test
- name: run GetAddrInfoAsyncCancelTest
run: |
cd test
ARCH=$(uname -m)
CPPHTTPLIB_TEST_ISSUE_2431=1 \
ASAN_OPTIONS=detect_stack_use_after_return=1 \
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
setarch "$ARCH" -R \
./test --gtest_filter='GetAddrInfoAsyncCancelTest.*'
- name: tear down test fixture
if: always()
run: |
sudo iptables -t nat -F OUTPUT || true
if [ -f /tmp/dns_fixture.pid ]; then
sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true
fi
macos:
runs-on: macos-latest
if: >
@@ -122,6 +214,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
strategy:
fail-fast: false
matrix:
tls_backend: [openssl, mbedtls, wolfssl]
name: macos (${{ matrix.tls_backend }})
@@ -141,7 +234,10 @@ jobs:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
- name: build and run tests (Mbed TLS)
if: matrix.tls_backend == 'mbedtls'
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
# macOS runners under ASAN+mbedTLS still flake at SHARDS=2 (rapid
# bind/connect on the fixture's fixed port races on the slower
# macos-latest runner). Serialize fully here; ubuntu stays at 2.
run: cd test && make test_split_mbedtls && SHARDS=1 make test_mbedtls_parallel
- name: build and run tests (wolfSSL)
if: matrix.tls_backend == 'wolfssl'
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
@@ -162,6 +258,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_windows == 'true')
strategy:
fail-fast: false
matrix:
config:
- with_ssl: false
@@ -228,7 +325,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 || '*' }}" `
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '-*_Online' }}" `
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
}
@@ -236,11 +333,14 @@ jobs:
$failed = $false
for ($i = 0; $i -lt $shards; $i++) {
$log = "shard_${i}.log"
if (Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet) {
$proc = $procs[$i]
$hasPassed = Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet
$hasFailed = Select-String -Path $log -Pattern "\[ FAILED \]" -Quiet
if ($hasPassed -and -not $hasFailed -and $proc.ExitCode -eq 0) {
$passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line
Write-Host "Shard ${i}: $passed"
} else {
Write-Host "=== Shard $i FAILED ==="
Write-Host "=== Shard $i FAILED (exit=$($proc.ExitCode)) ==="
Get-Content $log
if (Test-Path "${log}.err") { Get-Content "${log}.err" }
$failed = $true
+3
View File
@@ -43,6 +43,9 @@ test/test_mbedtls
test/test_wolfssl
test/test_no_tls
test/server_fuzzer
test/client_fuzzer
test/header_parser_fuzzer
test/url_parser_fuzzer
test/test_proxy
test/test_proxy_mbedtls
test/test_proxy_wolfssl
+21 -2
View File
@@ -3,15 +3,19 @@
A simple, blocking WebSocket implementation for C++11.
> [!IMPORTANT]
> This is a blocking I/O WebSocket implementation using a thread-per-connection model. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
> This is a blocking I/O WebSocket implementation using a thread-per-connection model (plus one heartbeat thread per connection). It is intended for small- to mid-scale workloads; handling large numbers of simultaneous WebSocket connections is outside the design target of this library. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
> [!NOTE]
> WebSocket extensions (`permessage-deflate` and others defined by RFC 6455) are **not supported**. If a client proposes an extension via `Sec-WebSocket-Extensions`, the server silently declines it — the negotiated connection always runs without extensions.
## Features
- **RFC 6455 compliant**: Full WebSocket protocol support
- **RFC 6455 compliant**: Full WebSocket protocol support (extensions are not implemented)
- **Server and Client**: Both sides included
- **SSL/TLS support**: `wss://` scheme for secure connections
- **Text and Binary**: Both message types supported
- **Automatic heartbeat**: Periodic Ping/Pong keeps connections alive
- **Unresponsive-peer detection**: Opt-in liveness check via `set_websocket_max_missed_pongs()`
- **Subprotocol negotiation**: `Sec-WebSocket-Protocol` support for GraphQL, MQTT, etc.
## Quick Start
@@ -352,6 +356,7 @@ if (ws.connect()) {
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (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_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
### Runtime Ping Interval
@@ -373,6 +378,20 @@ ws.set_websocket_ping_interval(10); // 10 seconds
ws.set_websocket_ping_interval(0);
```
### Unresponsive-Peer Detection (Pong Timeout)
By default the heartbeat only sends pings — it does not enforce that pongs come back. To detect a silently dropped connection faster, enable the max-missed-pongs check. Once `max_missed_pongs` consecutive pings go unanswered, the heartbeat thread closes the connection with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
```cpp
ws.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
```
The server side has the same `set_websocket_max_missed_pongs()`.
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. Even so, a dead connection does not linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. `max_missed_pongs` is the knob for detecting an unresponsive peer faster than that 5-minute fallback.
## Threading Model
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
+6 -6
View File
@@ -8,7 +8,7 @@ It's extremely easy to set up. Just include the **[httplib.h](https://raw.github
Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/) (built with [docs-gen](https://github.com/yhirose/docs-gen)).
> [!IMPORTANT]
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want. Only **HTTP/1.1** is supported — HTTP/2 and HTTP/3 are not implemented.
> [!WARNING]
> 32-bit platforms are **NOT supported**. Use at your own risk. The library may compile on 32-bit targets, but no security review has been conducted for 32-bit environments. Integer truncation and other 32-bit-specific issues may exist. **Security reports that only affect 32-bit platforms will be closed without action.** The maintainer does not have access to 32-bit environments for testing or fixing issues. CI includes basic compile checks only, not functional or security testing.
@@ -1462,7 +1462,11 @@ if (ws.connect()) {
SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.com/ws")`). Subprotocol negotiation (`Sec-WebSocket-Protocol`) is supported via `SubProtocolSelector` callback.
> **Note:** WebSocket connections occupy a thread for their entire lifetime. If you plan to handle many simultaneous WebSocket connections, consider using a dynamic thread pool: `svr.new_task_queue = [] { return new ThreadPool(8, 64); };`
> **Note:** WebSocket connections occupy a thread for their entire lifetime (plus an additional thread per connection for heartbeat pings). This thread-per-connection model is intended for small- to mid-scale workloads; large numbers of simultaneous WebSocket connections are outside the design target of this library. If you expect many concurrent WebSocket clients, configure a dynamic thread pool (`svr.new_task_queue = [] { return new ThreadPool(8, 64); };`) and measure carefully.
> **WebSocket extensions are not supported.** `permessage-deflate` and other RFC 6455 extensions are not implemented. If a client proposes them via `Sec-WebSocket-Extensions`, the server silently declines them in its handshake response.
> **Unresponsive-peer detection.** Heartbeat pings also serve as a liveness probe when `set_websocket_max_missed_pongs(n)` is set: if the client sends `n` consecutive pings without receiving a pong, it will close the connection. Disabled by default (`0`).
See [README-websocket.md](README-websocket.md) for more details.
@@ -1564,10 +1568,6 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
> [!NOTE]
> cpp-httplib officially supports only the latest Visual Studio. It might work with former versions of Visual Studio, but I can no longer verify it. Pull requests are always welcome for the older versions of Visual Studio unless they break the C++11 conformance.
### Deprecated APIs
A handful of older APIs are marked `[[deprecated]]` and are scheduled to be removed **by v1.0.0**. If you see a deprecation warning at build time, please migrate to the replacement shown in the message — for example, `Client::set_url_encode()` → `set_path_encode()`, `SSLClient::set_ca_cert_store()` → `load_ca_cert_store()`, and `Result::ssl_openssl_error()` → `ssl_backend_error()`. The full list lives alongside the `[[deprecated]]` attributes in `httplib.h`.
> [!NOTE]
> Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.42.0"
version = "0.44.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
@@ -57,4 +57,4 @@ Return `false` from the callback to abort the download. In the example above, if
>
> The `ResponseHandler` is called after headers arrive but before the body. Return `false` to skip the download entirely.
> To show download progress, see C11. Use the progress callback.
> To show download progress, see [C11. Use the progress callback](c11-progress-callback).
+2 -2
View File
@@ -31,6 +31,6 @@ if (res && res->status == 200) {
`res->body` is a `std::string`, so you can pass it straight to your JSON library.
> **Note:** Servers sometimes return HTML on errors. Check the status code before parsing to be safe. Some APIs also require an `Accept: application/json` header. If you're calling a JSON API repeatedly, C03. Set default headers can save you some boilerplate.
> **Note:** Servers sometimes return HTML on errors. Check the status code before parsing to be safe. Some APIs also require an `Accept: application/json` header. If you're calling a JSON API repeatedly, [C03. Set default headers](c03-default-headers) can save you some boilerplate.
> For how to receive and return JSON on the server side, see S02. Receive JSON requests and return JSON responses.
> For how to receive and return JSON on the server side, see [S02. Receive JSON requests and return JSON responses](s02-json-api).
@@ -52,4 +52,4 @@ auto res = cli.Get("/users", headers);
Per-request headers are **added** on top of the defaults. Both are sent to the server.
> For details on Bearer token auth, see C06. Call an API with a Bearer Token.
> For details on Bearer token auth, see [C06. Call an API with a Bearer token](c06-bearer-token).
@@ -35,4 +35,4 @@ Many sites redirect HTTP traffic to HTTPS. With `set_follow_location(true)` on,
> **Warning:** To follow redirects to HTTPS, you need to build cpp-httplib with OpenSSL (or another TLS backend). Without TLS support, redirects to HTTPS will fail.
> **Note:** Following redirects adds to the total request time. See C12. Set Timeouts for timeout configuration.
> **Note:** Following redirects adds to the total request time. See [C12. Set timeouts](c12-timeouts) for timeout configuration.
+1 -1
View File
@@ -43,4 +43,4 @@ For the more secure Digest authentication scheme, use `set_digest_auth()`. This
cli.set_digest_auth("alice", "s3cret");
```
> To call an API with a Bearer token, see C06. Call an API with a Bearer Token.
> To call an API with a Bearer token, see [C06. Call an API with a Bearer token](c06-bearer-token).
@@ -47,4 +47,4 @@ if (res && res->status == 401) {
> **Warning:** A Bearer token is itself a credential. Always send it over HTTPS, and never hard-code it into source or config files.
> To set multiple headers at once, see C03. Set Default Headers.
> To set multiple headers at once, see [C03. Set default headers](c03-default-headers).
@@ -49,4 +49,4 @@ The arguments to `make_file_provider()` are `(form name, file path, file name, c
> **Note:** You can mix `UploadFormDataItems` and `FormDataProviderItems` in the same request. A clean split is: text fields in `UploadFormDataItems`, files in `FormDataProviderItems`.
> To show upload progress, see C11. Use the progress callback.
> To show upload progress, see [C11. Use the progress callback](c11-progress-callback).
@@ -31,4 +31,4 @@ If the file can't be opened, `make_file_body()` returns `size` as `0` and `provi
> **Warning:** `make_file_body()` needs to fix the Content-Length up front, so it reads the file size ahead of time. If the file size might change mid-upload, this API isn't the right fit.
> To send the file as multipart form data instead, see C07. Upload a File as Multipart Form Data.
> To send the file as multipart form data instead, see [C07. Upload a file as multipart form data](c07-multipart-upload).
@@ -44,4 +44,4 @@ With a known size, the request carries a Content-Length header — so the server
> **Detail:** `sink.write()` returns a `bool` indicating whether the write succeeded. If it returns `false`, the connection is gone — return `false` from the lambda to stop.
> If you're just sending a file, `make_file_body()` is easier. See C08. POST a File as Raw Binary.
> If you're just sending a file, `make_file_body()` is easier. See [C08. POST a file as raw binary](c08-post-file-body).
@@ -48,5 +48,5 @@ Accumulate into a buffer, then pull out and parse one line each time you see a n
> **Warning:** When you pass a `ContentReceiver`, `res->body` stays **empty**. Store or process the body inside the callback yourself.
> To track download progress, combine this with C11. Use the Progress Callback.
> For Server-Sent Events (SSE), see E04. Receive SSE on the Client.
> To track download progress, combine this with [C11. Use the progress callback](c11-progress-callback).
> For Server-Sent Events (SSE), see [E04. Receive SSE on the client](e04-sse-client).
@@ -56,4 +56,4 @@ auto res = cli.Get("/large-file",
> **Note:** `ContentReceiver` and the progress callback can be used together. When you want to stream to a file and show progress at the same time, pass both.
> For a concrete example of saving to a file, see C01. Get the Response Body / Save to a File.
> For a concrete example of saving to a file, see [C01. Get the response body / save to a file](c01-get-response-body).
+1 -1
View File
@@ -47,4 +47,4 @@ cli.set_connection_timeout(3s);
cli.set_read_timeout(10s);
```
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use C13. Set an Overall Timeout.
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use [C13. Set an overall timeout](c13-max-timeout).
@@ -4,7 +4,7 @@ order: 13
status: "draft"
---
The three timeouts from C12. Set Timeouts all apply to a single `send` or `recv` call. To cap the total time a request can take, use `set_max_timeout()`.
The three timeouts from [C12. Set timeouts](c12-timeouts) all apply to a single `send` or `recv` call. To cap the total time a request can take, use `set_max_timeout()`.
## Basic usage
+1 -1
View File
@@ -38,7 +38,7 @@ cli.set_proxy_digest_auth("user", "password");
## Combine with end-server authentication
Proxy authentication is separate from authenticating to the end server (C05, C06). When both are needed, set both.
Proxy authentication is separate from authenticating to the end server ([C05. Use Basic authentication](c05-basic-auth), [C06. Call an API with a Bearer token](c06-bearer-token)). When both are needed, set both.
```cpp
cli.set_proxy("proxy.internal", 8080);
@@ -60,4 +60,4 @@ std::cout << res->body << std::endl;
Keep them separated in your head: network-layer errors go through `res.error()`, HTTP-level errors through `res->status`.
> To dig deeper into SSL-related errors, see C18. Handle SSL Errors.
> To dig deeper into SSL-related errors, see [C18. Handle SSL errors](c18-ssl-errors).
+1 -3
View File
@@ -48,6 +48,4 @@ if (res.ssl_backend_error() != 0) {
| `SSLServerHostnameVerification` | The cert's CN/SAN doesn't match the host |
| `SSLConnection` | TLS version mismatch, no shared cipher suite |
> **Note:** `ssl_backend_error()` was previously called `ssl_openssl_error()`. The old name is deprecated — use `ssl_backend_error()` now.
> To change certificate verification settings, see T02. Control SSL Certificate Verification.
> To change certificate verification settings, see [T02. Control SSL certificate verification](t02-cert-verification).
+3 -3
View File
@@ -56,7 +56,7 @@ svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
});
```
When the client disconnects, call `sink.done()` to stop. Details in S16. Detect When the Client Has Disconnected.
When the client disconnects, call `sink.done()` to stop. Details in [S16. Detect client disconnection](s16-disconnect).
## Heartbeats via comment lines
@@ -80,8 +80,8 @@ svr.new_task_queue = [] {
};
```
See S21. Configure the Thread Pool.
See [S21. Configure the thread pool](s21-thread-pool).
> **Note:** When `data:` contains newlines, split it into multiple `data:` lines — one per line. This is how the SSE spec requires multiline data to be transmitted.
> For event names, see E02. Use Named Events in SSE. For the client side, see E04. Receive SSE on the Client.
> For event names, see [E02. Use named events in SSE](e02-sse-event-names). For the client side, see [E04. Receive SSE on the client](e04-sse-client).
@@ -52,7 +52,7 @@ auto send_event = [](httplib::DataSink &sink,
send_event(sink, "message", "Hello!", "42");
```
The ID format is up to you. Monotonic counters or UUIDs both work — just pick something unique and orderable on the server side. See E03. Handle SSE Reconnection for details.
The ID format is up to you. Monotonic counters or UUIDs both work — just pick something unique and orderable on the server side. See [E03. Handle SSE reconnection](e03-sse-reconnect) for details.
## JSON payloads in data
+1 -1
View File
@@ -96,4 +96,4 @@ Use `last_event_id()` to read the current value.
> **Note:** `SSEClient::start()` blocks, which is fine for a one-off command-line tool. For GUI apps or embedded in a server, the `start_async()` + `stop()` pair is the usual pattern.
> For the server side, see E01. Implement an SSE Server.
> For the server side, see [E01. Implement an SSE server](e01-sse-server).
+2 -2
View File
@@ -61,6 +61,6 @@ svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
To add a response header, use `res.set_header("Name", "Value")`.
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see S18. Control Startup Order with `listen_after_bind`.
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind).
> To use path parameters like `/users/:id`, see S03. Use Path Parameters.
> To use path parameters like `/users/:id`, see [S03. Use path parameters](s03-path-params).
+2 -2
View File
@@ -69,6 +69,6 @@ svr.Get("/api/health", [&](const auto &req, auto &res) {
});
```
> **Note:** A large JSON body ends up entirely in `req.body`, which means it all sits in memory. For huge payloads, consider streaming reception — see S07. Receive Multipart Data as a Stream.
> **Note:** A large JSON body ends up entirely in `req.body`, which means it all sits in memory. For huge payloads, consider streaming reception — see [S07. Receive multipart data as a stream](s07-multipart-reader).
> For the client side, see C02. Send and Receive JSON.
> For the client side, see [C02. Send and receive JSON](c02-json).
@@ -52,4 +52,4 @@ svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
> **Warning:** The static file server methods are **not thread-safe**. Don't call them after `listen()` — configure everything before starting the server.
> For download-style responses, see S06. Return a File Download Response.
> For download-style responses, see [S06. Return a file download response](s06-download-response).
@@ -60,4 +60,4 @@ Call `sink.done()` to signal the end.
> **Note:** The provider lambda is called multiple times. Watch out for the lifetime of captured variables — wrap them in a `std::shared_ptr` if needed.
> To serve the file as a download, see S06. Return a File Download Response.
> To serve the file as a download, see [S06. Return a file download response](s06-download-response).
@@ -68,4 +68,4 @@ Only a small chunk sits in memory at any moment, so gigabyte-scale files are no
> **Warning:** When you use `HandlerWithContentReader`, `req.body` stays **empty**. Handle the body yourself inside the callbacks.
> For the client side of multipart uploads, see C07. Upload a File as Multipart Form Data.
> For the client side of multipart uploads, see [C07. Upload a file as multipart form data](c07-multipart-upload).
@@ -50,4 +50,4 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
> **Note:** Tiny responses barely benefit from compression and just waste CPU time. cpp-httplib skips compression for bodies that are too small to bother with.
> For the client-side counterpart, see C15. Enable Compression.
> For the client-side counterpart, see [C15. Enable compression](c15-compression).
@@ -49,6 +49,6 @@ If auth fails, return `Handled` to respond with 401 immediately. If it passes, r
## For per-route auth
If you want different auth rules per route rather than a single global check, `set_pre_request_handler()` is a better fit. See S11. Authenticate Per Route with a Pre-Request Handler.
If you want different auth rules per route rather than a single global check, `set_pre_request_handler()` is a better fit. See [S11. Authenticate per route with a pre-request handler](s11-pre-request).
> **Note:** If all you want is to modify the response, `set_post_routing_handler()` is the right tool. See S10. Add Response Headers with a Post-Routing Handler.
> **Note:** If all you want is to modify the response, `set_post_routing_handler()` is the right tool. See [S10. Add response headers with a post-routing handler](s10-post-routing).
@@ -4,7 +4,7 @@ order: 30
status: "draft"
---
The `set_pre_routing_handler()` from S09 runs **before routing**, so it has no idea which route matched. When you want per-route behavior, `set_pre_request_handler()` is what you need.
The `set_pre_routing_handler()` from [S09. Add pre-processing to all routes](s09-pre-routing) runs **before routing**, so it has no idea which route matched. When you want per-route behavior, `set_pre_request_handler()` is what you need.
## Pre-routing vs. pre-request
@@ -44,4 +44,4 @@ Same as pre-routing — return `HandlerResponse`.
## Passing auth info to the route handler
To pass decoded user info into the route handler, use `res.user_data`. See S12. Pass Data Between Handlers with `res.user_data`.
To pass decoded user info into the route handler, use `res.user_data`. See [S12. Pass data between handlers with `res.user_data`](s12-user-data).
@@ -48,4 +48,4 @@ svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
Now every error comes back in a consistent JSON shape.
> **Note:** `set_error_handler()` also fires for 500 responses caused by exceptions thrown from a route handler. To get at the exception itself, combine it with `set_exception_handler()`. See S14. Catch Exceptions.
> **Note:** `set_error_handler()` also fires for 500 responses caused by exceptions thrown from a route handler. To get at the exception itself, combine it with `set_exception_handler()`. See [S14. Catch exceptions](s14-exception-handler).
@@ -59,6 +59,6 @@ svr.set_logger([](const auto &req, const auto &res) {
});
```
For more on `user_data`, see S12. Pass Data Between Handlers with `res.user_data`.
For more on `user_data`, see [S12. Pass data between handlers with `res.user_data`](s12-user-data).
> **Note:** The logger runs synchronously on the same thread as request processing. Heavy work inside it hurts throughput — push it to a queue and process asynchronously if you need anything expensive.
@@ -49,4 +49,4 @@ Because the port is assigned at runtime, parallel test runs don't collide.
> **Note:** `bind_to_any_port()` returns `-1` on failure (permission errors, no available ports, etc.). Always check the return value.
> To stop the server, see S19. Shut Down Gracefully.
> To stop the server, see [S19. Shut down gracefully](s19-graceful-shutdown).
@@ -54,4 +54,4 @@ if (!svr.bind_to_port("0.0.0.0", 8080)) {
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
> **Note:** To auto-pick a free port, see S17. Bind to Any Available Port. Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
+2 -2
View File
@@ -52,6 +52,6 @@ Set `set_keep_alive_max_count(1)` and every request gets its own connection. Mos
## Relationship with the thread pool
A Keep-Alive connection holds a worker thread for its entire lifetime. If `connections × concurrent requests` exceeds the thread pool size, new requests wait. For thread counts, see S21. Configure the Thread Pool.
A Keep-Alive connection holds a worker thread for its entire lifetime. If `connections × concurrent requests` exceeds the thread pool size, new requests wait. For thread counts, see [S21. Configure the thread pool](s21-thread-pool).
> **Note:** For the client side, see C14. Understand Connection Reuse and Keep-Alive Behavior. Even when the server closes the connection on timeout, the client reconnects automatically.
> **Note:** For the client side, see [C14. Understand connection reuse and Keep-Alive behavior](c14-keep-alive). Even when the server closes the connection on timeout, the client reconnects automatically.
@@ -42,8 +42,8 @@ The usual approach is to treat each backend as a build variant and recompile the
Certificate verification control, standing up an SSLServer, reading the peer certificate — these all share the same API across backends:
- T02. Control SSL Certificate Verification
- T03. Start an SSL/TLS Server
- T05. Access the Peer Certificate on the Server Side
- [T02. Control SSL certificate verification](t02-cert-verification)
- [T03. Start an SSL/TLS server](t03-ssl-server)
- [T05. Access the peer certificate on the server](t05-peer-cert)
> **Note:** On macOS with an OpenSSL-family backend, cpp-httplib automatically loads root certificates from the system keychain (via `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`, on by default). To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
@@ -48,6 +48,6 @@ The certificate itself is still validated, so this is safer than fully disabling
On most Linux distributions, root certificates live in a single file like `/etc/ssl/certs/ca-certificates.crt`. cpp-httplib reads the OS default store at startup, so for most servers you don't need to configure anything.
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL.
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends).
> For details on diagnosing failures, see C18. Handle SSL Errors.
> For details on diagnosing failures, see [C18. Handle SSL errors](c18-ssl-errors).
+3 -3
View File
@@ -34,7 +34,7 @@ httplib::SSLServer svr("cert.pem", "key.pem",
nullptr, nullptr, "password");
```
The third and fourth arguments are for client certificate verification (mTLS, see T04). For now, pass `nullptr`.
The third and fourth arguments are for client certificate verification (mTLS, see [T04. Configure mTLS](t04-mtls)). For now, pass `nullptr`.
## Load PEM data from memory
@@ -73,6 +73,6 @@ openssl req -x509 -newkey rsa:2048 -days 365 -nodes \
In production, use certificates from Let's Encrypt or your internal CA.
> **Warning:** Binding an HTTPS server to port 443 requires root. For a safe way to do that, see the privilege-drop pattern in S18. Control Startup Order with `listen_after_bind`.
> **Warning:** Binding an HTTPS server to port 443 requires root. For a safe way to do that, see the privilege-drop pattern in [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind).
> For mutual TLS (client certificates), see T04. Configure mTLS.
> For mutual TLS (client certificates), see [T04. Configure mTLS](t04-mtls).
+1 -1
View File
@@ -59,7 +59,7 @@ Note you're using `SSLClient` directly, not `Client`. If the private key has a p
## Read client info from a handler
To see which client connected from inside a handler, use `req.peer_cert()`. Details in T05. Access the Peer Certificate on the Server Side.
To see which client connected from inside a handler, use `req.peer_cert()`. Details in [T05. Access the peer certificate on the server](t05-peer-cert).
## Use cases
+2 -2
View File
@@ -77,7 +77,7 @@ svr.set_pre_request_handler(
});
```
Combined with a pre-request handler, you can keep all authorization logic in one place. See S11. Authenticate Per Route with a Pre-Request Handler.
Combined with a pre-request handler, you can keep all authorization logic in one place. See [S11. Authenticate per route with a pre-request handler](s11-pre-request).
## SNI (Server Name Indication)
@@ -85,4 +85,4 @@ cpp-httplib handles SNI automatically. If one server hosts multiple domains, SNI
> **Warning:** `req.peer_cert()` only returns a meaningful value when mTLS is enabled and the client actually presented a certificate. For plain TLS, you get an empty `PeerCert`. Always do the `bool` check before using it.
> To set up mTLS, see T04. Configure mTLS.
> To set up mTLS, see [T04. Configure mTLS](t04-mtls).
@@ -71,7 +71,7 @@ ws.send("Hello"); // text frame
ws.send(binary_data, binary_data_size); // binary frame
```
The `std::string` overload sends as **text**; the `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it's intuitive. See W04. Send and Receive Binary Frames for details.
The `std::string` overload sends as **text**; the `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it's intuitive. See [W04. Send and receive binary frames](w04-websocket-binary) for details.
## Thread pool implications
@@ -83,6 +83,6 @@ svr.new_task_queue = [] {
};
```
See S21. Configure the Thread Pool.
See [S21. Configure the thread pool](s21-thread-pool).
> **Note:** To run WebSocket over HTTPS, use `httplib::SSLServer` instead of `httplib::Server` — the same `WebSocket()` handler just works. On the client side, use a `wss://` URL.
@@ -57,4 +57,24 @@ Too short wastes bandwidth; too long and connections get dropped. As a rule of t
> **Warning:** A very short ping interval spawns background work per connection and increases CPU usage. For servers with many connections, keep the interval modest.
> For handling a closed connection, see W03. Handle Connection Close.
## Detecting an unresponsive peer
Sending pings alone doesn't tell you anything if the peer just silently dies — the TCP socket might still look open while the process on the other end is long gone. To catch that, enable the max-missed-pongs check: if N consecutive pings go unanswered, the connection is closed.
```cpp
cli.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
```
The server side has the same `set_websocket_max_missed_pongs()`.
With a 30-second ping interval and `max_missed_pongs = 2`, a dead peer is detected within roughly 60 seconds and the connection is closed with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
The counter is reset whenever `read()` consumes an incoming Pong frame, so this only works if your code is actively calling `read()` in a loop — which is what a normal WebSocket client does anyway.
### Why the default is 0
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
Even with `0`, a dead connection won't linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. Think of `max_missed_pongs` as the knob for detecting an unresponsive peer **faster** than that.
> For handling a closed connection, see [W03. Handle connection close](w03-websocket-close).
@@ -56,7 +56,7 @@ Binary frames still come back in a `std::string`, but treat its contents as raw
## Ping is binary-ish, but hidden
WebSocket Ping/Pong frames are close cousins of binary frames at the opcode level, but cpp-httplib handles them automatically — you don't touch them. See W02. Set a WebSocket Heartbeat.
WebSocket Ping/Pong frames are close cousins of binary frames at the opcode level, but cpp-httplib handles them automatically — you don't touch them. See [W02. Set a WebSocket heartbeat](w02-websocket-ping).
## Example: send an image
@@ -57,4 +57,4 @@ auto res = cli.Get("/large-file",
>
> `ResponseHandler`はヘッダー受信後、ボディ受信前に呼ばれます。`false`を返せばダウンロード自体をスキップできます。
> ダウンロードの進捗を表示したい場合はC11. 進捗コールバックを使うを参照してください。
> ダウンロードの進捗を表示したい場合は[C11. 進捗コールバックを使う](c11-progress-callback)を参照してください。
+2 -2
View File
@@ -31,6 +31,6 @@ if (res && res->status == 200) {
`res->body``std::string`なので、そのままJSONライブラリに渡せます。
> **Note:** サーバーがエラー時にHTMLを返すことがあります。ステータスコードを確認してからパースすると安全です。また、APIによっては`Accept: application/json`ヘッダーが必要です。JSON APIを繰り返し呼ぶならC03. デフォルトヘッダーを設定するが便利です。
> **Note:** サーバーがエラー時にHTMLを返すことがあります。ステータスコードを確認してからパースすると安全です。また、APIによっては`Accept: application/json`ヘッダーが必要です。JSON APIを繰り返し呼ぶなら[C03. デフォルトヘッダーを設定する](c03-default-headers)が便利です。
> サーバー側でJSONを受け取って返す方法はS02. JSONリクエストを受け取りJSONレスポンスを返すを参照してください。
> サーバー側でJSONを受け取って返す方法は[S02. JSONリクエストを受け取りJSONレスポンスを返す](s02-json-api)を参照してください。
@@ -52,4 +52,4 @@ auto res = cli.Get("/users", headers);
リクエスト単位で渡したヘッダーはデフォルトヘッダーに**追加**されます。両方がサーバーに送られます。
> Bearerトークンを使った認証の詳細はC06. BearerトークンでAPIを呼ぶを参照してください。
> Bearerトークンを使った認証の詳細は[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)を参照してください。
@@ -35,4 +35,4 @@ auto res = cli.Get("/");
> **Warning:** HTTPSへのリダイレクトを追従するには、cpp-httplibをOpenSSL(または他のTLSバックエンド)付きでビルドしておく必要があります。TLSサポートがないと、HTTPSへのリダイレクトは失敗します。
> **Note:** リダイレクトを追従すると、リクエストの実行時間は伸びます。タイムアウトの設定はC12. タイムアウトを設定するを参照してください。
> **Note:** リダイレクトを追従すると、リクエストの実行時間は伸びます。タイムアウトの設定は[C12. タイムアウトを設定する](c12-timeouts)を参照してください。
+1 -1
View File
@@ -43,4 +43,4 @@ auto res = cli.Get("/private", headers);
cli.set_digest_auth("alice", "s3cret");
```
> BearerトークンでAPIを呼びたい場合はC06. BearerトークンでAPIを呼ぶを参照してください。
> BearerトークンでAPIを呼びたい場合は[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)を参照してください。
@@ -47,4 +47,4 @@ if (res && res->status == 401) {
> **Warning:** Bearerトークンはそれ自体が認証情報です。必ずHTTPS経由で送ってください。また、ソースコードや設定ファイルにトークンをハードコードしないようにしましょう。
> 複数のヘッダーをまとめて設定したいときはC03. デフォルトヘッダーを設定するも便利です。
> 複数のヘッダーをまとめて設定したいときは[C03. デフォルトヘッダーを設定する](c03-default-headers)も便利です。
@@ -49,4 +49,4 @@ auto res = cli.Post("/upload", httplib::Headers{}, items, provider_items);
> **Note:** `UploadFormDataItems``FormDataProviderItems`は同じリクエスト内で併用できます。テキストフィールドは`UploadFormDataItems`、ファイルは`FormDataProviderItems`、という使い分けがきれいです。
> アップロードの進捗を表示したい場合はC11. 進捗コールバックを使うを参照してください。
> アップロードの進捗を表示したい場合は[C11. 進捗コールバックを使う](c11-progress-callback)を参照してください。
@@ -31,4 +31,4 @@ auto res = cli.Put("/bucket/backup.tar.gz", size,
> **Warning:** `make_file_body()`はContent-Lengthを最初に確定させる必要があるため、ファイルサイズをあらかじめ取得します。送信中にファイルサイズが変わる可能性がある場合は、このAPIには向きません。
> マルチパート形式で送りたい場合はC07. ファイルをマルチパートフォームとしてアップロードするを参照してください。
> マルチパート形式で送りたい場合は[C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)を参照してください。
@@ -44,4 +44,4 @@ auto res = cli.Post("/upload", total_size,
> **Detail:** `sink.write()`は書き込みが成功したかどうかを`bool`で返します。`false`が返ったら回線が切れています。ラムダはそのまま`false`を返して終了しましょう。
> ファイルをそのまま送るだけなら、`make_file_body()`が便利です。C08. ファイルを生バイナリとしてPOSTするを参照してください。
> ファイルをそのまま送るだけなら、`make_file_body()`が便利です。[C08. ファイルを生バイナリとしてPOSTする](c08-post-file-body)を参照してください。
@@ -48,5 +48,5 @@ auto res = cli.Get("/events",
> **Warning:** `ContentReceiver`を渡すと、`res->body`は**空のまま**になります。ボディは自分でコールバック内で保存するか処理するかしてください。
> ダウンロードの進捗を知りたい場合はC11. 進捗コールバックを使うと組み合わせましょう。
> Server-Sent EventsSSE)を扱うときはE04. SSEをクライアントで受信するも参考になります。
> ダウンロードの進捗を知りたい場合は[C11. 進捗コールバックを使う](c11-progress-callback)と組み合わせましょう。
> Server-Sent EventsSSE)を扱うときは[E04. SSEをクライアントで受信する](e04-sse-client)も参考になります。
@@ -56,4 +56,4 @@ auto res = cli.Get("/large-file",
> **Note:** `ContentReceiver`と進捗コールバックは同時に使えます。ファイルに書き出しながら進捗を表示したいときは、両方を渡しましょう。
> ファイル保存と組み合わせる具体例はC01. レスポンスボディを取得する / ファイルに保存するも参照してください。
> ファイル保存と組み合わせる具体例は[C01. レスポンスボディを取得する / ファイルに保存する](c01-get-response-body)も参照してください。
+1 -1
View File
@@ -47,4 +47,4 @@ cli.set_connection_timeout(3s);
cli.set_read_timeout(10s);
```
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合はC13. 全体タイムアウトを設定するを使ってください。
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合は[C13. 全体タイムアウトを設定する](c13-max-timeout)を使ってください。
@@ -4,7 +4,7 @@ order: 13
status: "draft"
---
C12. タイムアウトを設定するで紹介した3種類のタイムアウトは、いずれも「1回の`send``recv`」に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
[C12. タイムアウトを設定する](c12-timeouts)で紹介した3種類のタイムアウトは、いずれも「1回の`send``recv`」に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
## 基本の使い方
+1 -1
View File
@@ -38,7 +38,7 @@ cli.set_proxy_digest_auth("user", "password");
## エンドのサーバー認証と組み合わせる
プロキシ認証と、エンドサーバーへの認証(C05やC06)は別物です。両方が必要なら、両方設定します。
プロキシ認証と、エンドサーバーへの認証([C05. Basic認証を使う](c05-basic-auth)や[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token))は別物です。両方が必要なら、両方設定します。
```cpp
cli.set_proxy("proxy.internal", 8080);
@@ -60,4 +60,4 @@ std::cout << res->body << std::endl;
ネットワーク層のエラーは`res.error()`、HTTPのエラーは`res->status`、と頭の中で分けておきましょう。
> SSL関連のエラーをさらに詳しく調べたい場合はC18. SSLエラーをハンドリングするを参照してください。
> SSL関連のエラーをさらに詳しく調べたい場合は[C18. SSLエラーをハンドリングする](c18-ssl-errors)を参照してください。
+1 -3
View File
@@ -48,6 +48,4 @@ if (res.ssl_backend_error() != 0) {
| `SSLServerHostnameVerification` | 証明書のCN/SANとホスト名が一致しない |
| `SSLConnection` | TLSバージョンの不一致、対応スイートが無い |
> **Note:** `ssl_backend_error()`は以前は`ssl_openssl_error()`と呼ばれていました。後者はdeprecatedで、現在は`ssl_backend_error()`を使ってください。
> 証明書の検証設定を変えたい場合はT02. SSL証明書の検証を制御するを参照してください。
> 証明書の検証設定を変えたい場合は[T02. SSL証明書の検証を制御する](t02-cert-verification)を参照してください。
+3 -3
View File
@@ -56,7 +56,7 @@ svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
});
```
クライアントが切断したら`sink.done()`で終了します。詳しくはS16. クライアントが切断したか検出するを参照してください。
クライアントが切断したら`sink.done()`で終了します。詳しくは[S16. クライアントが切断したか検出する](s16-disconnect)を参照してください。
## コメント行でハートビート
@@ -80,8 +80,8 @@ svr.new_task_queue = [] {
};
```
詳しくはS21. マルチスレッド数を設定するを参照してください。
詳しくは[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
> **Note:** `data:`の後ろに改行が含まれる場合、各行の先頭に`data: `を付けて複数の`data:`行として送ります。SSEの仕様で決まっているフォーマットです。
> イベント名を使い分けたい場合はE02. SSEでイベント名を使い分けるを、クライアント側はE04. SSEをクライアントで受信するを参照してください。
> イベント名を使い分けたい場合は[E02. SSEでイベント名を使い分ける](e02-sse-event-names)を、クライアント側は[E04. SSEをクライアントで受信する](e04-sse-client)を参照してください。
@@ -52,7 +52,7 @@ auto send_event = [](httplib::DataSink &sink,
send_event(sink, "message", "Hello!", "42");
```
IDの付け方は自由です。連番でもUUIDでも、サーバー側で重複せず順序が追えるものを選びましょう。再接続の詳細はE03. SSEの再接続を処理するを参照してください。
IDの付け方は自由です。連番でもUUIDでも、サーバー側で重複せず順序が追えるものを選びましょう。再接続の詳細は[E03. SSEの再接続を処理する](e03-sse-reconnect)を参照してください。
## JSONをdataに乗せる
+1 -1
View File
@@ -96,4 +96,4 @@ std::cout << "last id: " << sse.last_event_id() << std::endl;
> **Note:** SSEClientの`start()`はブロッキングなので、単発のツールならそのまま使えますが、GUIアプリやサーバーに組み込むときは`start_async()` + `stop()`の組み合わせが基本です。
> サーバー側の実装はE01. SSEサーバーを実装するを参照してください。
> サーバー側の実装は[E01. SSEサーバーを実装する](e01-sse-server)を参照してください。
+2 -2
View File
@@ -61,6 +61,6 @@ svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
レスポンスヘッダーを追加したいときは`res.set_header("Name", "Value")`です。
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要ならS18. `listen_after_bind`で起動順序を制御するを参照してください。
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要なら[S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)を参照してください。
> パスパラメーター(`/users/:id`)を使いたい場合はS03. パスパラメーターを使うを参照してください。
> パスパラメーター(`/users/:id`)を使いたい場合は[S03. パスパラメーターを使う](s03-path-params)を参照してください。
+2 -2
View File
@@ -69,6 +69,6 @@ svr.Get("/api/health", [&](const auto &req, auto &res) {
});
```
> **Note:** 大きなJSONボディを受け取ると、`req.body`がまるごとメモリに載ります。巨大なペイロードを扱うときはS07. マルチパートデータをストリーミングで受け取るのように、ストリーミング受信も検討しましょう。
> **Note:** 大きなJSONボディを受け取ると、`req.body`がまるごとメモリに載ります。巨大なペイロードを扱うときは[S07. マルチパートデータをストリーミングで受け取る](s07-multipart-reader)のように、ストリーミング受信も検討しましょう。
> クライアント側の書き方はC02. JSONを送受信するを参照してください。
> クライアント側の書き方は[C02. JSONを送受信する](c02-json)を参照してください。
@@ -52,4 +52,4 @@ svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
> **Warning:** 静的ファイル配信系のメソッドは**スレッドセーフではありません**。起動後(`listen()`以降)には呼ばないでください。起動前にまとめて設定しましょう。
> ダウンロード用のレスポンスを返したい場合はS06. ファイルダウンロードレスポンスを返すも参考になります。
> ダウンロード用のレスポンスを返したい場合は[S06. ファイルダウンロードレスポンスを返す](s06-download-response)も参考になります。
@@ -60,4 +60,4 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
> **Note:** プロバイダラムダは複数回呼ばれます。キャプチャする変数のライフタイムに気をつけてください。必要なら`std::shared_ptr`などで包みましょう。
> ファイルダウンロードとして扱いたい場合はS06. ファイルダウンロードレスポンスを返すを参照してください。
> ファイルダウンロードとして扱いたい場合は[S06. ファイルダウンロードレスポンスを返す](s06-download-response)を参照してください。
@@ -68,4 +68,4 @@ svr.Post("/upload",
> **Warning:** `HandlerWithContentReader`を使うと、`req.body`は**空のまま**です。ボディはコールバック内で自分で処理してください。
> クライアント側でマルチパートを送る方法はC07. ファイルをマルチパートフォームとしてアップロードするを参照してください。
> クライアント側でマルチパートを送る方法は[C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)を参照してください。
@@ -50,4 +50,4 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
> **Note:** 小さなレスポンスは圧縮しても効果が薄く、むしろCPU時間を無駄にすることがあります。cpp-httplibは小さすぎるボディは圧縮をスキップします。
> クライアント側の挙動はC15. 圧縮を有効にするを参照してください。
> クライアント側の挙動は[C15. 圧縮を有効にする](c15-compression)を参照してください。
@@ -49,6 +49,6 @@ svr.set_pre_routing_handler(
## 特定ルートだけに認証をかけたい場合
全ルート共通ではなく、ルート単位で認証を分けたいときは、S11. Pre-request handlerでルート単位の認証を行うのほうが適しています。
全ルート共通ではなく、ルート単位で認証を分けたいときは、[S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)のほうが適しています。
> **Note:** レスポンスを加工したいだけなら、`set_post_routing_handler()`のほうが適切です。S10. Post-routing handlerでレスポンスヘッダーを追加するを参照してください。
> **Note:** レスポンスを加工したいだけなら、`set_post_routing_handler()`のほうが適切です。[S10. Post-routing handlerでレスポンスヘッダーを追加する](s10-post-routing)を参照してください。
@@ -4,7 +4,7 @@ order: 30
status: "draft"
---
S09で紹介した`set_pre_routing_handler()`はルーティングの**前**に呼ばれるので、「どのルートにマッチしたか」を知れません。ルートによって認証の有無を変えたい場合は、`set_pre_request_handler()`のほうが便利です。
[S09. 全ルートに共通の前処理をする](s09-pre-routing)で紹介した`set_pre_routing_handler()`はルーティングの**前**に呼ばれるので、「どのルートにマッチしたか」を知れません。ルートによって認証の有無を変えたい場合は、`set_pre_request_handler()`のほうが便利です。
## Pre-routingとの違い
@@ -44,4 +44,4 @@ Pre-routingハンドラと同じく、`HandlerResponse`を返します。
## 認証情報を後続のハンドラに渡す
認証で取り出したユーザー情報などをルートハンドラに渡したいときは、`res.user_data`を使います。詳しくはS12. `res.user_data`でハンドラ間データを渡すを参照してください。
認証で取り出したユーザー情報などをルートハンドラに渡したいときは、`res.user_data`を使います。詳しくは[S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)を参照してください。
@@ -48,4 +48,4 @@ svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
これで全エラーが統一されたJSONで返ります。
> **Note:** `set_error_handler()`は、ルートハンドラが例外を投げた場合の500エラーにも呼ばれます。例外そのものの情報を取り出したい場合は`set_exception_handler()`を組み合わせましょう。S14. 例外をキャッチするを参照してください。
> **Note:** `set_error_handler()`は、ルートハンドラが例外を投げた場合の500エラーにも呼ばれます。例外そのものの情報を取り出したい場合は`set_exception_handler()`を組み合わせましょう。[S14. 例外をキャッチする](s14-exception-handler)を参照してください。
@@ -59,6 +59,6 @@ svr.set_logger([](const auto &req, const auto &res) {
});
```
`user_data`の使い方はS12. `res.user_data`でハンドラ間データを渡すも参照してください。
`user_data`の使い方は[S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)も参照してください。
> **Note:** ロガーはリクエスト処理と同じスレッドで同期的に呼ばれます。重い処理を直接入れると全体のスループットが落ちるので、必要ならキューに流して非同期で処理しましょう。
@@ -49,4 +49,4 @@ t.join();
> **Note:** `bind_to_any_port()`は失敗すると`-1`を返します。権限エラーや利用可能ポートが無いケースなので、返り値のチェックを忘れずに。
> サーバーを止める方法はS19. グレースフルシャットダウンするを参照してください。
> サーバーを止める方法は[S19. グレースフルシャットダウンする](s19-graceful-shutdown)を参照してください。
@@ -54,4 +54,4 @@ if (!svr.bind_to_port("0.0.0.0", 8080)) {
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
> **Note:** 空いているポートを自動で選びたいときはS17. ポートを動的に割り当てるを参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
+2 -2
View File
@@ -52,6 +52,6 @@ svr.set_keep_alive_max_count(1000);
## スレッドプールとの関係
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。接続数 × 同時リクエスト数がスレッドプールのサイズを超えると、新しいリクエストが待たされます。スレッド数の調整はS21. マルチスレッド数を設定するを参照してください。
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。接続数 × 同時リクエスト数がスレッドプールのサイズを超えると、新しいリクエストが待たされます。スレッド数の調整は[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
> **Note:** クライアント側の挙動はC14. 接続の再利用とKeep-Aliveの挙動を理解するを参照してください。サーバーがタイムアウトで接続を切っても、クライアントは自動で再接続します。
> **Note:** クライアント側の挙動は[C14. 接続の再利用とKeep-Aliveの挙動を理解する](c14-keep-alive)を参照してください。サーバーがタイムアウトで接続を切っても、クライアントは自動で再接続します。
@@ -42,8 +42,8 @@ wolfSSLには商用ライセンスとサポートがあります。製品に組
証明書の検証制御、SSLServerの立ち上げ、ピア証明書の取得などは、どのバックエンドでも同じAPIで呼べます。
- T02. SSL証明書の検証を制御する
- T03. SSL/TLSサーバーを立ち上げる
- T05. サーバー側でピア証明書を参照する
- [T02. SSL証明書の検証を制御する](t02-cert-verification)
- [T03. SSL/TLSサーバーを立ち上げる](t03-ssl-server)
- [T05. サーバー側でピア証明書を参照する](t05-peer-cert)
> **Note:** macOSでは、OpenSSL系のバックエンドを使う場合、システムのキーチェーンからルート証明書を自動で読む設定(`CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`)がデフォルトで有効です。無効にしたい場合は`CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`を定義してください。
@@ -48,6 +48,6 @@ cli.enable_server_hostname_verification(false);
多くのLinuxディストリビューションでは、`/etc/ssl/certs/ca-certificates.crt`などにルート証明書がまとまっています。cpp-httplibは起動時にOSのデフォルトストアを自動で読みにいくので、普通のサーバーならとくに設定不要です。
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方はT01. OpenSSL・mbedTLS・wolfSSLの選択指針を参照してください。
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方は[T01. OpenSSL・mbedTLS・wolfSSLの選択指針](t01-tls-backends)を参照してください。
> 失敗したときの詳細を調べる方法はC18. SSLエラーをハンドリングするを参照してください。
> 失敗したときの詳細を調べる方法は[C18. SSLエラーをハンドリングする](c18-ssl-errors)を参照してください。
+3 -3
View File
@@ -34,7 +34,7 @@ httplib::SSLServer svr("cert.pem", "key.pem",
nullptr, nullptr, "password");
```
第3、第4引数はクライアント証明書検証用(mTLS、T04参照)なので、今は`nullptr`を指定します。
第3、第4引数はクライアント証明書検証用(mTLS、[T04. mTLSを設定する](t04-mtls)参照)なので、今は`nullptr`を指定します。
## メモリ上のPEMから立ち上げる
@@ -73,6 +73,6 @@ openssl req -x509 -newkey rsa:2048 -days 365 -nodes \
本番では、Let's Encryptや社内CAから発行された証明書を使いましょう。
> **Warning:** HTTPSサーバーを443番ポートで立ち上げるにはroot権限が必要です。安全に立ち上げる方法はS18. `listen_after_bind`で起動順序を制御するの「特権降格」を参照してください。
> **Warning:** HTTPSサーバーを443番ポートで立ち上げるにはroot権限が必要です。安全に立ち上げる方法は[S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)の「特権降格」を参照してください。
> クライアント証明書による相互認証(mTLS)はT04. mTLSを設定するを参照してください。
> クライアント証明書による相互認証(mTLS)は[T04. mTLSを設定する](t04-mtls)を参照してください。
+1 -1
View File
@@ -59,7 +59,7 @@ auto res = cli.Get("/");
## ハンドラからクライアント情報を取得する
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくはT05. サーバー側でピア証明書を参照するを参照してください。
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくは[T05. サーバー側でピア証明書を参照する](t05-peer-cert)を参照してください。
## 用途
+2 -2
View File
@@ -77,7 +77,7 @@ svr.set_pre_request_handler(
});
```
Pre-requestハンドラと組み合わせれば、共通の認可ロジックを一箇所にまとめられます。詳しくはS11. Pre-request handlerでルート単位の認証を行うを参照してください。
Pre-requestハンドラと組み合わせれば、共通の認可ロジックを一箇所にまとめられます。詳しくは[S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)を参照してください。
## SNIServer Name Indication
@@ -85,4 +85,4 @@ Pre-requestハンドラと組み合わせれば、共通の認可ロジックを
> **Warning:** `req.peer_cert()`は、mTLSが有効で、かつクライアントが証明書を提示した場合のみ有効な値を返します。通常のTLS接続では空の`PeerCert`が返ります。使う前に必ず`bool`チェックしてください。
> mTLSの設定方法はT04. mTLSを設定するを参照してください。
> mTLSの設定方法は[T04. mTLSを設定する](t04-mtls)を参照してください。
@@ -71,7 +71,7 @@ ws.send("Hello"); // テキストフレーム
ws.send(binary_data, binary_data_size); // バイナリフレーム
```
`std::string`を受け取るオーバーロードはテキスト、`const char*`とサイズを受け取るオーバーロードはバイナリとして送られます。詳しくはW04. バイナリフレームを送受信するを参照してください。
`std::string`を受け取るオーバーロードはテキスト、`const char*`とサイズを受け取るオーバーロードはバイナリとして送られます。詳しくは[W04. バイナリフレームを送受信する](w04-websocket-binary)を参照してください。
## スレッドとの関係
@@ -83,6 +83,6 @@ svr.new_task_queue = [] {
};
```
詳細はS21. マルチスレッド数を設定するを参照してください。
詳細は[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
> **Note:** HTTPSサーバーの上でWebSocketを動かしたいときは、`httplib::Server`の代わりに`httplib::SSLServer`を使えば、同じ`WebSocket()`ハンドラがそのまま動きます。クライアント側は`wss://`スキームを指定するだけです。
@@ -57,4 +57,24 @@ WebSocketプロトコルでは、PingフレームにはPongフレームで応答
> **Warning:** Ping間隔を極端に短くすると、WebSocket接続ごとにバックグラウンドでスレッドが走るので、CPU負荷が上がります。接続数が多いサーバーでは控えめな値に設定しましょう。
> 接続が閉じたときの処理はW03. 接続クローズをハンドリングするを参照してください。
## 無応答のピアを検出する
Pingを送るだけでは、相手が「黙って落ちた」場合に気付けません。TCPの接続自体は生きているように見えるのに、相手のプロセスはもう応答しない、というケースです。これを検出するには、送ったPingに対してPongがN回連続で返ってこなかったら接続を切る、というオプションを有効にします。
```cpp
cli.set_websocket_max_missed_pongs(2); // 2回連続でPongが返ってこなければ切断
```
サーバー側にも同じ`set_websocket_max_missed_pongs()`があります。
たとえばPing間隔が30秒で`max_missed_pongs = 2`なら、無応答のピアは約60秒で検出され、`CloseStatus::GoingAway`(理由は`"pong timeout"`)で接続が閉じられます。
この仕組みは`read()`を呼んでPongフレームを消費したタイミングでカウンタがリセットされます。つまり通常のWebSocketクライアントのように`read()`をループで回していれば、特に意識することなく動きます。
### デフォルトは無効
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
ただし`0`のままでも最終的に接続が残り続けることはありません。`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働き、フレームが一定時間来なければ`read()`が失敗します。つまり`max_missed_pongs`は「**もっと速く**無応答を検出したい」ときに使うオプションだと考えてください。
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](w03-websocket-close)を参照してください。
@@ -56,7 +56,7 @@ switch (result) {
## Pingもバイナリフレームの一種
WebSocketのPing/PongフレームもOpcodeレベルではバイナリに近い扱いですが、cpp-httplibが自動で処理するので、アプリケーションコードで意識する必要はありません。W02. ハートビートを設定するを参照してください。
WebSocketのPing/PongフレームもOpcodeレベルではバイナリに近い扱いですが、cpp-httplibが自動で処理するので、アプリケーションコードで意識する必要はありません。[W02. ハートビートを設定する](w02-websocket-ping)を参照してください。
## サンプル: 画像を送る
+1 -4
View File
@@ -30,10 +30,7 @@ int main(void) {
} else {
cout << "error code: " << res.error() << std::endl;
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
auto result = cli.get_verify_result();
if (result) {
cout << "verify error: " << X509_verify_cert_error_string(result) << endl;
}
cout << "ssl backend error: " << res.ssl_backend_error() << endl;
#endif
}
+278 -606
View File
File diff suppressed because it is too large Load Diff
+33 -14
View File
@@ -2,10 +2,12 @@
#
# Release a new version of cpp-httplib.
#
# Usage: ./release.sh [--run]
# Usage: ./release.sh [--run] [--minor]
#
# By default, runs in dry-run mode (no changes made).
# Pass --run to actually update files, commit, tag, and push.
# Pass --minor to force a minor bump even when ABI is unchanged
# (use this for behavioral breaking changes that don't break ABI).
#
# This script:
# 1. Reads the current version from httplib.h
@@ -14,21 +16,30 @@
# 4. Determines the next version automatically:
# - abidiff passed → patch bump (e.g., 0.38.0 → 0.38.1)
# - abidiff failed → minor bump (e.g., 0.38.1 → 0.39.0)
# - --minor passed → forces minor bump regardless of abidiff
# 5. Updates httplib.h and docs-src/config.toml
# 6. Commits, tags (vX.Y.Z), and pushes
set -euo pipefail
DRY_RUN=1
if [ "${1:-}" = "--run" ]; then
DRY_RUN=0
shift
fi
if [ $# -ne 0 ]; then
echo "Usage: $0 [--run]"
exit 1
fi
FORCE_MINOR=0
while [ $# -gt 0 ]; do
case "$1" in
--run)
DRY_RUN=0
shift
;;
--minor)
FORCE_MINOR=1
shift
;;
*)
echo "Usage: $0 [--run] [--minor]"
exit 1
;;
esac
done
# --- Step 1: Read current version from httplib.h ---
CURRENT_VERSION=$(sed -n 's/^#define CPPHTTPLIB_VERSION "\([^"]*\)"/\1/p' httplib.h)
@@ -51,8 +62,7 @@ HEAD_SHORT=$(git rev-parse --short HEAD)
echo " Latest commit: $HEAD_SHORT"
# Fetch all workflow runs for the HEAD commit
RUNS=$(gh run list --json name,conclusion,headSha \
--jq "[.[] | select(.headSha == \"$HEAD_SHA\")]")
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,conclusion,headSha)
NUM_RUNS=$(echo "$RUNS" | jq 'length')
@@ -95,7 +105,12 @@ fi
echo " All non-abidiff CI checks passed."
# --- Step 4: Determine new version ---
if [ "$ABIDIFF_PASSED" -eq 1 ]; then
if [ "$FORCE_MINOR" -eq 1 ] && [ "$ABIDIFF_PASSED" -eq 1 ]; then
NEW_MINOR=$((V_MINOR + 1))
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
echo ""
echo "==> abidiff passed but --minor specified → forced minor bump"
elif [ "$ABIDIFF_PASSED" -eq 1 ]; then
NEW_PATCH=$((V_PATCH + 1))
NEW_VERSION="$V_MAJOR.$V_MINOR.$NEW_PATCH"
echo ""
@@ -104,7 +119,11 @@ else
NEW_MINOR=$((V_MINOR + 1))
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
echo ""
echo "==> abidiff failed → minor bump"
if [ "$FORCE_MINOR" -eq 1 ]; then
echo "==> abidiff failed → minor bump (--minor also specified)"
else
echo "==> abidiff failed → minor bump"
fi
fi
VERSION_HEX=$(printf "0x%02x%02x%02x" "${NEW_VERSION%%.*}" "$(echo "$NEW_VERSION" | cut -d. -f2)" "${NEW_VERSION##*.}")
+67 -17
View File
@@ -18,6 +18,10 @@ ifneq ($(OS), Windows_NT)
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -lwolfssl
ifeq ($(UNAME_S), Linux)
# Disable ASLR for ASAN compatibility on WSL2 (high-entropy ASLR conflicts with ASAN shadow memory)
SETARCH = setarch $(shell uname -m) -R
endif
endif
endif
@@ -59,28 +63,35 @@ STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \
$(wildcard example/*.h example/*.cc fuzzing/*.h fuzzing/*.cc *.h *.cc ../httplib.h))
all : test test_split
LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test
LSAN_OPTIONS=suppressions=lsan_suppressions.txt $(SETARCH) ./test
SHARDS ?= 4
define run_parallel
@echo "Running $(1) with $(SHARDS) shards in parallel..."
@fail=0; \
@fail=0; pids=""; \
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
GTEST_TOTAL_SHARDS=$(SHARDS) GTEST_SHARD_INDEX=$$i \
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
$(SETARCH) ./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
pids="$$pids $$!"; \
done; \
wait; \
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
if ! grep -q "\[ PASSED \]" $(1)_shard_$$i.log; then \
echo "=== Shard $$i FAILED ==="; \
cat $(1)_shard_$$i.log; \
fail=1; \
else \
passed=$$(grep "\[ PASSED \]" $(1)_shard_$$i.log); \
exits=""; \
for pid in $$pids; do \
wait $$pid; exits="$$exits $$?"; \
done; \
i=0; \
for ec in $$exits; do \
log=$(1)_shard_$$i.log; \
if grep -q "\[ PASSED \]" $$log && ! grep -q "\[ FAILED \]" $$log && [ $$ec -eq 0 ]; then \
passed=$$(grep "\[ PASSED \]" $$log); \
echo "Shard $$i: $$passed"; \
else \
echo "=== Shard $$i FAILED (exit=$$ec) ==="; \
cat $$log; \
fail=1; \
fi; \
i=$$((i+1)); \
done; \
if [ $$fail -ne 0 ]; then exit 1; fi; \
echo "All shards passed."
@@ -191,8 +202,25 @@ test_split_no_tls : test.cc ../httplib.h httplib.cc Makefile
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS)
# ThreadPool unit tests (no TLS, no compression needed)
#
# The constructor-exception-safety reproducer test interposes pthread_create
# at link time. The link flags below enable that interposition. ASAN is also
# stripped from this target because libasan installs its own pthread_create
# interceptor; layering our override on top corrupts ASAN's thread bookkeeping
# and trips "Joining already joined thread" on Linux. ThreadPool memory
# behavior is still covered by the ASAN-instrumented `test` binary.
ifneq ($(OS), Windows_NT)
ifeq ($(shell uname -s), Darwin)
THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,-flat_namespace
else
THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,--export-dynamic
endif
endif
THREAD_POOL_CXXFLAGS := $(filter-out -fsanitize=address,$(CXXFLAGS))
test_thread_pool : test_thread_pool.cc ../httplib.h Makefile
$(CXX) -o $@ -I.. $(CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread
$(CXX) -o $@ -I.. $(THREAD_POOL_CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread $(THREAD_POOL_INTERPOSE_LDFLAGS)
check_abi:
@./check-shared-library-abi-compatibility.sh
@@ -242,16 +270,38 @@ test_proxy_mbedtls : test_proxy.cc ../httplib.h Makefile cert.pem
test_proxy_wolfssl : test_proxy.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_WOLFSSL)
# Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE).
# Usage: make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
fuzz_test: server_fuzzer
./server_fuzzer fuzzing/corpus/*
# Runs all fuzz harnesses based on the value of $(LIB_FUZZING_ENGINE).
# By default LIB_FUZZING_ENGINE is standalone_fuzz_target_runner.o, so each
# fuzzer is replayed over its regression corpus.
# Override for actual fuzzing:
# make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
fuzz_test: server_fuzzer client_fuzzer header_parser_fuzzer url_parser_fuzzer
@m=""; for f in fuzzing/corpus/[0-9]* fuzzing/corpus/issue1264 fuzzing/corpus/clusterfuzz-testcase-minimized-server_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
if [ -n "$$m" ]; then echo "./server_fuzzer$$m"; ./server_fuzzer $$m; else echo "(no server_fuzzer corpus)"; fi
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
if [ -n "$$m" ]; then echo "./client_fuzzer$$m"; ./client_fuzzer $$m; else echo "(no client_fuzzer corpus)"; fi
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-header_parser_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
if [ -n "$$m" ]; then echo "./header_parser_fuzzer$$m"; ./header_parser_fuzzer $$m; else echo "(no header_parser_fuzzer corpus)"; fi
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-url_parser_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
if [ -n "$$m" ]; then echo "./url_parser_fuzzer$$m"; ./url_parser_fuzzer $$m; else echo "(no url_parser_fuzzer corpus)"; fi
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
server_fuzzer : fuzzing/server_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
@file $@
client_fuzzer : fuzzing/client_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
@file $@
header_parser_fuzzer : fuzzing/header_parser_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
@file $@
url_parser_fuzzer : fuzzing/url_parser_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
@file $@
# Standalone fuzz runner, which just reads inputs from fuzzing/corpus/ dir and
# feeds it to server_fuzzer.
standalone_fuzz_target_runner.o : fuzzing/standalone_fuzz_target_runner.cpp
@@ -264,5 +314,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 test_benchmark 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 client_fuzzer header_parser_fuzzer url_parser_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Delayed UDP responder used as a loopback test fixture.
This is a self-contained test fixture for the GetAddrInfoAsyncCancelTest
cases (reproducer for cpp-httplib issue #2431). It is NOT a general-purpose
nameserver and is only intended to run on 127.0.0.1 inside the test job's
own runner / container.
What it does
------------
Binds a UDP socket on 127.0.0.1:<port>, accepts well-formed DNS queries
from the test process, waits <delay_seconds>, then sends back a minimal
NXDOMAIN reply. The deliberate delay is what makes the bug reproducible:
* The test calls getaddrinfo_with_timeout() with timeout_sec=1.
* gai_suspend() returns EAI_AGAIN after 1s; the function returns and
its stack frame is destroyed.
* The fixture replies after <delay_seconds> (= 3s by default), so the
glibc resolver worker thread receives the response *after* the
caller's frame is gone and writes back into freed stack memory.
* AddressSanitizer (with detect_stack_use_after_return=1) catches the
write and aborts with a stack-use-after-return diagnostic.
Without this fixture the bug is hard to surface: dropping UDP/53 makes
the resolver hang forever, so the worker never receives anything and
never reaches the buggy write-back path.
Usage
-----
python3 test/dns_test_fixture.py <port> [<delay_seconds>]
Only standard library; no third-party dependencies.
"""
import socket
import struct
import sys
import threading
import time
def serve(port: int, delay_sec: float) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", port))
print(
f"[dns_test_fixture] listening on 127.0.0.1:{port}, "
f"reply delay={delay_sec}s",
flush=True,
)
while True:
try:
data, addr = sock.recvfrom(2048)
except OSError:
return
threading.Thread(
target=_reply_after_delay,
args=(sock, data, addr, delay_sec),
daemon=True,
).start()
def _reply_after_delay(sock, query: bytes, addr, delay_sec: float) -> None:
time.sleep(delay_sec)
if len(query) < 12:
return
# Header: copy transaction id, set QR=1 RA=1 RCODE=3 (NXDOMAIN),
# preserve the requester's RD bit, then echo the question section so
# glibc's resolver accepts the reply as matching its outstanding query.
txid = query[:2]
rd_bit = query[2] & 0x01
flags = struct.pack(">H", 0x8003 | (rd_bit << 8))
counts = struct.pack(">HHHH", 1, 0, 0, 0)
question = query[12:]
reply = txid + flags + counts + question
try:
sock.sendto(reply, addr)
except OSError:
pass
if __name__ == "__main__":
if len(sys.argv) < 2:
print(__doc__, file=sys.stderr)
sys.exit(2)
port_arg = int(sys.argv[1])
delay_arg = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
serve(port_arg, delay_arg)
+5 -1
View File
@@ -13,7 +13,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
BROTLI_DIR = /usr/local/opt/brotli
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer
# Runs all the tests and also fuzz tests against seed corpus.
all : $(FUZZERS)
@@ -25,6 +25,10 @@ server_fuzzer : server_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
zip -q -r server_fuzzer_seed_corpus.zip corpus
client_fuzzer : client_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
zip -q -r client_fuzzer_seed_corpus.zip corpus
header_parser_fuzzer : header_parser_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
+88
View File
@@ -0,0 +1,88 @@
#include <cstdint>
#include <cstring>
#include <httplib.h>
class FuzzedStream : public httplib::Stream {
public:
FuzzedStream(const uint8_t *data, size_t size)
: data_(data), size_(size), read_pos_(0) {}
ssize_t read(char *ptr, size_t size) override {
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
memcpy(ptr, data_ + read_pos_, size);
read_pos_ += size;
return static_cast<ssize_t>(size);
}
ssize_t write(const char *ptr, size_t size) override {
request_.append(ptr, size);
return static_cast<ssize_t>(size);
}
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
bool is_readable() const override { return true; }
bool wait_readable() const override { return true; }
bool wait_writable() const override { return true; }
void get_remote_ip_and_port(std::string &ip, int &port) const override {
ip = "127.0.0.1";
port = 8080;
}
void get_local_ip_and_port(std::string &ip, int &port) const override {
ip = "127.0.0.1";
port = 8080;
}
socket_t socket() const override { return 0; }
time_t duration() const override { return 0; };
private:
const uint8_t *data_;
size_t size_;
size_t read_pos_;
std::string request_;
};
class FuzzableClient : public httplib::ClientImpl {
public:
FuzzableClient() : httplib::ClientImpl("localhost", 8080) {}
void ProcessFuzzedResponse(FuzzedStream &stream, const std::string &method) {
httplib::Request req;
req.method = method;
req.path = "/";
httplib::Response res;
bool close_connection = false;
httplib::Error error = httplib::Error::Success;
process_request(stream, req, res, close_connection, error);
}
};
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 1) return 0;
FuzzedStream stream{data + 1, size - 1};
FuzzableClient client;
// Use the first byte to select method
std::string method;
switch (data[0] % 6) {
case 0: method = "GET"; break;
case 1: method = "POST"; break;
case 2: method = "PUT"; break;
case 3: method = "PATCH"; break;
case 4: method = "DELETE"; break;
case 5: method = "OPTIONS"; break;
}
client.ProcessFuzzedResponse(stream, method);
return 0;
}
@@ -0,0 +1,3 @@
HTTP/1.1 777
Content-Length:20000000000
@@ -0,0 +1,3 @@
HTTP/1.1 777
Content-Length:446744071854775
+4
View File
@@ -1,3 +1,7 @@
# OpenSSL 3.x internal caches (provider, cipher, keymgmt) are allocated
# lazily and intentionally kept until process exit. These are not real leaks.
leak:libcrypto
# wolfSSL keeps ECC point/scratch buffers alive across handshakes; they are
# released only at library shutdown which the test binaries do not invoke.
leak:libwolfssl
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Reproducer runner for Issue #2431
# (https://github.com/yhirose/cpp-httplib/issues/2431).
#
# Spins up an Ubuntu container, runs the loopback DNS test fixture
# (test/dns_test_fixture.py), routes the container's DNS lookups to
# that fixture via an iptables NAT rule, builds the test suite with
# g++ + ASAN, and runs the GetAddrInfoAsyncCancelTest cases.
#
# Expected outcomes:
# - HEAD prior to the fix: ASAN reports stack-use-after-return inside
# getaddrinfo_with_timeout's getaddrinfo_a path during one of the
# GetAddrInfoAsyncCancelTest cases.
# - HEAD with the fix applied: all three cases PASS.
#
# Usage:
# bash test/run_issue_2431_repro.sh
#
# Requirements: Docker (Linux container support). The container needs
# --privileged because the test binary uses `setarch -R` to disable ASLR
# for ASAN compatibility, and because the test job manages iptables
# rules inside the container.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
docker run --rm --privileged \
-v "$REPO_ROOT:/work" \
-w /work/test \
ubuntu:24.04 bash -c '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq --no-install-recommends \
ca-certificates g++ make pkg-config iptables iproute2 util-linux coreutils file \
python3 \
libssl-dev zlib1g-dev libbrotli-dev libzstd-dev libcurl4-openssl-dev \
>/dev/null
# Force DNS-only resolution: Ubuntu defaults nsswitch.conf to
# "hosts: files mdns4_minimal [NOTFOUND=return] dns ...", which
# short-circuits to NOTFOUND before reaching glibc DNS code, so the
# gai_cancel() branch never gets exercised.
sed -i "s/^hosts:.*/hosts: dns/" /etc/nsswitch.conf
# Start the loopback DNS test fixture (delayed UDP responder).
DNS_FIXTURE_PORT=15353
DNS_FIXTURE_DELAY=3
python3 /work/test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
>/tmp/dns_fixture.log 2>&1 &
FIXTURE_PID=$!
# Route the container DNS lookups to the fixture; conntrack handles the
# reply path automatically. /etc/resolv.conf is left untouched.
iptables -t nat -I OUTPUT -p udp --dport 53 \
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
trap '"'"'iptables -t nat -F OUTPUT 2>/dev/null || true; kill "$FIXTURE_PID" 2>/dev/null || true'"'"' EXIT
# Wait for the fixture to start listening.
for _ in $(seq 1 50); do
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
break
fi
sleep 0.1
done
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" || {
echo "ERROR: dns_test_fixture failed to start" >&2
cat /tmp/dns_fixture.log >&2 || true
exit 1
}
# Sanity check: a DNS lookup must take at least the fixture delay
# (proving the NAT rule routes the query to the fixture).
start=$(date +%s)
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
elapsed=$(( $(date +%s) - start ))
if [ "$elapsed" -lt 2 ]; then
echo "ERROR: lookup returned in ${elapsed}s; fixture not in DNS path" >&2
exit 1
fi
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
cd /work/test
echo "=== building test binary (g++ + ASAN) ==="
make CXX=g++ test 2>&1 | tail -5
ARCH=$(uname -m)
echo "=== running GetAddrInfoAsyncCancelTest with CPPHTTPLIB_TEST_ISSUE_2431=1 ==="
set +e
CPPHTTPLIB_TEST_ISSUE_2431=1 \
ASAN_OPTIONS=detect_stack_use_after_return=1 \
setarch "$ARCH" -R \
./test --gtest_filter="GetAddrInfoAsyncCancelTest.*" 2>&1
rc=$?
set -e
echo "=== test exit: $rc ==="
exit $rc
'
+452 -41
View File
@@ -1549,6 +1549,140 @@ TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
std::this_thread::sleep_for(std::chrono::seconds(8));
}
#if defined(__linux__) && defined(__GLIBC__) && \
defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO)
// Forward declaration: in split builds split.py strips `inline` and moves the
// definition into httplib.cc, so detail::getaddrinfo_with_timeout is not
// visible from the public httplib.h. Re-declaring it here lets the tests link
// against the symbol in both header-only and split builds.
namespace httplib {
namespace detail {
int getaddrinfo_with_timeout(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res, time_t timeout_sec);
} // namespace detail
} // namespace httplib
// Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
//
// On Linux/glibc, getaddrinfo_with_timeout() runs the lookup via
// getaddrinfo_a(GAI_NOWAIT) using a stack-local `struct gaicb`. When the
// gai_suspend() call hits the connection timeout the function calls
// gai_cancel() and returns immediately. gai_cancel() is non-blocking and
// can return EAI_NOTCANCELED, in which case the resolver worker thread is
// still alive and still references the now-destroyed stack frame.
//
// Triggering the bug requires DNS to actually hang (UDP/53 dropped, etc.),
// so these tests are gated on CPPHTTPLIB_TEST_ISSUE_2431=1 and are skipped
// during normal runs. test/run_issue_2431_repro.sh sets up the environment
// and runs them in a container.
namespace {
bool should_run_issue_2431_tests() {
const char *v = getenv("CPPHTTPLIB_TEST_ISSUE_2431");
return v && *v && std::string(v) != "0";
}
std::string unique_unresolvable_host(int n) {
// .invalid is reserved (RFC 6761) and is never served by real DNS, but
// glibc still asks the configured nameserver — which is exactly the path
// we want to exercise. A unique label per call avoids the resolver cache.
auto t = std::chrono::steady_clock::now().time_since_epoch().count();
return "h-" + std::to_string(::getpid()) + "-" + std::to_string(t) + "-" +
std::to_string(n) + ".invalid";
}
} // namespace
TEST(GetAddrInfoAsyncCancelTest, DirectCallSingleThread) {
if (!should_run_issue_2431_tests()) {
GTEST_SKIP()
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
}
for (int i = 0; i < 8; ++i) {
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
auto host = unique_unresolvable_host(i);
struct addrinfo *result = nullptr;
int rc = detail::getaddrinfo_with_timeout(host.c_str(), "80", &hints,
&result, /*timeout_sec=*/1);
if (rc == 0 && result) { freeaddrinfo(result); }
}
// Give orphaned getaddrinfo_a worker threads a chance to write into the
// stack region they still believe holds their gaicb.
std::this_thread::sleep_for(std::chrono::seconds(3));
}
TEST(GetAddrInfoAsyncCancelTest, DirectCallMultiThread) {
if (!should_run_issue_2431_tests()) {
GTEST_SKIP()
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
}
std::atomic<bool> stop{false};
std::vector<std::thread> threads;
for (int t = 0; t < 8; ++t) {
threads.emplace_back([t, &stop] {
int i = 0;
while (!stop.load(std::memory_order_relaxed)) {
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
auto host = unique_unresolvable_host(t * 100000 + i++);
struct addrinfo *result = nullptr;
int rc = detail::getaddrinfo_with_timeout(host.c_str(), "80", &hints,
&result, /*timeout_sec=*/1);
if (rc == 0 && result) { freeaddrinfo(result); }
}
});
}
std::this_thread::sleep_for(std::chrono::seconds(8));
stop.store(true, std::memory_order_relaxed);
for (auto &th : threads) {
th.join();
}
std::this_thread::sleep_for(std::chrono::seconds(3));
}
TEST(GetAddrInfoAsyncCancelTest, ClientGetMultiThread) {
if (!should_run_issue_2431_tests()) {
GTEST_SKIP()
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
}
std::atomic<bool> stop{false};
std::vector<std::thread> threads;
for (int t = 0; t < 8; ++t) {
threads.emplace_back([t, &stop] {
int i = 0;
while (!stop.load(std::memory_order_relaxed)) {
auto host = unique_unresolvable_host(t * 100000 + i++);
Client cli(host, 80);
cli.set_connection_timeout(1, 0);
cli.set_read_timeout(1, 0);
cli.set_write_timeout(1, 0);
(void)cli.Get("/");
}
});
}
std::this_thread::sleep_for(std::chrono::seconds(8));
stop.store(true, std::memory_order_relaxed);
for (auto &th : threads) {
th.join();
}
std::this_thread::sleep_for(std::chrono::seconds(3));
}
#endif // __linux__ && __GLIBC__ && CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
TEST(ConnectionErrorTest, InvalidHost) {
auto host = "-abcde.com";
@@ -3390,7 +3524,7 @@ protected:
})
.Get("/slow",
[&](const Request & /*req*/, Response &res) {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::this_thread::sleep_for(std::chrono::seconds(4));
res.set_content("slow", "text/plain");
})
#if 0
@@ -5012,6 +5146,39 @@ TEST_F(ServerTest, CaseInsensitiveTransferEncoding) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
// GHSA-h6wq-j5mv-f3q8: the server must reject malformed chunk-size lines
// rather than treat them as valid lengths.
template <typename ClientT>
static void expect_chunked_body_rejected(ClientT &cli, const char *body) {
Request req;
req.method = "POST";
req.path = "/chunked";
std::string host_and_port;
host_and_port += HOST;
host_and_port += ":";
host_and_port += std::to_string(PORT);
req.headers.emplace("Host", host_and_port.c_str());
req.headers.emplace("Content-Length", "0");
req.headers.emplace("Transfer-Encoding", "chunked");
req.body = body;
auto res = std::make_shared<Response>();
auto error = Error::Success;
ASSERT_TRUE(cli.send(req, *res, error));
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
}
TEST_F(ServerTest, RejectsNegativeChunkSize) {
expect_chunked_body_rejected(cli_, "-2\r\nAAAA\r\n0\r\n\r\n");
}
TEST_F(ServerTest, RejectsChunkSizeWithLeadingPlus) {
expect_chunked_body_rejected(
cli_, "+4\r\ndech\r\nf\r\nunked post body\r\n0\r\n\r\n");
}
TEST_F(ServerTest, GetStreamed2) {
auto res = cli_.Get("/streamed", {{make_range_header({{2, 3}})}});
ASSERT_TRUE(res);
@@ -6909,7 +7076,7 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
EXPECT_EQ(StatusCode::UriTooLong_414, res.status);
EXPECT_EQ("close", res.get_header_value("Connection"));
EXPECT_FALSE(cli_.is_socket_open());
EXPECT_LE(elapsed, 200);
EXPECT_LE(elapsed, 2000);
}
{
@@ -7274,6 +7441,122 @@ TEST(ServerRequestParsingTest, EmptyFieldValue) {
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
}
TEST(ServerRequestParsingTest, HeaderValueNotPercentDecoded) {
Server svr;
std::string x_custom;
std::string cookie;
std::string xff;
std::string x_unicode;
std::string x_iis;
svr.Get("/check", [&](const Request &req, Response &res) {
x_custom = req.get_header_value("X-Custom");
cookie = req.get_header_value("Cookie");
xff = req.get_header_value("X-Forwarded-For");
x_unicode = req.get_header_value("X-Unicode");
x_iis = req.get_header_value("X-IIS");
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
const std::string req = "GET /check HTTP/1.1\r\n"
"Host: localhost\r\n"
"X-Custom: a%0D%0AInjected: b\r\n"
"Cookie: session%3Dvictim%3B%20admin%3Dyes\r\n"
"X-Forwarded-For: 1.2.3.4%2C5.6.7.8\r\n"
"X-Unicode: %E3%81%82\r\n"
"X-IIS: %u00E9\r\n"
"Connection: close\r\n"
"\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
// Every value must be returned verbatim (wire form), with no decoding.
EXPECT_EQ("a%0D%0AInjected: b", x_custom);
EXPECT_EQ("session%3Dvictim%3B%20admin%3Dyes", cookie);
EXPECT_EQ("1.2.3.4%2C5.6.7.8", xff);
EXPECT_EQ("%E3%81%82", x_unicode);
EXPECT_EQ("%u00E9", x_iis);
}
// Applications that previously relied on automatic percent-decoding can
// reproduce the old behavior by explicitly calling decode_path_component()
// or, for RFC 3986 conformance, decode_uri_component().
TEST(ServerRequestParsingTest, HeaderValueExplicitDecodingByApplication) {
Server svr;
std::string decoded;
svr.Get("/check", [&](const Request &req, Response &res) {
decoded = decode_uri_component(req.get_header_value("X-Custom"));
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
const std::string req = "GET /check HTTP/1.1\r\n"
"Host: localhost\r\n"
"X-Custom: hello%20world\r\n"
"Connection: close\r\n"
"\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
EXPECT_EQ("hello world", decoded);
}
// Regression test for #2033. Browsers send Referer values that include
// percent-encoded characters such as %0A inside the URL. Decoding the
// header value would either trip the post-decode CR/LF/NUL guard (the
// original bug, returning 400) or, after that guard was relaxed, silently
// store a literal LF — both unacceptable. The wire form must round-trip.
TEST(ServerRequestParsingTest, RefererWithPercentEncodedNewline) {
Server svr;
std::string referer;
svr.Get("/check", [&](const Request &req, Response &res) {
referer = req.get_header_value("Referer");
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
const std::string req = "GET /check HTTP/1.1\r\n"
"Host: localhost\r\n"
"Referer: http://localhost:1111/?q=Hello%0A\r\n"
"Connection: close\r\n"
"\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
EXPECT_EQ("http://localhost:1111/?q=Hello%0A", referer);
}
TEST(ServerStopTest, StopServerWithChunkedTransmission) {
Server svr;
@@ -7805,6 +8088,31 @@ TEST(MountTest, MultibytesPathName) {
EXPECT_EQ(U8("日本語コンテンツ"), res->body);
}
#ifdef _WIN32
// Issue #2435: mmap::open() must succeed even when another handle holds
// the file open for writing (e.g. an active log file).
TEST(MmapTest, OpenWhileFileHeldForWriting) {
const char *path = "mmap_concurrent_writer_test.txt";
const char *content = "hello";
{
std::ofstream f(path, std::ios::binary);
f.write(content, static_cast<std::streamsize>(strlen(content)));
}
auto file_cleanup = detail::scope_exit([&] { std::remove(path); });
HANDLE writer = ::CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
ASSERT_NE(INVALID_HANDLE_VALUE, writer);
auto handle_cleanup = detail::scope_exit([&] { ::CloseHandle(writer); });
detail::mmap m(path);
ASSERT_TRUE(m.is_open());
EXPECT_EQ(strlen(content), m.size());
EXPECT_EQ(0, std::memcmp(content, m.data(), strlen(content)));
}
#endif
TEST(KeepAliveTest, ReadTimeout) {
Server svr;
@@ -9129,6 +9437,81 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
<< " bytes without truncation, but only read " << total_read << " bytes.";
}
// Regression test for OSS-Fuzz issue 508342856: a malicious server sending an
// enormous Content-Length must not cause the client to pre-allocate a huge
// response body buffer. The reservation is capped at payload_max_length_, and
// the read itself fails when the body exceeds the limit.
TEST(ClientVulnerabilityTest, HugeContentLengthDoesNotPreallocate) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
auto server_thread = std::thread([] {
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
default_socket_options(srv);
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
int opt = 1;
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
#ifdef _WIN32
reinterpret_cast<const char *>(&opt),
#else
&opt,
#endif
sizeof(opt));
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
::listen(srv, 1);
sockaddr_in cli_addr{};
socklen_t cli_len = sizeof(cli_addr);
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
if (cli != INVALID_SOCKET) {
char buf[4096];
::recv(cli, buf, sizeof(buf), 0);
// Malicious response: claim a 20GB body but send only a tiny payload.
std::string response = "HTTP/1.1 200 OK\r\n"
"Content-Length: 20000000000\r\n"
"\r\n"
"abc";
::send(cli,
#ifdef _WIN32
static_cast<const char *>(response.c_str()),
static_cast<int>(response.size()),
#else
response.c_str(), response.size(),
#endif
0);
detail::close_socket(cli);
}
detail::close_socket(srv);
});
std::this_thread::sleep_for(std::chrono::milliseconds(200));
{
Client cli("127.0.0.1", PORT + 2);
cli.set_read_timeout(5, 0);
// Default payload_max_length_ is 100MB; a 20GB Content-Length must not
// result in a 20GB pre-allocation. The Get() call is expected to fail
// (server claims more bytes than payload_max_length permits), but it must
// not exhaust memory before getting there.
auto res = cli.Get("/malicious");
EXPECT_FALSE(res); // Read fails because body exceeds payload_max_length_
}
server_thread.join();
}
// Verify that content_receiver bypasses the default payload_max_length,
// allowing streaming downloads larger than 100MB without requiring an explicit
// set_payload_max_length call.
@@ -12142,7 +12525,7 @@ TEST(MultipartFormDataTest, ManyItemsEndToEnd) {
TEST(MultipartFormDataTest, MakeFileProvider) {
// Verify make_file_provider sends a file's contents correctly.
const std::string file_content(4096, 'Z');
const std::string tmp_path = "/tmp/httplib_test_make_file_provider.bin";
const std::string tmp_path = "./httplib_test_make_file_provider.bin";
{
std::ofstream ofs(tmp_path, std::ios::binary);
ofs.write(file_content.data(),
@@ -12197,7 +12580,7 @@ TEST(MultipartFormDataTest, MakeFileProvider) {
TEST(MakeFileBodyTest, Basic) {
const std::string file_content(4096, 'Z');
const std::string tmp_path = "/tmp/httplib_test_make_file_body.bin";
const std::string tmp_path = "./httplib_test_make_file_body.bin";
{
std::ofstream ofs(tmp_path, std::ios::binary);
ofs.write(file_content.data(),
@@ -14275,8 +14658,23 @@ static std::thread serve_single_response(std::promise<int> &port_promise,
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
if (cli != INVALID_SOCKET) {
char buf[4096];
::recv(cli, buf, sizeof(buf), 0);
// Read the complete HTTP request (until the blank line that terminates
// headers) before sending the response. If the server closes the socket
// while the client's request data is still unread in the kernel receive
// buffer, the TCP stack sends RST instead of FIN. That RST resets the
// connection on both sides: it discards the client's receive buffer
// (which already holds our response) and causes any in-progress write on
// the client to fail with ECONNRESET. Reading all request data first
// ensures close() emits a graceful FIN.
{
char rbuf[256];
std::string req;
while (req.find("\r\n\r\n") == std::string::npos) {
auto n = ::recv(cli, rbuf, sizeof(rbuf), 0);
if (n <= 0) { break; }
req.append(rbuf, static_cast<size_t>(n));
}
}
::send(cli,
#ifdef _WIN32
@@ -14471,6 +14869,48 @@ TEST_F(SSLOpenStreamTest, PostChunked) {
auto body = read_all(handle);
EXPECT_EQ("Chunked SSL Data", body);
}
// RFC 7230 §3.3: a response body with neither chunked Transfer-Encoding nor
// Content-Length is terminated by the server closing the connection. When the
// SSL peer sends a close_notify after the body, the client must treat it as a
// clean EOF and return a successful response rather than an error.
TEST(SSLTest, ResponseBodyTerminatedByConnectionClose) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.set_keep_alive_max_count(1);
const std::string expected_body = "Hello from connection-close response!";
svr.Get("/no-content-length", [&](const Request & /*req*/, Response &res) {
res.set_content_provider("text/plain",
[&](size_t offset, DataSink &sink) -> bool {
if (offset < expected_body.size()) {
sink.write(expected_body.data() + offset,
expected_body.size() - offset);
}
sink.done();
return true;
});
});
auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
SSLClient cli(HOST, PORT);
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/no-content-length");
ASSERT_TRUE(res) << "Request failed: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(expected_body, res->body);
}
#endif // CPPHTTPLIB_SSL_ENABLED
//==============================================================================
@@ -16587,41 +17027,6 @@ TEST(BindServerTest, UpdateCerts) {
EVP_PKEY_free(key);
}
// Test that SSLServer(X509*, EVP_PKEY*, X509_STORE*) constructor sets
// client CA list correctly for TLS handshake
TEST(SSLClientServerTest, X509ConstructorSetsClientCAList) {
X509 *cert = readCertificate(SERVER_CERT_FILE);
X509 *ca_cert = readCertificate(CLIENT_CA_CERT_FILE);
EVP_PKEY *key = readPrivateKey(SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(cert != nullptr);
ASSERT_TRUE(ca_cert != nullptr);
ASSERT_TRUE(key != nullptr);
X509_STORE *cert_store = X509_STORE_new();
X509_STORE_add_cert(cert_store, ca_cert);
// Use X509-based constructor (deprecated but should still work correctly)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
SSLServer svr(cert, key, cert_store);
#pragma GCC diagnostic pop
ASSERT_TRUE(svr.is_valid());
// Verify that client CA list is set in SSL_CTX
auto ssl_ctx = static_cast<SSL_CTX *>(svr.tls_context());
ASSERT_TRUE(ssl_ctx != nullptr);
STACK_OF(X509_NAME) *ca_list = SSL_CTX_get_client_CA_list(ssl_ctx);
ASSERT_TRUE(ca_list != nullptr);
EXPECT_GT(sk_X509_NAME_num(ca_list), 0);
X509_free(cert);
X509_free(ca_cert);
EVP_PKEY_free(key);
}
// Test that update_certs() updates client CA list correctly
TEST(SSLClientServerTest, UpdateCertsSetsClientCAList) {
// Start with file-based constructor
@@ -17575,8 +17980,14 @@ TEST(SymlinkTest, SymlinkEscapeFromBaseDirectory) {
}
// Create symlink using absolute path so it resolves correctly
#ifdef PATH_MAX
char abs_secret[PATH_MAX];
ASSERT_NE(nullptr, realpath(secret_dir.c_str(), abs_secret));
#else
auto abs_secret = realpath(secret_dir.c_str(), nullptr);
auto abs_secret_guard = detail::scope_exit([&]() { std::free(abs_secret); });
ASSERT_NE(nullptr, abs_secret);
#endif
ASSERT_EQ(0, symlink(abs_secret, symlink_path.c_str()));
auto se = detail::scope_exit([&] {
+4
View File
@@ -291,10 +291,12 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
{
auto res = cli.Get("/get");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
{
auto res = cli.Get("/redirect/2");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
@@ -306,6 +308,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
for (auto path : paths) {
auto res = cli.Get(path.c_str());
ASSERT_TRUE(res != nullptr);
auto body = normalizeJson(res->body);
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
@@ -317,6 +320,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
int count = 10;
while (count--) {
auto res = cli.Get("/get");
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
}

Some files were not shown because too many files have changed in this diff Show More