Compare commits

..
128 Commits
Author SHA1 Message Date
yhirose d66d9a9599 Release v0.51.0 2026-07-23 22:52:55 -04:00
yhirose 2fa0417754 Fix mbedTLS is_peer_closed() destroying the first response byte
Mbed TLS has no SSL_peek() equivalent, so is_peer_closed() (called after
every SSL request write) probed liveness with a real 1-byte
mbedtls_ssl_read() and discarded whatever it read. If the response had
already arrived by the time the probe ran — plausible under CI load or
plain OS scheduling — the probe silently ate the first byte of the
status line, corrupting the response and surfacing as a fast
"Failed to read connection" failure.

This was the root cause of the long-standing MbedTLS-only CI flakiness
(ServerTest cases failing intermittently on Ubuntu and macOS), previously
worked around by reducing gtest shard parallelism. Fix: push the probed
byte back into MbedTlsSession and have tls::read()/pending() account for
it, so no data is lost.

Also fix a second, unrelated flake: ProxyTunnelTest.
OriginReturning407InsideTunnelDoesNotLeakProxyDigest used "localhost" for
its client while the test's proxy harness only listens on 127.0.0.1;
under dual-stack resolution this could race with another test's server
on ::1 using the same ephemeral port. Pin the test to 127.0.0.1.

With the root cause fixed, restore the mbedTLS CI jobs (ubuntu,
ubuntu-26.04, macOS) to the default shard count instead of the
previously reduced SHARDS=1/2 mitigation.
2026-07-23 22:41:43 -04:00
yhirose 82b1492c3d Merge pull request #2507 from yhirose/flaky-ci-diagnostics
Log res.error() on all ServerTest ASSERT_TRUE(res) assertions
2026-07-23 20:04:02 -04:00
yhirose f7c8455a62 Log res.error() on all ServerTest ASSERT_TRUE(res) assertions
Only 8 of 423 call sites logged the actual error on failure. The
MbedTLS-backend flaky CI failure (connection-level ASSERT_TRUE(res),
~20-30ms) keeps landing on assertions without this diagnostic, so the
real error code has never been captured. Broadens the existing
GetWithRange-only logging (a4d7066) to every plain ASSERT_TRUE(res);
site, no behavior change.
2026-07-23 18:55:41 -04:00
yhirose cc1be5ebb5 Merge pull request #2506 from yhirose/refactor/consolidate-field-validation
Consolidate header field-pair validation into fields::is_field_valid
2026-07-23 17:50:59 -04:00
yhirose cda2bb5342 Consolidate header field-pair validation into fields::is_field_valid
The is_field_name(name) && is_field_value(value) predicate was repeated
across five output paths (set_header, write_headers, write_content_chunked
trailer, perform_websocket_handshake, check_and_write_headers). Introduce
fields::is_field_valid(name, value) and route all five through it so the
CR/LF-injection guard has a single definition. No behavior change.
2026-07-23 17:34:57 -04:00
yhirose 613a41b49d Merge pull request #2505 from metsw24-max/write-headers-crlf-guard
skip invalid fields in write_headers to prevent response splitting
2026-07-23 17:32:22 -04:00
Sayed Kaif 695961f8ae skip invalid fields in write_headers to prevent response splitting 2026-07-23 21:42:37 +05:30
yhirose c64bf21a5e Merge pull request #2504 from emreay-/skip-drain-on-closing-connection
Skip request body drain when connection will close
2026-07-22 12:01:20 -04:00
Emre AyandClaude Opus 4.8 8bbfc90380 Apply clang-format to test.cc and httplib.h
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 10:53:51 +03:00
yhirose bf8baf0739 clang-format 2026-07-21 19:47:15 -04:00
yhirose fd0c18b1b5 Validate connecting peer before honoring X-Forwarded-For
Server::process_request only checked that trusted_proxies_ was
non-empty before deriving req.remote_addr from the X-Forwarded-For
header. It never verified that the actual TCP peer (remote_addr) was
itself one of the trusted proxies, so any client connecting directly
to the server could spoof remote_addr simply by sending an arbitrary
X-Forwarded-For header.

