There are 19 open security issues in trixie.
You can find information about how to handle these issues in the security team's documentation.
There are 7 open security issues in sid.
There are 7 open security issues in forky.
There are 22 open security issues in bookworm.
commit 7c05b45f50531eb30f8165d9caaf77ebbbbab125
Author: Mario Limonciello <superm1@gmail.com>
Date: Wed Nov 4 08:53:58 2026 -0600
releasing package cpp-httplib version 0.64.0+ds-0.1
commit 4bf87ab72dbc1883f0eddad4e992bb4f3985dae7
Author: Mario Limonciello <superm1@gmail.com>
Date: Wed Nov 4 08:53:39 2026 -0600
d/tests/control: pkg-config->pkgconf
commit b3de07710b18f02018a3ef683640050f44644409
Author: Mario Limonciello <superm1@gmail.com>
Date: Mon Nov 2 11:26:57 2026 -0600
Update changelog
commit 85e15830807cdfc9622b79aad07135a7e307aa26
Author: Mario Limonciello <superm1@gmail.com>
Date: Mon Nov 2 11:25:47 2026 -0600
d/control pkg->config -> pkfconf
commit f8fc9eb0c3274bf06c0a5ec453abe11fd0fba11e
Merge: afa2aec fbd757a
Author: Mario Limonciello <superm1@gmail.com>
Date: Mon Nov 2 10:35:08 2026 -0600
Merge branch 'upstream/latest' into update-0.64.0
commit fbd757a540de26f95cac13d56fbc7cf9fda49297
Author: Mario Limonciello <superm1@gmail.com>
Date: Mon Nov 2 10:34:50 2026 -0600
Remove benchmark/ to match debian/copyright
commit 5d3d2470392934be4c60e94ba474e21e56e55353
Merge: 3caa443 24e58ce
Author: Mario Limonciello <superm1@gmail.com>
Date: Mon Nov 2 10:18:38 2026 -0600
Merge tag 'v0.64.0' into upstream/latest
commit 3caa44361c03b0e5ab5be54e8a6387d6788a964c
Merge: 9751cff d66d9a9
Author: Andrea Pappacoda <tachi@debian.org>
Date: Fri Jul 24 17:20:13 2026 -0300
Merge tag 'v0.51.0' into upstream/latest
commit d66d9a95997d51a8ba9822a611d1267757741535
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jul 23 22:52:55 2026 -0400
Release v0.51.0
commit 2fa0417754347222065c423d7c4928ffa1577dfe
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jul 23 22:41:43 2026 -0400
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.
commit 82b1492c3dd380617cabf2c69592e6882c212798
Merge: cc1be5e f7c8455
Author: yhirose <yhirose@users.noreply.github.com>
Date: Thu Jul 23 20:04:02 2026 -0400
Merge pull request #2507 from yhirose/flaky-ci-diagnostics
Log res.error() on all ServerTest ASSERT_TRUE(res) assertions
commit f7c8455a624eb4bb1810f915443ce53a93f2cee6
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jul 23 18:06:22 2026 -0400
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.
commit cc1be5ebb5fb7f6ed1143854b77c5a42ab7ac04c
Merge: 613a41b cda2bb5
Author: yhirose <yhirose@users.noreply.github.com>
Date: Thu Jul 23 17:50:59 2026 -0400
Merge pull request #2506 from yhirose/refactor/consolidate-field-validation
Consolidate header field-pair validation into fields::is_field_valid
commit cda2bb5342650801e655abd03cf6b1c4c473b493
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jul 23 17:34:57 2026 -0400
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.
commit 613a41b49ddbcb14a4da04ce163ff1f590858858
Merge: c64bf21 695961f
Author: yhirose <yhirose@users.noreply.github.com>
Date: Thu Jul 23 17:32:22 2026 -0400
Merge pull request #2505 from metsw24-max/write-headers-crlf-guard
skip invalid fields in write_headers to prevent response splitting
commit 695961f8ae7eedec245e4a214eec61a841aebc14
Author: Sayed Kaif <metsw24@gmail.com>
Date: Thu Jul 23 21:42:37 2026 +0530
skip invalid fields in write_headers to prevent response splitting
commit c64bf21a5ea10c196187cda52ccd776725968e3c
Merge: bf8baf0 8bbfc90
Author: yhirose <yhirose@users.noreply.github.com>
Date: Wed Jul 22 12:01:20 2026 -0400
Merge pull request #2504 from emreay-/skip-drain-on-closing-connection
Skip request body drain when connection will close
commit 8bbfc90380ca402aa135576d8f8697c40a8193af
Author: Emre Ay <emre@emreay.com>
Date: Wed Jul 22 10:53:34 2026 +0300
Apply clang-format to test.cc and httplib.h
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
commit bf8baf07391cec982b99028b0bc2182fc5ad5083
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Tue Jul 21 19:47:15 2026 -0400
clang-format
commit fd0c18b1b5bce8003c46130c5ce17ac44021038a
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Tue Jul 21 18:33:59 2026 -0400
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_.
commit bbd56a7e2cdc3306822bac1417f6a2c525aa5758
Merge: 54b3c1d 877a52f
Author: yhirose <yhirose@users.noreply.github.com>
Date: Tue Jul 21 19:15:50 2026 -0400
Merge pull request #2497 from superm1/superm1/SWSPLAT-23636
Limit the number of header lines per multipart form-data part
commit 54b3c1d072a5a6adeadcc79cfbd1be7afbbca71f
Author: yhirose <yhirose@users.noreply.github.com>
Date: Tue Jul 21 18:45:48 2026 -0400
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.
commit 6c4cbd49a2e4ee6e76f2ca8c432fd5897de0996f
Author: metsw24-max <metsw24@gmail.com>
Date: Wed Jul 22 03:42:40 2026 +0530
scan X-Forwarded-For right-to-left in get_client_ip (#2503)
commit f3e9a4d88742a5c332918f1be5203942aab70c50
Author: Emre Ay <emre@emreay.com>
Date: Mon Jul 20 20:36:06 2026 +0300
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.
commit 877a52f6b34b1a9d426fc5344f7dbb119548fc2b
Author: Mario Limonciello <mario.limonciello@amd.com>
Date: Wed Jul 15 10:57:57 2026 -0500
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.
commit ef2beaea34cca0c4e31b9f9a8463e36ca2938d02
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 18 20:08:33 2026 -0400
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.
commit 3adc525cec4e08d917a61a9fa54305c178d039b4
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 18 18:09:55 2026 -0400
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.
commit a415362dfff5b140bcb9a7eb381de4afd888a4eb
Author: Sayed Kaif <metsw24@gmail.com>
Date: Mon Jul 13 12:08:34 2026 +0530
update OpenStreamMalformedContentLength.OutOfRange for stricter parsing
commit 982235c0a9975bef121a1d7866b546e165e243e4
Author: Sayed Kaif <metsw24@gmail.com>
Date: Sun Jul 12 17:10:51 2026 +0530
flag out-of-range Content-Length in get_header_value_u64
commit 255c075b82cf9f221e6e607e8e6789e6e2bf4c2a
Author: yhirose <yhirose@users.noreply.github.com>
Date: Sat Jul 18 18:07:44 2026 -0400
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>
commit 75938f08c743eb985152c88c485b91d8fd42f948
Author: yhirose <yhirose@users.noreply.github.com>
Date: Sat Jul 18 18:07:29 2026 -0400
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.
commit 0c1cc8c9866bb567ff11c1cd0d09779e8c5f8585
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sun Jul 12 10:45:36 2026 -0400
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.
commit 62d899feac3cf9215a55f2b43da250fdd98d2156
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 21:37:36 2026 -0400
Release v0.50.1
commit 2f986fd5e56e7c5f686d965174516360930f371d
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 21:20:36 2026 -0400
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).
commit ed97a6edbabb096fc0a7fb2c63599475b94b81a8
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 16:01:02 2026 -0400
Release v0.50.0
commit 06b8b9158997338967600f2cf3bd210e8cc3333d
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 15:52:54 2026 -0400
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.
commit 873d7019723cb91efdd8b89687222ec3029655c0
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 15:27:42 2026 -0400
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.
commit b40937cea80d674fa67e50c3690b6b5056f05d47
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 14:12:25 2026 -0400
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).
commit 568d434e72fc51729d0ad33abffb181e5f7a453d
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jul 11 13:54:02 2026 -0400
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().
commit 0fa4912891f7229af34417ce859b02393f57ff5d
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 8 22:45:29 2026 -0400
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`.
commit 32abac3de5a1a5c57e0b06b4a8c261263c9e6499
Author: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sun Jul 5 18:43:11 2026 -0400
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>
commit 2132205e1a69c9fce8096f085b1b8d72efc759fa
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 23:56:17 2026 -0400
Release v0.49.0
commit a4d7066c2ce21e0108d21fcede5e8ea8e4eb26a3
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 23:46:05 2026 -0400
Log res.error() in GetWithRange test assertions to diagnose flaky CI failures
commit bd455f8b9168733cc9bfc204058abaced365f235
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 23:00:48 2026 -0400
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.
commit 9a5321aadb71f043de72ba1da50f6be0df072c8d
Author: Yanjun Yang(Pluto) <yangyj.ee@gmail.com>
Date: Thu Jul 2 10:21:44 2026 +0800
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>
commit 32ff75e3556d792b8ddf0982fb6775c8e80a1e0e
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 22:08:01 2026 -0400
Make ThreadPool idle timeout configurable at runtime (Fix #2481)
commit 45da614dddcceacbdc491ee6c73a935b85702670
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 21:38:11 2026 -0400
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.
commit f5c8c982df7989ae25f47129683962ea862d1ddd
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 21:22:15 2026 -0400
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.
commit 9e8b960e4457215354a98baa09173b1832839fb3
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 21:15:39 2026 -0400
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.
commit 8149bb38fc06d54537656a32ab0906879cbc431a
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 21:15:15 2026 -0400
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.
commit 3fe32b63b42d7b273cd4d76d69df0097560375d6
Author: Saber Haj Rabiee <sabify@users.noreply.github.com>
Date: Wed Jul 1 18:18:05 2026 -0700
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.
commit 0ae93881b44bf94437843403bfcdc4f50445992e
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jul 1 20:12:44 2026 -0400
Clarify comment on base64_encode accumulator signedness
commit a7b886b9cb982a415906490f8fdd0e7292dac878
Author: metsw24-max <metsw24@gmail.com>
Date: Thu Jul 2 05:42:00 2026 +0530
Use an unsigned accumulator in base64_encode (#2477)
* use an unsigned accumulator in base64_encode
* Forward-declare detail::base64_encode for split builds
commit 9d159bb412393eb7bc821fb193999cc44ef32636
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jun 18 20:13:57 2026 -0400
Release v0.48.0
commit 9ac64e90db602cc4dad4501e762981ffa909e64f
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jun 18 19:50:46 2026 -0400
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.
commit ba390f23994cad46ebcd1497414d22a2ebf736de
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jun 18 12:37:16 2026 -0400
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
commit 7307c41aa87adc194167b9badd2c2e6cfa12063b
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jun 17 23:21:36 2026 -0400
Better `from_chars` implementation (Fix #2475)
commit df0b7d243b06860d8966688cda61dfbeeb55b755
Author: Arthur Chan <arthur.chan@adalogics.com>
Date: Sun Jun 14 03:39:45 2026 +0100
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>
commit 28d95937b5a710436f367ab8e0e351fc2f3bb416
Author: metsw24-max <metsw24@gmail.com>
Date: Fri Jun 12 23:24:04 2026 +0530
use strict hex parsing in decode_query_component (#2472)
commit 75329322764a67cf27fc3b5ec0350b96287baaa6
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Fri Jun 12 13:20:15 2026 -0400
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.
commit 8829fac98e615f52a880bf758573806b39af9d9d
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jun 10 23:18:15 2026 -0400
Use full git history in docs workflow for sitemap lastmod
commit fe332fa06bac76a1c6d402c08f414052999347da
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jun 10 00:24:05 2026 -0400
Release v0.47.0
commit fa981cedae004ea9d946f1392b9dec22fac6fee6
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jun 10 00:07:27 2026 -0400
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).
commit 39ec7d05082cd0c50f7a2fdb945352bf75de66ec
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Tue Jun 9 21:50:37 2026 -0400
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
commit e7e7bf7b446252f5d9eaeff55c5a3da9da0ed3e2
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Tue Jun 9 20:46:26 2026 -0400
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.
commit 78ff94e6047938b5dbe4b4a321b9dd7c848b19ac
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Tue Jun 9 20:54:24 2026 -0400
Wait for server startup before running benchmark
commit 018ce8e4ca194a42727f9c39cdc0fd8023673eab
Author: metsw24-max <metsw24@gmail.com>
Date: Wed Jun 10 05:04:43 2026 +0530
cast to unsigned char before ctype calls in is_hex and is_token_char (#2469)
commit 77bdf7921a21d18af7c4d82e6ab3e58e93e363ec
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Tue Jun 9 13:49:56 2026 -0400
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).
commit 79d83feb1829dc6e44d636019e2883662c1b89ff
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sun Jun 7 17:08:11 2026 -0400
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.
commit fe56a07da5bc993cdf0e05704649fc6fd41f57c0
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat Jun 6 13:38:36 2026 -0400
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.
commit 907257f51de5213b6f1febe983e7d39ad6e1118c
Author: Kim, Hyuk <hyukya@gmail.com>
Date: Sat Jun 6 05:34:20 2026 +0900
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
commit 0c2f535b74f4aa32233a381805a15eb652a638b0
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu Jun 4 21:20:37 2026 -0400
Fix #2467
commit c7ba963a17ee4139f3d8d8a31c727da494d2ac65
Author: Florian Fischer <60611027+florianfischerx@users.noreply.github.com>
Date: Fri Jun 5 02:15:21 2026 +0200
Ignore ranges for unknown-length streams (#2465)
commit 4465e81b9f7e08e2c88cc316db8dd6ddb6dafab6
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed Jun 3 22:24:52 2026 -0400
Fix #2464
commit 44215e23e92c473a3553d24ae634aed6eefc7dd0
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Mon Jun 1 12:24:27 2026 -0400
Release v0.46.1
commit 91219d450816b51ac1cdbcced6a7888b4a72ac8d
Author: yhirose <yhirose@users.noreply.github.com>
Date: Fri May 29 06:19:40 2026 -0400
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`.
commit c86c192f3e86ab2ccb5062c070274102a64780c3
Author: NsPro04 <64797367+NsPro04@users.noreply.github.com>
Date: Fri May 29 04:19:33 2026 +0600
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]"
commit 008e107d0fcddee7cb96dc5ad3c3189fd090e40a
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Mon May 25 00:30:27 2026 -0400
Release v0.46.0
commit d278f965ccd05fe1ce908edb1a224a6730343e4e
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Mon May 25 00:21:57 2026 -0400
Fix #2457
commit 4c4b62dd7e7ab59aec12c2c098f2084bdbc2d982
Author: yhirose <yhirose@users.noreply.github.com>
Date: Sun May 24 23:50:48 2026 -0400
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.
commit b1792ef29ca66f8dcb44b1678e597676a33a3f1b
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sun May 24 20:58:48 2026 -0400
Release v0.45.1
commit 0f3d063f0a6580307e9a9ac2a7bc8e8e167a86fe
Author: yhirose <yhirose@users.noreply.github.com>
Date: Sun May 24 02:48:46 2026 -0400
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.
commit 0d7d6374666956c9f5e2767573fb7be0d4477d16
Author: sakurai-ryuhei <50360667+sakurai-ryuhei@users.noreply.github.com>
Date: Sun May 24 00:59:58 2026 +0900
Fix zstd detection in installed httplibConfig.cmake (#2453)
commit 1ff0c8588d7a2c1baa251e8d615e86301526d8ec
Author: yhirose <yhirose@users.noreply.github.com>
Date: Sat May 23 08:39:45 2026 -0400
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.
commit b1cc8095a8feca1b99e296ef78eddd312e80d2c6
Author: NsPro04 <64797367+NsPro04@users.noreply.github.com>
Date: Sat May 16 19:50:08 2026 +0600
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
commit 28f8264d134a576422cd0f99f19719c2749d9e47
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Fri May 15 09:22:11 2026 +0900
Release v0.45.0
commit 91271c062d3a357d7e3ef5152db5495b7d6ba95a
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Fri May 15 06:57:51 2026 +0900
Fix keep-alive corruption on requests without framed body (#2450)
commit d755c43d586b6ca49a62a3f4ad8ba57fbff90d0d
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Fri May 15 06:56:16 2026 +0900
Extract has_framed_body and is_connection_persistent helpers
commit 5c9285776ee1803d8dc5f3ced5e3179454bd6bbd
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Thu May 14 23:19:36 2026 +0900
Fix crash on empty X-Forwarded-For with trusted proxies configured
commit 811dd0b6f2382f50f801f3cfdb5ba349d08bb575
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sun May 10 21:46:24 2026 +0900
Release v0.44.0
commit e8e652824b93a6f2e8a9dcd2f9967bea4d9302c3
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sun May 10 13:24:57 2026 +0900
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.
commit fbb031ed8504a0e48d9ca63caecc0614c296f755
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sun May 10 12:40:33 2026 +0900
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.
commit 7d5082cc0e6e9b8e5e6c3d7daf18e7b7331569d6
Author: yhirose <yhirose@users.noreply.github.com>
Date: Sat May 9 21:13:40 2026 -0400
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.
commit 600d220c84853f52011ae4b95fc8518742bcdfac
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat May 9 21:29:23 2026 +0900
Release v0.43.4
commit 87d62db46b37a660f64bc5663162e993752dd14a
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Sat May 9 16:52:32 2026 +0900
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).
commit a1fdc07f34ef4788b24c7be3c3fb41a402409662
Author: yhirose <yhirose@users.noreply.github.com>
Date: Wed May 6 08:36:38 2026 -0400
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.
commit eb49a304b6ecbb200e94e720446ae540067ea9a4
Author: yhirose <yhirose@users.noreply.github.com>
Date: Wed May 6 08:25:56 2026 -0400
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.
commit a9bfe5914b2c00415a818d62a43b462d93e08a64
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Wed May 6 18:44:14 2026 +0900
Fix #2441
commit ec5ce17929c56c8bb7f0307ce2af496bd017a3c9
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Mon May 4 16:19:49 2026 +0900
Release v0.43.3
commit f6524c08020aa1e8732d153ca57c64931d382b62
Author: yhirose <yuji.hirose.bug@gmail.com>
Date: Fri May 1 22:20:41 2026 +0900
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.