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.
* 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
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`.
* 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.
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.
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.
* 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.
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).
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.
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.
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.
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.
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.
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.
* 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>
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>
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>
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>
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>
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>
* 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>
* 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.
- 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.
The server listens on AF_INET6 only (::1), so the test fails:
[ RUN ] WebSocketIntegrationTest.SocketSettings
test/test.cc:17160: Failure
Value of: client.connect()
Actual: false
Expected: true
Fixes#2419.
Co-authored-by: Jiri Slaby <jslaby@suse.cz>