Now X-Forwarded-For is only honored when the connecting peer address
matches an entry in trusted_proxies_.
2026-07-21 19:44:39 -04:00
yhirose bbd56a7e2c Merge pull request #2497 from superm1/superm1/SWSPLAT-23636
Limit the number of header lines per multipart form-data part
2026-07-21 19:15:50 -04:00
yhirose 54b3c1d072 Add Mbed TLS 4.x support (PSA Crypto) (#2502)
* Add Mbed TLS 4.x support (PSA Crypto) for macOS

Auto-detect Mbed TLS 4.x via MBEDTLS_VERSION_MAJOR and adapt the backend:
- Include psa/crypto.h and drop the headers removed in 4.x (ctr_drbg,
  entropy, md5, sha*), gated behind the version macro.
- Compute MD5/SHA-256/SHA-512 via PSA (psa_hash_compute) and initialize
  PSA Crypto once with std::call_once.
- Drop the explicit entropy/CTR-DRBG RNG (PSA provides the TLS RNG) and
  skip the RNG-callback overloads of pk_parse_key/pk_check_pair on 4.x.
- Retry on a TLS 1.3 NewSessionTicket (the 4.x default) in connect, read,
  write and is_peer_closed via a single mbedtls_is_session_ticket() helper,
  so online HTTPS works, including large redirected downloads where the
  ticket arrives mid-write.

Note V4 implies V3, so 3.x-only paths now check V3 && !V4.

Build systems (macOS): the CMake config and pkg-config shipped by Homebrew
resolve 4.x transitively, so CMakeLists.txt and meson.build need no change
for linking; the Makefile links libtfpsacrypto when present, else
libmbedcrypto.

Tests: generate the encrypted client key as both PBES2-AES (3.6+/4.x,
OpenSSL, wolfSSL) and PBES1-3DES (Mbed TLS 2.28) and pick by version, since
4.x dropped DES and 2.28 lacks PBES2. Also generate the IP-host certs in
test/meson.build to match gen-certs.sh and CMakeLists.txt.

* CI: test Mbed TLS 4.x on macOS, 3.x on Ubuntu 26.04

Homebrew's default mbedtls is now 4.x, so switch the macOS build and CI job
to it (drop the mbedtls@3 pin). That leaves 3.x (Ubuntu 24.04 apt ships 2.28,
macOS now 4.x) uncovered, so add an ubuntu-26.04 job whose apt provides Mbed
TLS 3.6. Net coverage: 2.28 (ubuntu-latest), 3.6 (ubuntu-26.04), 4.2 (macOS).

ubuntu-26.04 is a public-preview runner image; fold it into the main ubuntu
matrix once ubuntu-latest moves to 26.04.

* Document Mbed TLS 4.x support and libtfpsacrypto rename

Update README.md and the tour's TLS setup pages (en/ja) to note that
Mbed TLS 4.x is now auto-detected and that it renames libmbedcrypto
to libtfpsacrypto.
2026-07-21 18:45:48 -04:00
metsw24-max 6c4cbd49a2 scan X-Forwarded-For right-to-left in get_client_ip (#2503) 2026-07-21 18:12:40 -04:00
Emre Ay f3e9a4d887 Skip request body drain when connection will close
The post-response drain exists to keep unread framed body bytes from being parsed as a subsequent request on a persistent connection. Once response generation has committed the connection to close, there can be no subsequent request, so draining no longer provides that protection.

Continuing to drain is especially harmful when a ContentReader aborts an unterminated chunked upload: the server can wait indefinitely for the terminal chunk even after sending Connection: close. This delays the transport close that tells an in-flight uploader to stop and leaves a worker occupied consuming discarded data.

Use the finalized response Connection header as the single source of truth for whether to skip the drain. write_response_core already sets this header for keep-alive exhaustion, request-directed closure, handler-directed closure, and error responses. Marking connection_closed then terminates the keep-alive loop and closes the socket.

Add a raw-socket regression test whose ContentReader rejects the first chunk of an unterminated upload. The test verifies that the 409 response announces Connection: close and that the peer observes EOF rather than timing out while the server drains.
2026-07-20 20:36:06 +03:00
Mario Limonciello 877a52f6b3 Limit the number of header lines per multipart form-data part
The multipart/form-data parser bounded the length of each individual
header line via CPPHTTPLIB_HEADER_MAX_LENGTH but did not cap how many
header lines a single part could contain. Within the overall payload
limit, a client could pack a large number of small header lines into
one part, each of which is parsed twice, inflating CPU usage.

Add a per-part header-line counter that resets when a new entry begins
and abort parsing once it reaches CPPHTTPLIB_HEADER_MAX_COUNT, mirroring
the limit already enforced by read_headers() for request headers.
2026-07-18 19:20:51 -05:00
yhirose ef2beaea34 Fix MSVC C4146 warning for unsigned types in from_chars
The from_chars template is instantiated with unsigned types (e.g.
uint64_t for Content-Length), where unary minus on `result` triggers
MSVC warning C4146, failing the 32-bit build under warnings-as-errors.
Use `T(0) - result`, which yields the identical two's-complement value
without the warning.
2026-07-18 20:08:33 -04:00
yhirose 3adc525cec Use detail::from_chars for out-of-range Content-Length check
Replace the strtoull + errno + cast-back dance in get_header_value_u64
with the existing hand-written detail::from_chars, which reports
result_out_of_range at size_t width. This detects the 32-bit truncation
case directly (instead of via a separate cast-back comparison), drops
the reliance on the global errno, and keeps the parsing locale-
independent and consistent with the rest of the codebase.
2026-07-18 18:29:17 -04:00
Sayed Kaif a415362dff update OpenStreamMalformedContentLength.OutOfRange for stricter parsing 2026-07-18 18:29:16 -04:00
Sayed Kaif 982235c0a9 flag out-of-range Content-Length in get_header_value_u64 2026-07-18 18:29:16 -04:00
yhiroseandSayed Kaif 255c075b82 Reject CR/LF in the request target and fail the request cleanly (#2501)
* reject crlf in request target in write_request_line

* declare write_request_line in test.cc for split builds

split.py strips `inline` and moves the definition into httplib.cc, so
detail::write_request_line is not visible from the split httplib.h and
test_split failed to compile. Re-declare it in test.cc, matching what the
base64_encode and getaddrinfo_with_timeout tests already do.

* Fail the request when write_request_line rejects the target

The CR/LF guard in write_request_line returns -1, but ClientImpl::write_request
ignored that return value. A rejected target therefore produced a request-line-
less request (headers only) that the client silently reported as a successful
send. This is the primary path reachable via a decoded redirect Location under
set_path_encode(false), and it also carries the CONNECT target.

Check the return value like ClientImpl::open_stream already does and fail with
Error::Write. Add an end-to-end test asserting the client refuses a CR/LF target
instead of putting it on the wire.

---------

Co-authored-by: Sayed Kaif <metsw24@gmail.com>
2026-07-18 18:07:44 -04:00
yhirose 75938f08c7 Match chunked as the final transfer coding, order-independently (#2500)
is_chunked_transfer_encoding compared the whole Transfer-Encoding field
value against "chunked", so a message whose final coding is chunked but
which names another coding first ("gzip, chunked", valid under RFC 9112
6.1) was read as unframed. On a keep-alive server the body was then left
in the socket and parsed as a smuggled request; open_stream repeated the
check case-sensitively with a raw ==, desyncing the client stream the
same way.

Rework the helper to match the last coding token case-insensitively and
route open_stream through it so both paths agree. The codings may also be
split across multiple Transfer-Encoding lines (RFC 9110 5.3); since
Headers is an unordered_multimap whose duplicate-key iteration order is
not portable, the final coding of a multi-line field cannot be
determined reliably, so treat any such message that names chunked as
chunked (fail safe: a mis-parse only closes the connection, whereas the
opposite error enables smuggling). A unit test covers the helper,
including order-independent multi-line cases.

Based on #2487 by @metsw24-max.
2026-07-18 18:07:29 -04:00
yhirose 0c1cc8c986 Strip Cookie headers on cross-origin redirect
Cookie and Cookie2 headers were forwarded to the new host when following
a cross-origin redirect, even though Host, Proxy-Authorization, and
Authorization were already stripped. Add them to the removal list so
session cookies are not leaked to a different origin.
2026-07-12 10:45:36 -04:00
yhirose 62d899feac Release v0.50.1 2026-07-11 21:37:36 -04:00
yhirose 2f986fd5e5 Fix use-after-free of TLS session in WebSocketClient::shutdown_and_close()
shutdown_and_close() freed the TLS session before ws_->close() sent the
WebSocket close frame. The WebSocket's SSLSocketStream keeps a raw pointer
to that session, so sending the close frame then read/wrote a freed SSL
object. Reorder so ws_->close()/ws_.reset() run while the session is still
alive, then free the session (GHSA-w7p7-f35j-mw7q).
2026-07-11 21:20:36 -04:00
yhirose ed97a6edba Release v0.50.0 2026-07-11 16:01:02 -04:00
yhirose 06b8b91589 Fix Response::content_length_ not reflecting body size in Logger (Fix #2488)
Server::apply_ranges computed the correct Content-Length header for
body-based responses but never updated content_length_, so the
Logger callback always saw 0. Set content_length_ to the final body
size (post-range/post-compression) alongside the header.
2026-07-11 15:52:54 -04:00
yhirose 873d701972 Fix use-after-free in SSLClient destructor with mbedTLS (Fix #2492)
SSLClient::~SSLClient() freed the TLS context before shutting down
the SSL session. mbedTLS sessions hold a raw pointer into the
context's mbedtls_ssl_config, so a live keep-alive session's
close_notify would read freed memory. Shut down the session first,
then free the context.

Add a regression test that destructs an SSLClient while a keep-alive
mbedTLS session is still open.
2026-07-11 15:27:42 -04:00
yhirose b40937cea8 Fix README WebSocket example to match actual API (Fix #2493)
The quick preview used a nonexistent httplib::ws::Message type with
.is_text()/.data. The actual API, as shown in README-websocket.md,
uses a plain std::string with ws.read(msg).
2026-07-11 14:12:25 -04:00
yhirose 568d434e72 Fix CRLF injection in chunked response trailers
Trailer field names and values written by write_content_chunked()'s
done_with_trailer lambda were never validated, unlike every other
header output path (set_header, WebSocket handshake, client request
headers). An application reflecting untrusted input into a trailer
via DataSink::done_with_trailer() could inject CR/LF sequences and
achieve HTTP response splitting.

Skip trailer fields with invalid names or values, matching the
silent-skip behavior of set_header().
2026-07-11 13:54:02 -04:00
yhirose 0fa4912891 Fix broken relative links in cookbook docs (Fix #2490)
Cookbook body links referenced sibling pages with a bare slug
(e.g. `c14-keep-alive`). Under the pretty-URL layout each page lives
in its own directory, so these resolve against the page's own
directory and 404. Prefix them with `../` to match the convention
already used in the tour and llm-app sections.

Verified clean with `docs-gen check`.
2026-07-08 22:45:29 -04:00
Copilot 32abac3de5 Fix ambiguous Get() examples in README after new Params overload (#2486)
* Initial plan

* Add Get(path, params) overload to ClientImpl and Client; fix ambiguous test calls

* Fix clang-format style errors in test.cc

* Fix remaining ambiguous Get() calls in test_proxy.cc and test.cc

* Update README Get() examples to disambiguate Headers overload

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-05 18:43:11 -04:00
yhirose 2132205e1a Release v0.49.0 2026-07-01 23:56:17 -04:00
yhirose a4d7066c2c Log res.error() in GetWithRange test assertions to diagnose flaky CI failures 2026-07-01 23:46:05 -04:00
yhirose bd455f8b91 Add locale-independent ASCII classification helpers (Fix #2482, Fix #2483)
std::isalnum and std::isdigit consult the global C locale, so a byte
like 0xC5 can classify as alphanumeric once an embedder calls
setlocale() (observed on macOS). HTTP grammars are defined over ASCII,
so raw bytes must be classified without regard to the locale.

Add detail::is_ascii_digit/is_ascii_alpha/is_ascii_alnum and use them
at every classification site: multipart boundary validation, token
checks, URI encoding, range header parsing, is_numeric, is_hex, and
IPv4 host detection. Also unify the hand-written digit range checks in
from_chars, URL parsing, and parse_ipv4 onto the same helpers.

With these in place nothing uses <cctype> anymore, so the include is
dropped.
2026-07-01 23:14:53 -04:00
Yanjun Yang(Pluto) 9a5321aadb meson: fix build failure on glibc >= 2.34 without standalone libanl (#2484)
On glibc >= 2.34, getaddrinfo_a is no longer provided by a standalone
libanl shared library — it is built directly into libc.  Newer
architectures (e.g. loongarch64, riscv64) have never shipped a separate
libanl, causing the meson build to fail with:

    C++ shared or static library 'anl' not found

Signed-off-by: Pluto Yang <yangyj.ee@gmail.com>
2026-07-01 22:21:44 -04:00
yhirose 32ff75e355 Make ThreadPool idle timeout configurable at runtime (Fix #2481) 2026-07-01 22:08:01 -04:00
yhirose 45da614ddd Omit default port from WebSocket handshake Host header (Fix #2480)
The WebSocket upgrade request always appended ":port" to the Host
header, violating RFC 6455 Section 4.1 which says the port should be
included only when it is not the default (80 for ws, 443 for wss).
Some CDNs alter routing when the Host header carries an explicit
default port.

Build the Host header with detail::make_host_and_port_string, which
also brackets IPv6 literal hosts correctly.
2026-07-01 21:38:57 -04:00
yhirose f5c8c982df Escape CR and LF in multipart part content types
Same header-injection vector as the name/filename fix: item.content_type
was concatenated into the part's Content-Type header unescaped, so
embedded CR/LF could inject arbitrary part headers.

Escape CR -> %0D and LF -> %0A via escape_multipart_field with a new
escape_quote = false mode. '"' is left intact since it is legal in
Content-Type values (e.g. quoted charset parameters) and appears outside
a quoted-string context here.
2026-07-01 21:38:57 -04:00
yhirose 9e8b960e44 Add public MultipartFormDataWriter for multipart body serialization
Building a multipart/form-data body outside Client::Post/Put/Patch
(e.g. to feed a custom content provider or compose with other body
sources) previously required calling several detail:: functions
directly, which is not a stable interface.

Introduce MultipartFormDataWriter, a small public class that owns the
boundary and delegates to the existing detail:: serializers: whole-body
serialization with known content length, and per-part framing
(item_begin/item_end/finish) for streaming. Also expose
is_valid_multipart_boundary() so callers using an explicit boundary can
validate it without exceptions.
2026-07-01 21:38:57 -04:00
yhirose 8149bb38fc Escape quote, CR and LF in multipart field names and filenames
detail::serialize_multipart_formdata_item_begin() concatenated item.name
and item.filename into the Content-Disposition quoted-string without any
escaping. A '"' in either value silently terminated the quoted-string
early, and embedded CR/LF allowed injecting arbitrary part headers or
forging part boundaries when a filename comes from external input.

Escape both values following the WHATWG HTML standard ('"' -> %22,
CR -> %0D, LF -> %0A), which matches what browsers send in
multipart/form-data bodies. Behavior only changes for inputs that
previously produced malformed HTTP.
2026-07-01 21:38:57 -04:00
Saber Haj Rabiee 3fe32b63b4 Send query string verbatim when path encoding is disabled (#2479)
set_path_encode(false) only suppressed encoding of the path part. The
query part was always run through normalize_query_string(), which
decodes then re-encodes each key/value pair regardless of the flag.

That round-trip is lossy for pre-encoded payloads: re-encoding emits
sub-delimiters literally (%2C->",", %24->"$", %3B->";", ...) and turns
%20 into "+", which a strict RFC 3986 server decodes back as "+"
(0x2B) rather than a space (0x20), corrupting binary query data.

Honor path_encode_ for the query as well: when disabled, append the
caller-supplied query verbatim. Add a regression test asserting on the
raw request target, since the server decodes "+" as space and would
otherwise mask the difference.
2026-07-01 21:18:05 -04:00
yhirose 0ae93881b4 Clarify comment on base64_encode accumulator signedness 2026-07-01 20:12:44 -04:00
metsw24-max a7b886b9cb Use an unsigned accumulator in base64_encode (#2477)
* use an unsigned accumulator in base64_encode

* Forward-declare detail::base64_encode for split builds
2026-07-01 20:12:00 -04:00
yhirose 9d159bb412 Release v0.48.0 2026-06-18 20:13:57 -04:00
yhirose 9ac64e90db Generate IP-host test certificates in CMake build
The cert_ip_cn.pem and cert_ipv6.pem certificates added in ba390f2 were
only generated by gen-certs.sh, which the Makefile-based Linux/macOS CI
uses. The Windows CI builds with CMake, whose own certificate-generation
block was not updated, so cert_ipv6.pem was missing there and
SSLClientServerTest.TlsVerifyHostnameIpv6San failed on is_valid().

Mirror the two openssl commands into test/CMakeLists.txt to keep both
certificate-generation paths in sync.
2026-06-18 19:50:46 -04:00
yhirose ba390f2399 Restrict IP-host hostname verification to iPAddress SANs on Mbed TLS and wolfSSL
An IP-literal host must only be authenticated via a matching iPAddress SAN,
never via the certificate's Common Name (RFC 9110), as the OpenSSL backend
already does through X509_check_ip. The Mbed TLS and wolfSSL backends instead
fell back to the CN when no IP SAN matched, and recognized IPv4 only.

This is a more complete solution for #2476, which gated the CN fallback for
IPv4 hosts only; here the same gap is closed for IPv6 as well, and IPv6
iPAddress SANs are actually matched.

- Add impl::parse_ip_address() to parse IPv4/IPv6 literals into raw bytes
- Match IPv6 (16-byte) iPAddress SANs, not just IPv4
- Skip the CN fallback for IP-literal hosts (both IPv4 and IPv6)
- Remove the unused SSLClient::verify_host* dead code
- Add regression tests and test certificates for the IP-host cases
2026-06-18 12:37:16 -04:00
yhirose 7307c41aa8 Better from_chars implementation (Fix #2475) 2026-06-17 23:21:36 -04:00
Arthur Chan df0b7d243b OSS-Fuzz: Add new fuzzer targets multipart parsing (#2473)
* OSS-Fuzz: Add new fuzzer targets multipart parsing

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>

* Fix formatting

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>

---------

Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-06-13 22:39:45 -04:00
metsw24-max 28d95937b5 use strict hex parsing in decode_query_component (#2472) 2026-06-12 13:54:04 -04:00
yhirose 7532932276 Fix OpenSSL 4.0 deprecation warnings
OpenSSL 4.0 deprecates X509_STORE_get0_objects() and
X509_NAME_get_text_by_NID(), producing warnings on the OpenSSL
backend (surfaced by Debian bug #1138434, which also reported a
const-conversion build error already fixed in v0.43.1).

- Fetch CA store objects via X509_STORE_get1_objects() (thread-safe,
  OpenSSL 3.3+) in get_ca_certs() and get_ca_names(), releasing the
  snapshot with a scope_exit guard; keep using get0 on older OpenSSL,
  BoringSSL, and LibreSSL
- Extract the subject CN in get_cert_subject_cn() with
  X509_NAME_get_index_by_NID()/X509_NAME_get_entry() instead of the
  deprecated X509_NAME_get_text_by_NID()
- Delegate SSLClient::verify_host_with_common_name() to
  tls::get_cert_subject_cn() to drop the remaining deprecated call

Verified against OpenSSL 4.0.1, 3.6.2, and 3.0: builds warning-free
and passes the full test suite.
2026-06-12 13:20:15 -04:00
yhirose 8829fac98e Use full git history in docs workflow for sitemap lastmod 2026-06-10 23:18:15 -04:00
yhirose fe332fa06b Release v0.47.0 2026-06-10 00:24:05 -04:00
yhirose fa981cedae Fix TLS chain verification bypass for IP hosts on Mbed TLS and wolfSSL
For connections to IP-literal hosts with server certificate
verification enabled, the Mbed TLS and wolfSSL backends downgraded the
verification mode before the handshake because no hostname could be
bound for in-handshake checks:

- SSLClient skipped certificate chain validation entirely; only the
  post-handshake identity check (IP SAN match) remained, so any
  untrusted certificate carrying a matching IP SAN was accepted
- The WebSocket client skipped verification altogether on Mbed TLS,
  accepting any certificate

Keep the verification mode enabled for IP hosts and verify the
certificate identity post-handshake via tls::verify_hostname(), which
supports IP SANs on all backends. The WebSocket path now performs the
same post-handshake identity check as SSLClient. On Mbed TLS, sessions
explicitly opt out of in-handshake hostname verification (mandatory
since Mbed TLS 3.6.4) and the post-handshake check covers identity
instead; DNS hosts still bind the hostname during the handshake. Also
stop sending SNI for IP hosts on Mbed TLS and wolfSSL (RFC 6066).
2026-06-10 00:07:27 -04:00
yhirose 39ec7d0508 Add enable_system_ca() and unify WebSocketClient CA handling (#2471)
Add an explicit opt-in for loading system CA certs alongside a custom
CA, addressing the request in #2471. The default behavior is unchanged:
a custom CA remains exclusive.

- Add Client/SSLClient/WebSocketClient::enable_system_ca(bool); the
  policy carries over to redirect clients
- Extract the CA loading policy into detail::load_client_ca_config()
  shared by SSLClient and WebSocketClient, making WebSocketClient
  exclusive by default (it previously always merged system certs)
- Make the WebSocketClient TLS context live as long as the client,
  fixing a use-after-free when reconnecting after set_ca_cert_store()
- Free the source store in the Mbed TLS and wolfSSL set_ca_store()
  backends, honoring the take-ownership contract (memory leak)
- Verify IP hosts against IP SANs in the OpenSSL set_hostname() backend
  so WebSocket connections to IP hosts can use full verification
2026-06-09 21:50:37 -04:00
yhirose e7e7bf7b44 Fix set_ca_cert_store() breaking CA exclusivity and redirect CA transfer
Since the TLS abstraction layer was introduced, SSLClient::set_ca_cert_store()
handed the store to the TLS context without leaving any trace on the client.
As a result:

- load_certs() merged system CA certs into the user-provided store,
  silently broadening the trust set (a custom store used to suppress
  system CA loading)
- Client::load_ca_cert_store() went through the native store path,
  bypassing the PEM retention used for redirect transfer, so CA certs
  were not carried over to clients created for HTTPS redirects
- The Windows Schannel verification skip for custom CA certs did not
  trigger

Track custom store assignment with a flag checked by load_certs() and
the Schannel path, and route Client::load_ca_cert_store() through the
PEM-based SSLClient path so the CA data survives redirects.
2026-06-09 21:03:19 -04:00
yhirose 78ff94e604 Wait for server startup before running benchmark 2026-06-09 20:55:03 -04:00
metsw24-max 018ce8e4ca cast to unsigned char before ctype calls in is_hex and is_token_char (#2469) 2026-06-09 19:34:43 -04:00
yhirose 77bdf7921a Read request body after route matching and pre-request handler
Previously, for regular handlers the request body was read in routing()
before the route was matched, so the pre-request handler always saw an
already-read body. The ContentReader path, in contrast, ran the
pre-request handler before the body was read. This inconsistency made
it impossible to reject a request (e.g. failed per-route authentication
via req.matched_route) without buffering a potentially large body.

Move the read_content() call into dispatch_request(), after route
matching and the pre-request handler, so both paths behave the same:
route matching -> pre_request_handler -> body read -> handler. A
request rejected by the pre-request handler no longer reads the body
at all; the existing keep-alive drain logic still consumes any framed
body afterwards.

Note: code that referenced req.body or body-derived form fields inside
the pre-request handler will now see an empty body. Inspect headers,
path, query parameters, or matched_route instead.

Also document the handler execution order in README and update the
pre-request cookbook pages (en/ja).
2026-06-09 13:49:56 -04:00
yhirose 79d83feb18 Fix WebSocketClient dropping query string from URL during handshake (#2468)
The constructor stored only uc.path in path_, discarding uc.query, so the
WebSocket upgrade handshake sent the Request-URI without the query string.
Append the query to path_ so query parameters (e.g. auth tokens) are sent.
2026-06-07 17:08:11 -04:00
yhirose fe56a07da5 Wait for in-progress CI runs before releasing
The release check treated runs with an empty conclusion as failures.
Now it inspects each run's status and aborts with an error if any CI
check is still running, so releases wait until CI completes.
2026-06-06 13:38:36 -04:00
Kim, Hyuk 907257f51d add set_hostname_addr_map to WebSocketClient (#2463)
* add set_hostname_addr_map to WebSocketClient

* add WebSocketTest unit test cases
* SpecifyServerIPAddress_AnotherHostname
* SpecifyServerIPAddress_RealHostname

* Change wrong_ip from 0.0.0.0 to 192.0.2.1

Use 192.0.2.1 (RFC 5737 documentation address) to ensure it acts
as a non-routable address and does not alias to loopback.

* Fix style check

* set short timeout in WebSocketTest.SpecifyServerIPAddress_RealHostname

cannot reach wrong_ip
2026-06-05 16:34:20 -04:00
yhirose 0c2f535b74 Fix #2467 2026-06-04 21:20:37 -04:00
Florian Fischer c7ba963a17 Ignore ranges for unknown-length streams (#2465) 2026-06-04 20:15:21 -04:00
yhirose 4465e81b9f Fix #2464 2026-06-03 22:24:52 -04:00
yhirose 44215e23e9 Release v0.46.1 2026-06-01 12:24:27 -04:00
yhirose 91219d4508 Fix #2458: send body when no 100 Continue arrives over TLS (#2460)
The auto-added `Expect: 100-continue` (for bodies >= 1024 bytes) decided
whether to withhold the request body based on raw socket readability via
select_read(). Over TLS, post-handshake records such as TLS 1.3 session
tickets make the socket readable without any HTTP response being
available, so the client withheld the body and then blocked reading a
response that never came, failing with `Failed to read connection`.

Decide based on whether a status line can actually be read within the
100-continue timeout instead: temporarily shorten the read timeout, try
to read the status line, and if none arrives, send the body and proceed
as usual (matching curl). This keeps the `100 Continue` and early
final-response paths working while no longer being fooled by TLS records.

Add a regression test using a raw OpenSSL server that never sends
`100 Continue`.
2026-05-29 06:19:40 -04:00
NsPro04 c86c192f3e Fix: (#2459)
"httplib.h(5733,29): warning : missing field 'InternalHigh' initializer [-Wmissing-field-initializers]"
"httplib.h(5742,28): warning : missing field 'ai_family' initializer [-Wmissing-field-initializers]"
2026-05-28 18:19:33 -04:00
yhirose 008e107d0f Release v0.46.0 2026-05-25 00:30:27 -04:00
yhirose d278f965cc Fix #2457 2026-05-25 00:21:57 -04:00
yhirose 4c4b62dd7e Feature 2446 no proxy env (#2448)
* Route proxy-enabled checks through is_proxy_enabled_for_host helper

In preparation for NO_PROXY support (#2446), centralize the proxy-enabled
decision in a single helper so the upcoming bypass logic can be added in
one place rather than to six divergent call sites. The helper's body for
now is identical to the existing condition; the host parameter is unused
until set_no_proxy() lands.

Refactored sites:
  ClientImpl::create_client_socket
  ClientImpl::handle_request           (HTTP request rewrite)
  ClientImpl::setup_redirect_client
  ClientImpl::process_request          (SSL is_proxy_enabled flag)
  SSLClient::setup_proxy_connection
  SSLClient::ensure_socket_connection

The two prepare_default_headers Proxy-Authorization injection blocks
(currently gated only on proxy auth credentials being set) are
intentionally not wrapped here. Doing so would change behavior in the
rare misconfiguration case where credentials are set without set_proxy,
so the gating is deferred to the NO_PROXY commit where it becomes
meaningful.

No behavior change. All 608 unit tests and the 22 squid-backed proxy
tests pass.

* Add detail::parse_proxy_url with control-char and scheme validation

Building block for the upcoming set_proxy_from_env (#2446). Parses
"http(s)://[user[:pass]@]host[:port][/...]" into a detail::ProxyUrl
struct.

Rejects:
  - empty input
  - any control character (< 0x20 or 0x7F), including CR/LF/NUL — these
    would otherwise let a malicious env value inject extra header lines
    into a CONNECT request or Proxy-Authorization header
  - schemes other than http and https
  - ports outside [1, 65535]
  - malformed IPv6 host literals (validated via inet_pton(AF_INET6))
  - non-numeric or trailing-garbage port strings

Notes:
  - userinfo is split on the LAST '@' so passwords containing '@' are
    preserved in the password field
  - if no port is present, defaults to 80 (http) / 443 (https)
  - integer parse goes through detail::from_chars to stay compatible
    with -fno-exceptions builds

The helper has no callers yet; it lands consumer-side when
set_proxy_from_env arrives. All 608 unit tests pass.

* Add NO_PROXY parsing and matching helpers in detail namespace

Building blocks for the upcoming Client::set_no_proxy (#2446):

  - NoProxyEntry / NoProxyKind: parsed list entry (wildcard, hostname
    suffix, IPv4 CIDR, IPv6 CIDR)
  - NormalizedTarget: pre-normalized form of the connection's target
    host (lowercase, brackets stripped, trailing dot stripped, with
    inet_pton already attempted)
  - parse_no_proxy_entry / parse_no_proxy_list: token / list parsing.
    Port-specific entries are rejected by design — cpp-httplib's other
    host-keyed APIs (e.g. set_hostname_addr_map) are hostname-only, so
    supporting host:port for NO_PROXY alone would be inconsistent.
  - ipv4_in_cidr / ipv6_in_cidr: CIDR membership. IPv4 special-cases
    prefix=0 to avoid the (1u << 32) shift UB. IPv6 uses byte-wise
    memcmp plus a masked partial-byte compare.
  - normalize_target: prepares the target host for matching. Routes
    every IP literal through inet_pton so "127.0.0.1" vs
    "127.000.000.001" vs decimal-form integers cannot be used to bypass
    a NO_PROXY entry via alternate string forms.
  - host_matches_no_proxy: matches a normalized target against an
    entry list. Hostname suffix matching uses a dot-boundary rule so
    "evilexample.com" does NOT match the entry "example.com". IPv4 and
    IPv6 entries match only their own address family — IPv4-mapped IPv6
    ("::ffff:1.2.3.4") is not cross-matched against IPv4 entries.

These helpers have no callers yet; they land consumer-side in the
upcoming set_no_proxy / set_proxy_from_env commits. All 608 unit tests
pass.

* Add Client::set_no_proxy and wire NO_PROXY into proxy decision

Implements the user-facing half of #2446 (set_proxy_from_env follows in
the next commit). When a NO_PROXY pattern matches the target host, the
client now bypasses the configured proxy and the corresponding
Proxy-Authorization header is suppressed.

Public API:
  - Client::set_no_proxy(const std::vector<std::string> &patterns)
    Patterns: "*", hostname suffix (e.g. "example.com" or
    ".example.com"), IPv4/IPv6 CIDR (e.g. "10.0.0.0/8", "fe80::/10"),
    or single IP literals. Replaces any previous list. Malformed
    entries are silently dropped.

Internals:
  - is_proxy_enabled_for_host now consults no_proxy_entries_, normalizing
    the target through inet_pton so leading-zero or alternate-form IPs
    cannot be used to bypass an entry.
  - prepare_default_headers gates both Proxy-Authorization injection
    blocks (basic and bearer) on is_proxy_enabled_for_host(host_).
    Previously, Proxy-Authorization was sent whenever proxy auth
    credentials were configured, even when the request was going direct
    to the target. With NO_PROXY now in play, that path would leak
    proxy credentials to the destination server — analog of the
    redirect-leak class of bugs (cf. CVE-2023-32681 in Python requests,
    GHSA-6hrp-7fq9-3qv2 in cpp-httplib).
  - setup_redirect_client now takes the redirect target host as a
    parameter and re-evaluates is_proxy_enabled_for_host against it.
    no_proxy_entries_ is always copied to the redirect client so the
    bypass policy follows across redirects. This is the cross-origin
    leak surface that GHSA-c3h8-fqq4-xm4g lives in; centralizing the
    decision through is_proxy_enabled_for_host removes the chance of
    branch divergence.
  - copy_settings copies no_proxy_entries_.

The slight behavior change for the rare misconfiguration "set
proxy_basic_auth without set_proxy" — Proxy-Authorization is no longer
sent in that case — is deliberate. The header has no addressee when
the proxy is unset.

All 608 unit tests and 22 squid-backed proxy integration tests pass.

* Add Client::set_proxy_from_env with httpoxy mitigation

Final user-facing piece for #2446. Reads proxy-related environment
variables and configures the client.

  - HTTPS clients (SSLClient) read https_proxy / HTTPS_PROXY
  - HTTP clients read http_proxy (lowercase only — see below)
  - Both also read no_proxy / NO_PROXY
  - Returns true if at least one variable was found and applied

The lowercase-only http_proxy rule mitigates httpoxy / CVE-2016-5385.
In CGI / FastCGI environments the uppercase HTTP_PROXY collides with
the HTTP_* namespace used to expose request headers, so a remote
attacker controlling the "Proxy:" header can inject a proxy URL.
cpp-httplib follows curl, Go, and Python requests in honoring only
the lowercase form. https_proxy/HTTPS_PROXY and no_proxy/NO_PROXY do
not have this problem because their names don't begin with HTTP_.

Scheme dispatch uses virtual is_ssl(): an SSLClient picks
https_proxy and a plain ClientImpl picks http_proxy. There is
intentionally no cross-scheme fallback — the two variables describe
different traffic.

set_proxy_from_env() reads getenv() synchronously and is documented
as "call once at startup" — concurrent setenv from other threads is
undefined.

All 608 unit tests pass.

* Add NO_PROXY behavior tests

27 black-box tests exercising the public Client API only (no detail::
calls, BORDER-friendly; no EXPECT_NO_THROW, -fno-exceptions-friendly).

In-process proxy mock + target server. Each test asserts which side
of the routing decision each request landed on, and what headers (in
particular Proxy-Authorization) the receiving side saw.

Coverage:

  Suffix matching (dot-boundary rule)
    - exact-host match
    - subdomain match
    - "evilexample.com" does NOT match "example.com"  ← regression
      guard for the classic NO_PROXY suffix-match pitfall
    - "example.com.evil.com" does NOT match
    - leading-dot pattern still matches the bare domain (Go/curl
      convention)
    - case-insensitive
    - trailing-dot host normalization

  Wildcard
    - "*" bypasses everything

  IP normalization
    - exact IPv4 match
    - "::1" matches "0:0:0:0:0:0:0:1" via inet_pton
    - IPv4-mapped IPv6 ("::ffff:127.0.0.1") is NOT cross-matched
      against an IPv4 entry

  CIDR
    - basic v4 in-cidr / not-in-cidr
    - "0.0.0.0/0" (prefix=0; verifies no shift UB)
    - bare IP treated as /32
    - malformed prefix (/33) silently dropped → no NO_PROXY effect

  Proxy-Authorization handling
    - suppressed when NO_PROXY matches the target
    - sent when NO_PROXY does not match

  Backward compat
    - default behavior unchanged when set_no_proxy is never called

  Parsing edge cases
    - port-specific entries ("host:port") rejected
    - empty / whitespace tokens dropped

  Cross-origin redirect (analog of GHSA-6hrp-7fq9-3qv2)
    - redirect target in NO_PROXY → redirect leg goes direct, no
      Proxy-Authorization carried over

  set_proxy_from_env (Unix only — uses setenv/unsetenv)
    - lowercase http_proxy applied
    - uppercase HTTP_PROXY ignored (httpoxy / CVE-2016-5385)
    - NO_PROXY-only env returns true and applies the bypass list
    - CRLF in env value rejected (cf. CVE-2026-21428)
    - empty env value treated as unset

635 tests (608 prior + 27 new) pass under both the regular and the
split builds.

* Document set_no_proxy and set_proxy_from_env in README

Adds two subsections under "Proxy server support":

  - "Bypass the proxy for specific hosts (NO_PROXY)" — set_no_proxy,
    pattern syntax, dot-boundary rule, IP normalization, limitations
    (no port-specific entries, no v4-mapped v6 cross-match, replace
    semantics).

  - "Read proxy settings from the environment" — set_proxy_from_env,
    which variables are read, the lowercase-only http_proxy rule with
    an inline httpoxy / CVE-2016-5385 explanation, threading
    expectations.

Documentation only. Closes the doc gap from #2446.

* Document NO_PROXY and set_proxy_from_env in cookbook c16-proxy

Replaces the now-incorrect Note at the bottom of c16-proxy ("cpp-httplib
does not read HTTP_PROXY...") with the actual API.

JA is the master per the project's translation workflow; the EN
translation lands in the same PR. Both pages remain `status: "draft"`
for normal review.

Adds two sections:

  - Bypass the proxy for specific hosts (set_no_proxy):
    pattern syntax, dot-boundary rule, case-insensitivity, IP
    normalization via inet_pton, port-specific-entries unsupported,
    malformed entries dropped.

  - Read proxy settings from the environment (set_proxy_from_env):
    which variables are read, lowercase-only http_proxy with an
    inline httpoxy / CVE-2016-5385 explanation, threading caveat.

* Simplify NO_PROXY implementation per review

Apply seven post-implementation cleanups:

  - Move ProxyUrl, ProxyEnvSettings and most helper forward declarations
    below the BORDER. Only NoProxyKind/NoProxyEntry/NormalizedTarget stay
    above (they are used as ClientImpl members or by inline cache state).
    This shrinks the public header surface area considerably.

  - Drop ProxyUrl::scheme: the field was write-only after parsing. Track
    is_https as a local during parse_proxy_url and use it for the
    default-port branch directly.

  - Hoist the duplicate is_proxy_enabled_for_host(host_) gate in
    write_request: the previous form had two adjacent gates bracketing
    an unrelated end-server bearer-token block. Reordering puts the two
    proxy-auth blocks together under a single gate.

  - Drop the redundant trim_copy + empty-check inside parse_no_proxy_list:
    detail::split already trims each token and skips empties, so the inner
    work was dead code.

  - Cache normalize_target(host_) on the client. host_ is const, so the
    normalized form is invariant for the client's lifetime. The gate is
    called up to 7 times per request when NO_PROXY is configured;
    caching avoids repeating two heap allocations + two inet_pton calls
    per request. Cross-host calls (only setup_redirect_client passing
    next_host) still compute fresh.

  - Trim narrative comments in setup_redirect_client and
    set_proxy_from_env: replace WHAT-narration with single-line WHY
    statements.

  - Drop test comments that paraphrased their own test name.

All 635 unit tests pass under both the regular and split builds.

* Inline proxy URL parsing and env reading; drop intermediate structs

The previous design had two intermediate structs that existed only to
ferry parsed values between helper functions and the consuming method:

  - detail::ProxyUrl: filled by parse_proxy_url, drained back into
    proxy_host_ / proxy_port_ / proxy_basic_auth_* by set_proxy_from_env.
  - detail::ProxyEnvSettings: bundle of two ProxyUrl + a NoProxyEntry
    vector returned by read_proxy_env, drained by set_proxy_from_env.

Both bundles had exactly one producer and exactly one consumer. Drop
them and let the parsing flow directly into ClientImpl state:

  - New private member ClientImpl::apply_proxy_url(url) parses a proxy
    URL and, on success, assigns the result to proxy_host_, proxy_port_,
    and proxy_basic_auth_*. Same validation as before (CRLF rejection,
    scheme allowlist, port range, IPv6 bracket validation), same commit-
    on-success ordering — the local variables are kept until every check
    has passed so a malformed URL leaves no partial state.

  - set_proxy_from_env now reads getenv() directly, dispatches between
    https_proxy / http_proxy via virtual is_ssl(), and applies via
    apply_proxy_url. NO_PROXY is parsed in place via parse_no_proxy_list.

Net effect:

  - Two structs and two free helper functions removed (~150 lines of
    declaration + body deleted).
  - set_proxy_from_env body grows ~20 lines (still well under 50).
  - Per-request hot path is unchanged (NoProxyEntry / NormalizedTarget
    cache stays). Setup path is marginally faster (no intermediate
    string copies through ProxyUrl / ProxyEnvSettings).

635 unit tests pass under both the regular and split builds.

* Trim doc comments to match the rest of httplib.h

The new code carried inline doc comments (15-line set_no_proxy block,
18-line set_proxy_from_env block, plus narrating comments inside parser
bodies, plus section dividers in the test file) that were heavy
compared to the rest of the codebase — neighboring setters like
set_proxy / set_proxy_basic_auth carry no doc at all, the test file
does not use sub-section dividers, and the README / cookbook already
document the behavior in detail.

Removed:
  - Public-API doc blocks on set_no_proxy and set_proxy_from_env.
  - Narrating comments inside parse_no_proxy_entry, normalize_target,
    apply_proxy_url, host_matches_no_proxy that were just describing
    the obvious code structure.
  - Multi-line BORDER-rationale meta comments.
  - In-test sub-section dividers ("// ---- Hostname suffix matching",
    etc.) and per-class doc comments on the test fixtures.
  - Test-side comments that paraphrased their own test name.
  - Redundant ordering comments inside setup_redirect_client.

Kept:
  - Security WHY comments (CRLF rejection, dot-boundary suffix matching,
    httpoxy / CVE-2016-5385, GHSA-6hrp-7fq9-3qv2 analog, CVE-2026-21428).
  - Regression-target WHY comments (UB shift on prefix=0).
  - Non-obvious external knowledge (detail::split already trims).

635 unit tests still pass under both the regular and split builds.

* Add NO_PROXY tests covering edge cases found during PR review

Three regression guards added during review of an alternate NO_PROXY
implementation (PR #2449). All three pass on the current implementation
and surface bugs in the alternate one:

  - BareIPv6LiteralMatchesIPv6Cidr: a host given as a bare IPv6 literal
    (no surrounding brackets) must still be recognized as IPv6 for CIDR
    matching. An implementation that only detects IPv6 when the host
    string starts with '[' would split the host at the first ':' and
    misclassify it as a hostname.

  - TrailingDotOnEntryIsNormalized: trailing dots must be canonicalized
    on BOTH sides — host and entry. An implementation that strips the
    host-side trailing dot only would fail to match host "example.com"
    against entry "example.com." because the substring lengths differ.

  - ValidEntryWithSurroundingWhitespaceStillMatches: an entry with
    leading/trailing whitespace must still match. An implementation
    that feeds raw tokens directly to inet_pton would reject valid
    CIDRs ("  10.0.0.0/8  ") because of the spaces.

635 unit tests pass.

* Unify IPv4/IPv6 CIDR matching into a single byte-buffer helper

Adopts the unified 16-byte address representation suggested by the
alternate NO_PROXY implementation in PR #2449. Both v4 and v6 entries
now share one storage type and one matcher; the v4/v6 distinction is
only the address-family flag and the max prefix length.

  - detail::NoProxyEntry: replaces in_addr v4_net + in6_addr v6_net
    with a single IPBytes net (std::array<uint8_t, 16>). v4 occupies
    the first 4 bytes, v6 fills all 16.
  - detail::NormalizedTarget: replaces in_addr v4 + in6_addr v6 with
    a single IPBytes ip.
  - Replaces detail::ipv4_in_cidr and detail::ipv6_in_cidr with one
    detail::ip_in_cidr that takes the address, the network, the prefix
    length and the family's max bits (32 for v4, 128 for v6). The mask
    is constructed by the byte-fill approach from the previous v6
    helper, which is straightforward to read and avoids the shift UB
    that the v4 helper had to special-case.
  - The NoProxyKind enum keeps IPv4Cidr / IPv6Cidr as separate values
    so the match dispatch stays explicit and IPv4 entries cannot
    accidentally cross-match an IPv6 target (the same address-family
    isolation the previous code had).

Net change: -28 lines + -1 helper function. All 30 NoProxyTest cases
plus 643 unit tests pass under both the regular and split builds.

* Drop set_proxy_from_env per #2446 discussion

Per @unterwegi's feedback in #2446, environment variable handling
conflicts with cpp-httplib's long-standing policy of explicit
configuration (e.g. set_ca_cert_path requires explicit paths instead
of reading SSL_CERT_FILE / SSL_CERT_DIR). The NO_PROXY matching logic
is the genuinely tricky part worth keeping in the library; getenv
parsing is trivial and is left to the caller.

- Remove Client::set_proxy_from_env, ClientImpl::set_proxy_from_env,
  and ClientImpl::apply_proxy_url
- Remove ScopedEnv test helper and env-driven NoProxyTest cases
- Replace the "Read proxy settings from the environment" docs with a
  short snippet showing how to parse no_proxy and feed set_no_proxy()
- Keep set_no_proxy() and all NO_PROXY pattern matching intact

* docs: blend NO_PROXY env-var note into c16-proxy cookbook style

Match the granularity of the surrounding sections: imperative heading,
inline paragraph instead of a heavyweight callout, and a simpler getenv
snippet without the C++17 if-init.

* Skip digest 407 retry when target is bypassed by NO_PROXY

Before this fix, a NO_PROXY-bypassed origin that returns
407 Proxy-Authentication-Required with a Digest challenge would
trigger the same retry path the proxy uses, computing a
Proxy-Authorization header from proxy_digest_auth_* and sending the
user's proxy credentials directly to that (potentially hostile)
origin.

A 407 from a direct origin is semantically meaningless — RFC 9110
defines it strictly as a proxy response. Skip the retry when the
current target is not actually going through the proxy and let the
407 propagate to the caller unchanged.

Regression test BypassedTargetReturning407DoesNotLeakProxyDigest
Credentials reproduces the leak without this gate.

* Make set_no_proxy safe across redirects and keep-alive

Two correctness bugs that the dynamic NO_PROXY API exposed:

1. Multi-hop redirect through a bypassed host lost the proxy.
   setup_redirect_client only copied proxy_host_/port and the proxy auth
   credentials when is_proxy_enabled_for_host(next_host) was true. After
   a chain like A (proxied) -> B (NO_PROXY-matched, direct) -> C, the
   redirect client built for B had no proxy configured, so the further
   B -> C hop went direct even when C should have been proxied. Copy the
   proxy configuration unconditionally and let is_proxy_enabled_for_host
   gate at send time. The next_host parameter is no longer needed and
   removed from the signature.

2. Keep-alive socket reuse with a stale bypass decision. set_proxy() /
   set_no_proxy() left the existing keep-alive socket open, so the next
   request reused a socket pointed at the previous endpoint (proxy vs
   origin) while write_request emitted the new request-line form
   (absolute vs relative URL). Add invalidate_keep_alive_socket() and
   call it from both setters; the helper handles the in-flight case by
   deferring the close.

Regression tests MultiHopRedirectThroughBypassedHostKeepsProxy and
KeepAliveSocketInvalidatedOnSetNoProxy reproduce each bug without the
respective fix.

* Tighten NO_PROXY entry parsing

Three small parser fixes surfaced during code review:

- Accept bracketed IPv6 entries like "[::1]" and "[fe80::]/10". Users
  coming from URL syntax naturally write the bracketed form; previously
  it was silently rejected because inet_pton does not accept brackets
  and the subsequent ':' check tripped.
- Reject malformed trailing-slash CIDRs like "127.0.0.1/" instead of
  silently treating them as /32 (or /128). A typoed entry quietly
  turning into a single-host bypass changes semantics with no
  diagnostic.
- Delete detail::parse_no_proxy_list — leftover from the removed
  set_proxy_from_env path, no longer called from anywhere.

New regression tests: BracketedIPv6EntryAccepted,
BracketedIPv6CidrEntryAccepted, TrailingSlashCidrIsRejected.

* Refactor: introduce disconnect() and remove invalidate_keep_alive_socket

Replace the repeated `shutdown_ssl + shutdown_socket + close_socket`
pattern with a single `disconnect(bool gracefully)` helper. Used by
`stop()`, the send_() peer-closed and epilogue branches, and the close
in process_request after a non-keep-alive response.

Drop `invalidate_keep_alive_socket()` — its body collapses to a
`lock + disconnect()` pair which is now inlined in `set_proxy()` and
`set_no_proxy()` directly.

Also simplify `setup_redirect_client`: drop the now-unused next_host
parameter and the verbose comment block; the per-target proxy decision
is re-evaluated at send time anyway.

Net -47 lines in httplib.h.

* Fix MultiHopRedirect test on Windows; trim NoProxyTest comments

The bypass leg redirected to "http://localhost:<port>/...", but on
Windows `localhost` resolves to ::1 first while the mock server is
bound to 127.0.0.1, causing the redirect leg to time out. Use the
literal 127.0.0.1 in the Location and switch the NO_PROXY entry to
match, so the test exercises the same multi-hop path on every
platform.

Also trim the heavier inline comments and EXPECT messages I added on
recent NoProxyTest cases so they match the surrounding test style.

* Consolidate NoProxyTest server boilerplate; drop hardcoded sentinel ports

Add a small ScopedServer helper to no_proxy_test that wraps the
bind/listen/thread/cleanup dance (~13 lines per server before). Use it
to rewrite the four big tests (Redirect, BypassedTarget407, MultiHop,
KeepAlive), shaving ~100 lines.

Also drop the hardcoded port-1 / port-80 sentinels that violated the
"new standalone tests MUST use bind_to_any_port" convention and risked
collisions across gtest shards: re-use existing dynamic ports
(target.port() / bypass_server.port()) instead.

Verified pass under 4-shard parallel run.

* Trim README NO_PROXY section to match surrounding granularity

The block had ballooned to 62 lines while neighboring subsections
(Authentication, Proxy server support, Range, Redirect) are 13-18 each.
Collapse to a single code example + one-line behavior summary; point at
the cookbook for the entry-form details, env-var parsing snippet, and
httpoxy note that used to live inline.
2026-05-24 23:50:48 -04:00
yhirose b1792ef29c Release v0.45.1 2026-05-24 20:58:48 -04:00
yhirose 0f3d063f0a ci: add best-effort BoringSSL job (#2456)
Adds Ubuntu and macOS CI jobs that build BoringSSL from source and exercise cpp-httplib's existing OpenSSL backend path (continue-on-error: best-effort). Makes SSLClientServerTest.TlsVerifyHostname backend-aware (BoringSSL is SAN-only per RFC 6125 §6.4.4). README notes BoringSSL as a best-effort variant with the C++14 and SAN-only caveats.
2026-05-24 02:48:46 -04:00
sakurai-ryuhei 0d7d637466 Fix zstd detection in installed httplibConfig.cmake (#2453) 2026-05-23 11:59:58 -04:00
yhirose 1ff0c8588d Fix iOS build break and modernize macOS Keychain cert loading (#2455)
* Replace deprecated SecTrustCopyAnchorCertificates on macOS

SecTrustCopyAnchorCertificates was deprecated in macOS 13. Switch to
SecTrustSettingsCopyCertificates, iterating over the System, Admin, and
User trust domains to retain equivalent coverage of anchor certificates.

* Restrict Keychain cert loading to macOS

TARGET_OS_MAC is true on all Apple platforms including iOS, tvOS, and
watchOS, which caused the keychain enumeration path to be compiled on
iOS where SecTrustSettingsCopyCertificates is unavailable.

Narrow the auto-enable and the Security.h include guards to
TARGET_OS_OSX, and emit an explicit #error when the user defines
CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on a non-macOS Apple platform,
directing them to use set_ca_cert_path() with a bundled CA file.

Addresses the iOS build break reported in #2454.

* Add iOS header parse check to CI

Run a cross-compile syntax check against the iOS SDK to catch
accidental use of macOS-only APIs or guards (e.g. TARGET_OS_MAC vs
TARGET_OS_OSX) that would silently break iOS builds. Also verify that
defining CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on iOS fires the
expected #error.

iOS is not officially supported as a runtime target; this job only
guarantees the header stays parse-clean on iOS toolchains.
2026-05-23 08:39:45 -04:00
NsPro04 b1cc8095a8 Specifying "Server::stop()" as noexcept (#2451)
* The current implementation of "Server::stop()" doesn't throw an exception, so why not specify this explicitly?

* Adding the missing "noexcept" to the declaration
2026-05-16 09:50:08 -04:00
yhirose 28f8264d13 Release v0.45.0 2026-05-15 09:22:11 +09:00
yhirose 91271c062d Fix keep-alive corruption on requests without framed body (#2450) 2026-05-15 06:57:51 +09:00
yhirose d755c43d58 Extract has_framed_body and is_connection_persistent helpers 2026-05-15 06:56:16 +09:00
yhirose 5c9285776e Fix crash on empty X-Forwarded-For with trusted proxies configured 2026-05-14 23:19:36 +09:00
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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.7 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
yhiroseandClaude Opus 4.6 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
112 changed files with 6588 additions and 1763 deletions
+3
View File
@@ -20,6 +20,9 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
with:
# Full history so sitemap <lastmod> reflects each page's real last commit date
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install docs-gen
+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:
+320 -5
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 }})
@@ -114,6 +117,265 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
# Ubuntu 26.04's apt ships Mbed TLS 3.6, giving 3.x coverage that
# ubuntu-latest (24.04 = 2.28) and macOS (Homebrew = 4.x) no longer provide.
# Uses the 26.04 public-preview image; fold into the main ubuntu matrix once
# ubuntu-latest moves to 26.04.
ubuntu-2604-mbedtls:
runs-on: ubuntu-26.04
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: ubuntu-26.04 (mbedtls 3.x)
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: install Mbed TLS
run: sudo apt-get install -y libmbedtls-dev
- name: build and run tests (Mbed TLS)
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
# BoringSSL is Google's fork of OpenSSL. It has no API stability guarantee
# and is not packaged by distros, so we build it from source. cpp-httplib
# treats it as an OpenSSL backend variant via the OPENSSL_IS_BORINGSSL
# macro (see httplib.h). This job is best-effort: continue-on-error keeps
# upstream API drift from blocking PRs while still surfacing breakage.
ubuntu-boringssl:
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')
continue-on-error: true
name: ubuntu (boringssl, best-effort)
env:
# Tracking HEAD keeps us honest about upstream churn. If breakage
# becomes routine, replace HEAD with a 40-char commit SHA; the
# resolve step uses the SHA directly when it matches that shape.
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(nproc)" --target install
- name: build and run tests (BoringSSL)
# Override OPENSSL_SUPPORT to point the existing OpenSSL Makefile path
# at BoringSSL's prefix. BoringSSL defines OPENSSL_IS_BORINGSSL in
# <openssl/base.h>, which httplib.h and test.cc use to switch on API
# differences (e.g. SAN-only hostname verification, no CN fallback).
#
# BoringSSL's public headers (<openssl/stack.h>) use std::enable_if_t,
# so consumers must compile with C++14 or later. cpp-httplib itself
# supports C++11, but anyone pairing it with BoringSSL inherits this
# constraint. EXTRA_CXXFLAGS appends after the Makefile's -std=c++11
# and the later flag wins.
run: |
cd test
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -lpthread"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# macOS counterpart of the BoringSSL job. Same best-effort posture; the
# extra framework links cover the macOS Keychain integration that
# httplib.h auto-enables for any TLS backend on macOS.
macos-boringssl:
runs-on: macos-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_macos == 'true')
continue-on-error: true
name: macos (boringssl, best-effort)
env:
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(sysctl -n hw.ncpu)" --target install
- name: build and run tests (BoringSSL)
run: |
cd test
# CoreFoundation/Security frameworks satisfy the Keychain integration
# auto-enabled in httplib.h for macOS TLS builds.
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -framework CoreFoundation -framework Security"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# 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 +384,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 }})
@@ -130,7 +393,7 @@ jobs:
uses: actions/checkout@v4
- name: install Mbed TLS
if: matrix.tls_backend == 'mbedtls'
run: brew install mbedtls@3
run: brew install mbedtls
- name: install wolfSSL
if: matrix.tls_backend == 'wolfssl'
run: brew install wolfssl
@@ -154,6 +417,54 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
ios-parse-check:
runs-on: macos-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_macos == 'true')
name: ios header parse check (not officially supported)
steps:
- name: checkout
uses: actions/checkout@v4
- name: install OpenSSL headers
run: brew install openssl@3
- name: verify header parses on iOS target
run: |
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
OPENSSL_INC=$(brew --prefix openssl@3)/include
echo "Using iOS SDK: $IOS_SDK"
echo '#include "httplib.h"' | clang++ \
-isysroot "$IOS_SDK" \
-target arm64-apple-ios16.0 \
-std=c++11 \
-DCPPHTTPLIB_OPENSSL_SUPPORT \
-I"$OPENSSL_INC" \
-I. -Wall -Wextra \
-fsyntax-only -x c++ -
- name: verify CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is rejected on iOS
run: |
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
OPENSSL_INC=$(brew --prefix openssl@3)/include
out=$(echo '#include "httplib.h"' | clang++ \
-isysroot "$IOS_SDK" \
-target arm64-apple-ios16.0 \
-std=c++11 \
-DCPPHTTPLIB_OPENSSL_SUPPORT \
-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN \
-I"$OPENSSL_INC" \
-I. \
-fsyntax-only -x c++ - 2>&1 || true)
if echo "$out" | grep -q "only supported on macOS"; then
echo "OK: #error fired as expected"
else
echo "FAIL: expected #error did not fire"
echo "--- compiler output ---"
echo "$out"
exit 1
fi
windows:
runs-on: windows-latest
if: >
@@ -162,6 +473,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 +540,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 +548,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.
+70 -15
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.
@@ -67,12 +67,15 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
| Backend | Define | Libraries | Notes |
| :------ | :----- | :-------- | :---- |
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | [3.0 or later](https://www.openssl.org/policies/releasestrat.html) required |
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | 2.x and 3.x supported (auto-detected) |
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | 2.x, 3.x, and 4.x supported (auto-detected); 4.x renames `libmbedcrypto` to `libtfpsacrypto` |
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | 5.x supported; must build with `--enable-opensslall` |
> [!NOTE]
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
> [!NOTE]
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).
```c++
// Use either OpenSSL, Mbed TLS, or wolfSSL
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
@@ -447,6 +450,8 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
### Pre request handler
The pre-request handler runs after the route has been matched (so `req.matched_route` and `req.path_params` are available) but **before the request body is read**. This means you can reject a request — for example on a failed authentication or authorization check — without forcing the server to buffer a potentially large body.
```cpp
svr.set_pre_request_handler([](const auto& req, auto& res) {
if (req.matched_route == "/user/:user") {
@@ -461,6 +466,38 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
});
```
> [!NOTE]
> Because the body has not been read yet, `req.body` and form fields parsed from the body are not available in the pre-request handler. Inspect headers, the path, query parameters, or `req.matched_route` instead.
### Handler execution order
`set_start_handler` runs once when the server starts. For each request, handlers run in the following order:
```
Request received
│
├─ pre_routing_handler route not matched yet, body not read
│ └─ returns Handled → stop here
│
├─ file_request_handler (GET/HEAD, static file serving)
│
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
│
├─ route matching → req.matched_route is set
│
├─ pre_request_handler route matched, body NOT read yet
│ └─ returns Handled → stop here (route handler is skipped)
│
├─ route handler Get/Post/...; the request body is read first
│
└─ post_routing_handler after routing completes
On a thrown exception → exception_handler
On an error status (4xx/5xx) → error_handler
```
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
### Response user data
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
@@ -787,6 +824,15 @@ svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/12, /*max_threa
Default limit is 0 (unlimited). Once the limit is reached, the listener
will shutdown the client connection.
#### Idle timeout for dynamic threads
The idle timeout for dynamic threads can also be set at runtime via the
fourth parameter (in seconds):
```cpp
svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/8, /*max_threads=*/64, /*max_queued_requests=*/0, /*idle_timeout_sec=*/10); };
```
### Override the default thread pool with yours
You can supply your own thread pool implementation according to your need.
@@ -950,7 +996,7 @@ auto res = cli.Get("/hi", headers);
or
```c++
auto res = cli.Get("/hi", {{"Hello", "World!"}});
auto res = cli.Get("/hi", httplib::Headers{{"Hello", "World!"}});
```
or
@@ -1178,12 +1224,23 @@ cli.set_proxy_bearer_token_auth("pass");
> [!NOTE]
> OpenSSL is required for Digest Authentication.
#### Bypass the proxy for specific hosts (`NO_PROXY`)
```cpp
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
```
Each pattern is `*`, a hostname suffix, an IP literal, or a CIDR block.
Hostname matching is case-insensitive with a dot-boundary rule. See the
[NO_PROXY cookbook](https://yhirose.github.io/cpp-httplib/en/cookbook/c16-proxy)
for details and for reading the variable from the environment.
### Range
```cpp
httplib::Client cli("httpcan.org");
auto res = cli.Get("/range/32", {
auto res = cli.Get("/range/32", httplib::Headers{
httplib::make_range_header({{1, 10}}) // 'Range: bytes=1-10'
});
// res->status should be 206.
@@ -1307,13 +1364,13 @@ The default `Accept-Encoding` value contains all possible compression types. So,
```c++
res = cli.Get("/resource/foo");
res = cli.Get("/resource/foo", {{"Accept-Encoding", "br, gzip, deflate, zstd"}});
res = cli.Get("/resource/foo", httplib::Headers{{"Accept-Encoding", "br, gzip, deflate, zstd"}});
```
If we don't want a response without compression, we have to set `Accept-Encoding` to an empty string. This behavior is similar to curl.
```c++
res = cli.Get("/resource/foo", {{"Accept-Encoding", ""}});
res = cli.Get("/resource/foo", httplib::Headers{{"Accept-Encoding", ""}});
```
### Compress request body on client
@@ -1433,11 +1490,9 @@ See [README-sse.md](README-sse.md) for more details.
httplib::Server svr;
svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
httplib::ws::Message msg;
std::string msg;
while (ws.read(msg)) {
if (msg.is_text()) {
ws.send("Echo: " + msg.data);
}
ws.send("Echo: " + msg);
}
});
@@ -1462,7 +1517,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 +1623,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.
+3 -2
View File
@@ -7,11 +7,12 @@ CPPHTTPLIB_FLAGS = -DCPPHTTPLIB_THREAD_POOL_COUNT=16
BENCH = bombardier -c 10 -d 5s localhost:8080
MONITOR = ali http://localhost:8080
WAIT = while ! nc -z localhost 8080 >/dev/null 2>&1; do sleep 0.05; done
# cpp-httplib
bench: server
@echo "--------------------\n cpp-httplib latest\n--------------------\n"
@./server & export PID=$$!; $(BENCH); kill $${PID}
@./server & export PID=$$!; $(WAIT); $(BENCH); kill $${PID}
@echo ""
monitor: server
@@ -26,7 +27,7 @@ server : cpp-httplib/main.cpp ../httplib.h
# crow
bench-crow: server-crow
@echo "-------------\n Crow v1.3.1\n-------------\n"
@./server-crow & export PID=$$!; $(BENCH); kill $${PID}
@./server-crow & export PID=$$!; $(WAIT); $(BENCH); kill $${PID}
@echo ""
monitor-crow: server-crow
+1 -1
View File
@@ -61,7 +61,7 @@ if(@HTTPLIB_IS_USING_ZSTD@)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
set(httplib_fd_zstd_required_arg REQUIRED)
endif()
find_package(zstd QUIET)
find_package(zstd 1.5.6 CONFIG QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
if(PKG_CONFIG_FOUND)
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.42.0"
version = "0.51.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
+37 -2
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);
@@ -49,4 +49,39 @@ cli.set_bearer_token_auth("api-token"); // for the end server
`Proxy-Authorization` is sent to the proxy, `Authorization` to the end server.
> **Note:** cpp-httplib does not read `HTTP_PROXY` or `HTTPS_PROXY` environment variables automatically. If you want to honor them, read them in your application and pass the values to `set_proxy()`.
## Bypass the proxy for specific hosts
You often want internal endpoints to skip the proxy. Configure a bypass list with `set_no_proxy()`.
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
```
Each entry is one of:
- `*` — bypass the proxy for all hosts
- a hostname suffix (e.g. `example.com`) — matches `example.com` itself and any subdomain (`foo.example.com`). A leading dot is permitted but informational; both forms are equivalent.
- a single IP literal (e.g. `192.168.1.1`, `::1`)
- a CIDR block (e.g. `10.0.0.0/8`, `fe80::/10`)
Hostname matching is case-insensitive and uses a dot-boundary rule, so an entry of `example.com` does **not** match `evilexample.com`. IP comparisons are normalized through `inet_pton`, so `127.0.0.1` cannot be bypassed via alternate string forms (e.g. `127.000.000.001`). When an entry matches, the `Proxy-Authorization` header is suppressed as well.
Malformed entries are silently dropped. Port-specific entries such as `example.com:8080` are not supported (cpp-httplib's other host-keyed APIs are also keyed on hostname only).
## Read proxy settings from the environment
cpp-httplib doesn't touch `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` on its own — the config API is always explicit, the same way `set_ca_cert_path()` is. If you'd like that behavior, read the variables in your application and feed them to `set_proxy()` and `set_no_proxy()`.
```cpp
if (const char *v = std::getenv("no_proxy")) {
std::vector<std::string> patterns;
std::stringstream ss(v);
for (std::string item; std::getline(ss, item, ',');) {
if (!item.empty()) { patterns.push_back(item); }
}
cli.set_no_proxy(patterns);
}
```
If you also read `HTTP_PROXY` yourself, honor the lowercase `http_proxy` only. The uppercase form is poisoned in CGI/FastCGI environments by the `Proxy:` request header ([CVE-2016-5385 / "httpoxy"](https://httpoxy.org/)). `HTTPS_PROXY` and `NO_PROXY` are safe in either case because their names don't begin with `HTTP_`.
@@ -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,17 +4,19 @@ 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
| Hook | When it runs | Route info |
| --- | --- | --- |
| `set_pre_routing_handler` | Before routing | Not available |
| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` |
| Hook | When it runs | Route info | Request body |
| --- | --- | --- | --- |
| `set_pre_routing_handler` | Before routing | Not available | Not read yet |
| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` | Not read yet |
In a pre-request handler, `req.matched_route` holds the **pattern string** that matched. You can vary behavior based on the route definition itself.
Because the body has not been read when the pre-request handler runs, you can reject a request — for example on a failed auth check — without consuming a (potentially large) request body. Note that this also means `req.body` and form fields parsed from the body are not available here; inspect headers, the path, query parameters, or `req.matched_route` instead.
## Switch auth per route
```cpp
@@ -44,4 +46,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
+2
View File
@@ -79,6 +79,8 @@ cpp-httplib also supports Mbed TLS and wolfSSL in addition to OpenSSL. You can s
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` |
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` |
Mbed TLS 2.x, 3.x, and 4.x are all supported and auto-detected. Note that Mbed TLS 4.x renames `libmbedcrypto` to `libtfpsacrypto`, so link against that instead.
This tour assumes OpenSSL, but the API is the same regardless of which backend you choose.
## Next Step
@@ -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 Events(SSE)を扱うときはE04. SSEをクライアントで受信するも参考になります。
> ダウンロードの進捗を知りたい場合は[C11. 進捗コールバックを使う](../c11-progress-callback)と組み合わせましょう。
> Server-Sent Events(SSE)を扱うときは[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()`を使います。
## 基本の使い方
+37 -2
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);
@@ -49,4 +49,39 @@ cli.set_bearer_token_auth("api-token"); // エンドサーバー向け
プロキシには`Proxy-Authorization`、エンドサーバーには`Authorization`ヘッダーが送られます。
> **Note:** 環境変数の`HTTP_PROXY`や`HTTPS_PROXY`は自動的には読まれません。必要ならアプリケーション側で読み取って`set_proxy()`に渡してください。
## 特定のホストだけプロキシをバイパスする
社内エンドポイントなどはプロキシを経由させたくないことがあります。`set_no_proxy()`で除外リストを指定できます。
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
```
エントリは次のいずれかです。
- `*` — すべてのホストでバイパス
- ホスト名サフィックス(例: `example.com`)— `example.com`本体と任意のサブドメイン(`foo.example.com`)にマッチ。先頭にドットを付けても同じ意味です(`.example.com`)。
- 単一のIPリテラル(例: `192.168.1.1`、`::1`)
- CIDRブロック(例: `10.0.0.0/8`、`fe80::/10`)
ホスト名のマッチは大文字小文字を区別せず、ドット境界でしか一致しません。たとえば`example.com`というエントリは`evilexample.com`にはマッチしません。IPの比較は`inet_pton`で正規化されるので、`127.0.0.1`を`127.000.000.001`のような別表記でバイパスすることはできません。マッチした場合、`Proxy-Authorization`ヘッダーも自動的に外れます。
不正な書式のエントリは黙って捨てられます。`example.com:8080`のようなポート指定エントリはサポート外です(cpp-httplibの他のホストキーAPIもホスト名のみを扱う設計のため)。
## 環境変数からプロキシ設定を読み込む
cpp-httplib本体は`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`を読みません。`set_ca_cert_path()`と同じで、設定APIは常に明示的にしています。環境変数を反映させたい場合は、アプリ側で読んで`set_proxy()`や`set_no_proxy()`に渡してください。
```cpp
if (const char *v = std::getenv("no_proxy")) {
std::vector<std::string> patterns;
std::stringstream ss(v);
for (std::string item; std::getline(ss, item, ',');) {
if (!item.empty()) { patterns.push_back(item); }
}
cli.set_no_proxy(patterns);
}
```
`HTTP_PROXY`も自分で読むなら、小文字の`http_proxy`だけを採用してください。大文字の方はCGI/FastCGI環境で`Proxy:`リクエストヘッダーから汚染される可能性があります([CVE-2016-5385 / "httpoxy"](https://httpoxy.org/))。`HTTPS_PROXY`と`NO_PROXY`は名前が`HTTP_`で始まらないので、どちらの大文字小文字でも安全です。
@@ -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,17 +4,19 @@ 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との違い
| フック | 呼ばれるタイミング | ルート情報 |
| --- | --- | --- |
| `set_pre_routing_handler` | ルーティングの前 | 取得できない |
| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 |
| フック | 呼ばれるタイミング | ルート情報 | リクエストボディ |
| --- | --- | --- | --- |
| `set_pre_routing_handler` | ルーティングの前 | 取得できない | まだ読まれていない |
| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 | まだ読まれていない |
Pre-requestハンドラなら、`req.matched_route`に「マッチしたパターン文字列」が入っているので、ルートに応じて処理を変えられます。
Pre-requestハンドラが呼ばれる時点ではボディがまだ読まれていないので、認証に失敗したリクエストなどを、(巨大かもしれない)ボディを読み込む前に拒否できます。その代わり、`req.body`やボディから解析されるフォームフィールドはこの時点では参照できません。ヘッダ・パス・クエリパラメータ・`req.matched_route`を使って判断してください。
## ルートごとに認証を切り替える
```cpp
@@ -44,4 +46,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)を参照してください。
## SNI(Server 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)を参照してください。
## サンプル: 画像を送る
+2
View File
@@ -79,6 +79,8 @@ cpp-httplibはOpenSSL以外にも、Mbed TLSとwolfSSLに対応しています
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` |
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` |
Mbed TLSは2.x、3.x、4.xいずれも対応していて、自動判定されます。4.xでは`libmbedcrypto`が`libtfpsacrypto`という名前に変わっているので、リンクするライブラリはそちらに読み替えてください。
このTourではOpenSSLを前提に進めますが、APIはどのバックエンドでも共通です。
## 次のステップ
+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
}
+1576 -1074
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -107,7 +107,12 @@ if host_machine.system() == 'windows'
elif host_machine.system() == 'darwin'
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation'], required: async_ns_opt)
else
async_ns_dep = cxx.find_library('anl', required: async_ns_opt)
has_gai_a = cxx.has_function('getaddrinfo_a', args: '-D_GNU_SOURCE')
if has_gai_a
async_ns_dep = declare_dependency()
else
async_ns_dep = cxx.find_library('anl', required: async_ns_opt)
endif
endif
if async_ns_dep.found()
+50 -16
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,status,conclusion,headSha)
NUM_RUNS=$(echo "$RUNS" | jq 'length')
@@ -65,8 +75,17 @@ fi
echo " Found $NUM_RUNS workflow run(s):"
FAILED=0
RUNNING=0
ABIDIFF_PASSED=0
while IFS=$'\t' read -r name conclusion; do
while IFS=$'\t' read -r name status conclusion; do
# A run that hasn't completed yet has an empty conclusion; don't treat it
# as a failure — the release should wait until CI finishes.
if [ "$status" != "completed" ]; then
echo " [ .. ] $name (still running)"
RUNNING=1
continue
fi
if [[ "$name" == *abidiff* ]] || [[ "$name" == *abi* && "$name" != *stability* ]]; then
if [ "$conclusion" = "success" ]; then
echo " [ OK ] $name"
@@ -84,7 +103,13 @@ while IFS=$'\t' read -r name conclusion; do
echo " [FAIL] $name ($conclusion)"
FAILED=1
fi
done < <(echo "$RUNS" | jq -r '.[] | [.name, .conclusion] | @tsv')
done < <(echo "$RUNS" | jq -r '.[] | [.name, .status, .conclusion] | @tsv')
if [ "$RUNNING" -eq 1 ]; then
echo ""
echo "Error: Some CI checks are still running. Wait for them to complete before releasing."
exit 1
fi
if [ "$FAILED" -eq 1 ]; then
echo ""
@@ -95,7 +120,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 +134,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##*.}")
+33 -3
View File
@@ -103,19 +103,49 @@ if(HTTPLIB_IS_USING_OPENSSL)
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# Encrypted client key: make an unencrypted key + cert first, then wrap the
# same key two ways. Mbed TLS 4.x dropped DES/PBES1, while Ubuntu's Mbed TLS
# 2.28 has no PBES2-AES, so ship both and let test.cc pick by version.
execute_process(
COMMAND ${OPENSSL_COMMAND} genrsa -aes256 -passout pass:test012! 2048
OUTPUT_FILE client_encrypted.key.pem
COMMAND ${OPENSSL_COMMAND} genrsa -out client_encrypted.tmp.key.pem 2048
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} req -new -batch -config ${CMAKE_CURRENT_LIST_DIR}/test.conf -key client_encrypted.key.pem -passin pass:test012!
COMMAND ${OPENSSL_COMMAND} req -new -batch -config ${CMAKE_CURRENT_LIST_DIR}/test.conf -key client_encrypted.tmp.key.pem
COMMAND ${OPENSSL_COMMAND} x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial
OUTPUT_FILE client_encrypted.cert.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} pkcs8 -topk8 -v2 aes-256-cbc -in client_encrypted.tmp.key.pem -passout pass:test012! -out client_encrypted.key.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} pkcs8 -topk8 -v1 PBE-SHA1-3DES -in client_encrypted.tmp.key.pem -passout pass:test012! -out client_encrypted_pbes1.key.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/client_encrypted.tmp.key.pem)
# Certificates for IP-host hostname verification regression tests.
# cert_ip_cn.pem: CN is an IPv4 literal with NO subjectAltName. An IP host
# must NOT be authenticated via the CN, so verifying it
# against this cert must fail.
execute_process(
COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=127.0.0.1 -out cert_ip_cn.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# cert_ipv6.pem: CN is an IPv6 literal plus an IPv6 iPAddress SAN for a
# different address. The SAN address must match; the CN
# address must be ignored.
execute_process(
COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=::1 -addext subjectAltName=IP:2001:db8::1 -out cert_ipv6.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
endif()
add_subdirectory(fuzzing)
+72 -19
View File
@@ -8,8 +8,11 @@ ifneq ($(OS), Windows_NT)
OPENSSL_DIR = $(PREFIX)/opt/openssl@3
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
OPENSSL_SUPPORT += -framework CoreFoundation -framework Security
MBEDTLS_DIR ?= $(shell brew --prefix mbedtls@3)
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib -lmbedtls -lmbedx509 -lmbedcrypto
# Homebrew's default mbedtls is 4.x; override MBEDTLS_DIR for other versions.
MBEDTLS_DIR ?= $(shell brew --prefix mbedtls)
# Mbed TLS 4.x renamed libmbedcrypto to libtfpsacrypto; pick whichever exists.
MBEDTLS_CRYPTO_LIB ?= $(shell test -f "$(MBEDTLS_DIR)/lib/libtfpsacrypto.dylib" -o -f "$(MBEDTLS_DIR)/lib/libtfpsacrypto.a" && echo tfpsacrypto || echo mbedcrypto)
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib -lmbedtls -lmbedx509 -l$(MBEDTLS_CRYPTO_LIB)
MBEDTLS_SUPPORT += -framework CoreFoundation -framework Security
WOLFSSL_DIR ?= $(shell brew --prefix wolfssl)
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -I$(WOLFSSL_DIR)/include -I$(WOLFSSL_DIR)/include/wolfssl -L$(WOLFSSL_DIR)/lib -lwolfssl
@@ -18,6 +21,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 +66,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 +205,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 +273,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 +317,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)
+8 -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 multipart_parser_fuzzer
# Runs all the tests and also fuzz tests against seed corpus.
all : $(FUZZERS)
@@ -25,11 +25,18 @@ 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
url_parser_fuzzer : url_parser_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
multipart_parser_fuzzer : multipart_parser_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
clean:
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
+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;
}

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