Compare commits

...

87 Commits

Author SHA1 Message Date
yhirose
fe332fa06b Release v0.47.0 2026-06-10 00:24:05 -04:00
yhirose
fa981cedae Fix TLS chain verification bypass for IP hosts on Mbed TLS and wolfSSL
For connections to IP-literal hosts with server certificate
verification enabled, the Mbed TLS and wolfSSL backends downgraded the
verification mode before the handshake because no hostname could be
bound for in-handshake checks:

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

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

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

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

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

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

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

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

* add WebSocketTest unit test cases
* SpecifyServerIPAddress_AnotherHostname
* SpecifyServerIPAddress_RealHostname

* Change wrong_ip from 0.0.0.0 to 192.0.2.1

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

* Fix style check

* set short timeout in WebSocketTest.SpecifyServerIPAddress_RealHostname

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

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

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

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

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

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

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

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

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

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

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

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

* Add NO_PROXY parsing and matching helpers in detail namespace

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

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

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

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

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

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

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

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

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

* Add Client::set_proxy_from_env with httpoxy mitigation

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

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

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

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

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

All 608 unit tests pass.

* Add NO_PROXY behavior tests

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

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

Coverage:

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

  Wildcard
    - "*" bypasses everything

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

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

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

  Backward compat
    - default behavior unchanged when set_no_proxy is never called

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

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

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

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

* Document set_no_proxy and set_proxy_from_env in README

Adds two subsections under "Proxy server support":

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

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

Documentation only. Closes the doc gap from #2446.

* Document NO_PROXY and set_proxy_from_env in cookbook c16-proxy

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

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

Adds two sections:

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

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

* Simplify NO_PROXY implementation per review

Apply seven post-implementation cleanups:

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

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

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

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

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

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

  - Drop test comments that paraphrased their own test name.

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

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

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

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

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

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

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

Net effect:

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

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

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

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

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

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

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

* Add NO_PROXY tests covering edge cases found during PR review

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

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

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

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

635 unit tests pass.

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

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

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

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

* Drop set_proxy_from_env per #2446 discussion

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

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

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

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

* Skip digest 407 retry when target is bypassed by NO_PROXY

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

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

Regression test BypassedTargetReturning407DoesNotLeakProxyDigest
Credentials reproduces the leak without this gate.

* Make set_no_proxy safe across redirects and keep-alive

Two correctness bugs that the dynamic NO_PROXY API exposed:

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

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

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

* Tighten NO_PROXY entry parsing

Three small parser fixes surfaced during code review:

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

New regression tests: BracketedIPv6EntryAccepted,
BracketedIPv6CidrEntryAccepted, TrailingSlashCidrIsRejected.

* Refactor: introduce disconnect() and remove invalidate_keep_alive_socket

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

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

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

Net -47 lines in httplib.h.

* Fix MultiHopRedirect test on Windows; trim NoProxyTest comments

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

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

* Consolidate NoProxyTest server boilerplate; drop hardcoded sentinel ports

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

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

Verified pass under 4-shard parallel run.

* Trim README NO_PROXY section to match surrounding granularity

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

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

* Restrict Keychain cert loading to macOS

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

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

Addresses the iOS build break reported in #2454.

* Add iOS header parse check to CI

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

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

* Adding the missing "noexcept" to the declaration
2026-05-16 09:50:08 -04:00
yhirose
28f8264d13 Release v0.45.0 2026-05-15 09:22:11 +09:00
yhirose
91271c062d Fix keep-alive corruption on requests without framed body (#2450) 2026-05-15 06:57:51 +09:00
yhirose
d755c43d58 Extract has_framed_body and is_connection_persistent helpers 2026-05-15 06:56:16 +09:00
yhirose
5c9285776e Fix crash on empty X-Forwarded-For with trusted proxies configured 2026-05-14 23:19:36 +09:00
yhirose
811dd0b6f2 Release v0.44.0 2026-05-10 21:46:24 +09:00
yhirose
e8e652824b Add --minor flag to release.sh for forced minor bumps
Allows forcing a minor version bump even when abidiff passes,
for behavioral breaking changes that don't break ABI.
2026-05-10 21:28:38 +09:00
yhirose
fbb031ed85 Stop percent-decoding HTTP request header values
parse_header() applied decode_path_component() to every header value
except Location and Referer, after is_field_value() validation. Wire
sequences like %0D%0A passed the check and expanded into literal CR/LF
inside stored values, enabling response splitting, log injection, and
proxy smuggling. %3D/%2C/%3B also flipped Cookie and X-Forwarded-For
boundaries against WAFs inspecting the wire form.

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

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

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

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

Fix #2444

* Strip ASAN from test_thread_pool to coexist with pthread_create override

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fix split build for #2431 reproducer tests

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

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

* Cap issue-2431 repro job at 5 minutes

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

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

* Enable ASAN detect_stack_use_after_return for #2431 repro

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

* Revert ASAN detect_stack_use_after_return for #2431 repro

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

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

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

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

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

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

---------

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:50:38 -04:00
yhirose
09d00c099c Update README 2026-04-11 22:24:04 -04:00
yhirose
6bdd657713 Enhance WebSocket support with unresponsive-peer detection and documentation updates
- Added `set_websocket_max_missed_pongs` method to configure unresponsive-peer detection.
- Updated README and documentation to clarify WebSocket limitations and features.
- Introduced tests for detecting non-responsive peers and ensuring responsive peers do not trigger timeouts.
2026-04-11 22:17:38 -04:00
yhirose
b4eec3ee77 Removed deprecated APIs (#2423) 2026-04-11 20:54:06 -04:00
yhirose
c0248ff7fc Add links to other topics in Cookbook documents 2026-04-11 20:40:08 -04:00
yhirose
203e1bf2ac Code cleanup 2026-04-11 20:40:08 -04:00
yhirose
ff04679538 Release v0.42.0 2026-04-11 18:53:36 -04:00
yhirose
d97749a315 Update README 2026-04-11 17:15:37 -04:00
yhirose
994d76ab39 Fix #2422 2026-04-11 15:38:35 -04:00
yhirose
529dafdee3 Add Cookbook other topics (draft) 2026-04-10 19:02:44 -04:00
yhirose
361b753f19 Add Cookbook S01-S22 (draft) 2026-04-10 18:47:42 -04:00
yhirose
61e533ddc5 Add Cookbook C01-C19 (draft) 2026-04-10 18:16:02 -04:00
yhirose
783de4ec4e Update README 2026-04-08 18:35:56 -04:00
yhirose
7a7f9b30e7 Update README 2026-04-08 18:22:25 -04:00
yhirose
834a444435 Fixed warnings 2026-04-08 18:10:34 -04:00
yhirose
4f589c1ffb Fix #2421 2026-04-08 18:09:22 -04:00
Jiri Slaby
fc885cc62d test: WebSocketIntegrationTest.SocketSettings: do not set AF_INET (#2420)
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>
2026-04-08 07:48:13 -04:00
yhirose
ca82c93772 Refactor SSLVerifierResponse to enum class and add get_param_values method to Request 2026-04-04 00:02:26 -04:00
134 changed files with 10696 additions and 993 deletions

View File

@@ -21,7 +21,8 @@ jobs:
- name: Build (Win32)
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set VSDIR=%%i
call "%VSDIR%\VC\Auxiliary\Build\vcvarsall.bat" x86 || exit /b 1
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
test-arm32:

View File

@@ -25,7 +25,9 @@ concurrency:
cancel-in-progress: true
env:
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '*' }}
# Exclude *_Online tests by default — they hit external services and flake on
# CI runners. Run with workflow_dispatch + a custom filter to include them.
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '-*_Online' }}
jobs:
style-check:
@@ -75,6 +77,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
strategy:
fail-fast: false
matrix:
tls_backend: [openssl, mbedtls, wolfssl]
name: ubuntu (${{ matrix.tls_backend }})
@@ -101,7 +104,10 @@ jobs:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
- name: build and run tests (Mbed TLS)
if: matrix.tls_backend == 'mbedtls'
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
# Run mbedTLS shards with reduced parallelism — under ASAN+mbedTLS the
# default 4 shards overload CI runners enough that timing-sensitive
# ServerTest cases flake on first-request keep-alive reuse.
run: cd test && make test_split_mbedtls && SHARDS=2 make test_mbedtls_parallel
- name: build and run tests (wolfSSL)
if: matrix.tls_backend == 'wolfssl'
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
@@ -114,6 +120,241 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
# BoringSSL is Google's fork of OpenSSL. It has no API stability guarantee
# and is not packaged by distros, so we build it from source. cpp-httplib
# treats it as an OpenSSL backend variant via the OPENSSL_IS_BORINGSSL
# macro (see httplib.h). This job is best-effort: continue-on-error keeps
# upstream API drift from blocking PRs while still surfacing breakage.
ubuntu-boringssl:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
continue-on-error: true
name: ubuntu (boringssl, best-effort)
env:
# Tracking HEAD keeps us honest about upstream churn. If breakage
# becomes routine, replace HEAD with a 40-char commit SHA; the
# resolve step uses the SHA directly when it matches that shape.
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(nproc)" --target install
- name: build and run tests (BoringSSL)
# Override OPENSSL_SUPPORT to point the existing OpenSSL Makefile path
# at BoringSSL's prefix. BoringSSL defines OPENSSL_IS_BORINGSSL in
# <openssl/base.h>, which httplib.h and test.cc use to switch on API
# differences (e.g. SAN-only hostname verification, no CN fallback).
#
# BoringSSL's public headers (<openssl/stack.h>) use std::enable_if_t,
# so consumers must compile with C++14 or later. cpp-httplib itself
# supports C++11, but anyone pairing it with BoringSSL inherits this
# constraint. EXTRA_CXXFLAGS appends after the Makefile's -std=c++11
# and the later flag wins.
run: |
cd test
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -lpthread"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# macOS counterpart of the BoringSSL job. Same best-effort posture; the
# extra framework links cover the macOS Keychain integration that
# httplib.h auto-enables for any TLS backend on macOS.
macos-boringssl:
runs-on: macos-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
continue-on-error: true
name: macos (boringssl, best-effort)
env:
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(sysctl -n hw.ncpu)" --target install
- name: build and run tests (BoringSSL)
run: |
cd test
# CoreFoundation/Security frameworks satisfy the Keychain integration
# auto-enabled in httplib.h for macOS TLS builds.
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -framework CoreFoundation -framework Security"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
# When gai_suspend() hits the connection timeout, gai_cancel() is called
# but does not block; the resolver worker can later write back into the
# destroyed stack frame. To make the worker actually reach that write,
# the test job runs a loopback UDP responder (test/dns_test_fixture.py)
# that delays its reply past the test's 1s timeout, and uses an iptables
# NAT rule so glibc's lookups land on that fixture instead of a real
# nameserver. With ASAN's detect_stack_use_after_return enabled, the
# late write-back is reported as a stack-use-after-return.
issue-2431-repro:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
name: issue-2431 repro (Linux + ASAN)
# Bound the whole job in case anything in the test harness hangs
# unexpectedly. With the fixture in place a normal run is well under
# a minute either way (ASAN abort on broken HEAD, clean pass on fix).
timeout-minutes: 5
env:
DNS_FIXTURE_PORT: "15353"
DNS_FIXTURE_DELAY: "3"
steps:
- name: checkout
uses: actions/checkout@v4
- name: install libraries
run: |
sudo apt-get update
sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \
libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2
- name: start loopback DNS test fixture
run: |
# Force glibc through its DNS code path: Ubuntu's default
# nsswitch short-circuits to NOTFOUND through mdns4_minimal,
# which would skip the buggy code entirely.
sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf
# Run the loopback fixture (delayed UDP responder).
python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
>/tmp/dns_fixture.log 2>&1 &
echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null
# Wait for the fixture to start listening.
for _ in $(seq 1 50); do
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
break
fi
sleep 0.1
done
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \
|| { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; }
# Send the test process's DNS lookups to the loopback fixture.
# NAT only the local OUTPUT chain; conntrack handles the reply path.
sudo iptables -t nat -I OUTPUT -p udp --dport 53 \
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
# Sanity check: a query must take at least the fixture delay
# and resolve to NXDOMAIN (proving traffic reaches the fixture).
start=$(date +%s)
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
elapsed=$(( $(date +%s) - start ))
if [ "$elapsed" -lt 2 ]; then
echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2
exit 1
fi
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
- name: build test binary
run: cd test && make test
- name: run GetAddrInfoAsyncCancelTest
run: |
cd test
ARCH=$(uname -m)
CPPHTTPLIB_TEST_ISSUE_2431=1 \
ASAN_OPTIONS=detect_stack_use_after_return=1 \
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
setarch "$ARCH" -R \
./test --gtest_filter='GetAddrInfoAsyncCancelTest.*'
- name: tear down test fixture
if: always()
run: |
sudo iptables -t nat -F OUTPUT || true
if [ -f /tmp/dns_fixture.pid ]; then
sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true
fi
macos:
runs-on: macos-latest
if: >
@@ -122,6 +363,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
strategy:
fail-fast: false
matrix:
tls_backend: [openssl, mbedtls, wolfssl]
name: macos (${{ matrix.tls_backend }})
@@ -141,7 +383,10 @@ jobs:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
- name: build and run tests (Mbed TLS)
if: matrix.tls_backend == 'mbedtls'
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
# macOS runners under ASAN+mbedTLS still flake at SHARDS=2 (rapid
# bind/connect on the fixture's fixed port races on the slower
# macos-latest runner). Serialize fully here; ubuntu stays at 2.
run: cd test && make test_split_mbedtls && SHARDS=1 make test_mbedtls_parallel
- name: build and run tests (wolfSSL)
if: matrix.tls_backend == 'wolfssl'
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
@@ -154,6 +399,54 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
ios-parse-check:
runs-on: macos-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
name: ios header parse check (not officially supported)
steps:
- name: checkout
uses: actions/checkout@v4
- name: install OpenSSL headers
run: brew install openssl@3
- name: verify header parses on iOS target
run: |
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
OPENSSL_INC=$(brew --prefix openssl@3)/include
echo "Using iOS SDK: $IOS_SDK"
echo '#include "httplib.h"' | clang++ \
-isysroot "$IOS_SDK" \
-target arm64-apple-ios16.0 \
-std=c++11 \
-DCPPHTTPLIB_OPENSSL_SUPPORT \
-I"$OPENSSL_INC" \
-I. -Wall -Wextra \
-fsyntax-only -x c++ -
- name: verify CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is rejected on iOS
run: |
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
OPENSSL_INC=$(brew --prefix openssl@3)/include
out=$(echo '#include "httplib.h"' | clang++ \
-isysroot "$IOS_SDK" \
-target arm64-apple-ios16.0 \
-std=c++11 \
-DCPPHTTPLIB_OPENSSL_SUPPORT \
-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN \
-I"$OPENSSL_INC" \
-I. \
-fsyntax-only -x c++ - 2>&1 || true)
if echo "$out" | grep -q "only supported on macOS"; then
echo "OK: #error fired as expected"
else
echo "FAIL: expected #error did not fire"
echo "--- compiler output ---"
echo "$out"
exit 1
fi
windows:
runs-on: windows-latest
if: >
@@ -162,6 +455,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_windows == 'true')
strategy:
fail-fast: false
matrix:
config:
- with_ssl: false
@@ -228,7 +522,7 @@ jobs:
for ($i = 0; $i -lt $shards; $i++) {
$log = "shard_${i}.log"
$procs += Start-Process -FilePath ./Release/httplib-test.exe `
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}" `
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '-*_Online' }}" `
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
}
@@ -236,11 +530,14 @@ jobs:
$failed = $false
for ($i = 0; $i -lt $shards; $i++) {
$log = "shard_${i}.log"
if (Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet) {
$proc = $procs[$i]
$hasPassed = Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet
$hasFailed = Select-String -Path $log -Pattern "\[ FAILED \]" -Quiet
if ($hasPassed -and -not $hasFailed -and $proc.ExitCode -eq 0) {
$passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line
Write-Host "Shard ${i}: $passed"
} else {
Write-Host "=== Shard $i FAILED ==="
Write-Host "=== Shard $i FAILED (exit=$($proc.ExitCode)) ==="
Get-Content $log
if (Test-Path "${log}.err") { Get-Content "${log}.err" }
$failed = $true

3
.gitignore vendored
View File

@@ -43,6 +43,9 @@ test/test_mbedtls
test/test_wolfssl
test/test_no_tls
test/server_fuzzer
test/client_fuzzer
test/header_parser_fuzzer
test/url_parser_fuzzer
test/test_proxy
test/test_proxy_mbedtls
test/test_proxy_wolfssl

View File

@@ -3,15 +3,19 @@
A simple, blocking WebSocket implementation for C++11.
> [!IMPORTANT]
> This is a blocking I/O WebSocket implementation using a thread-per-connection model. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
> This is a blocking I/O WebSocket implementation using a thread-per-connection model (plus one heartbeat thread per connection). It is intended for small- to mid-scale workloads; handling large numbers of simultaneous WebSocket connections is outside the design target of this library. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
> [!NOTE]
> WebSocket extensions (`permessage-deflate` and others defined by RFC 6455) are **not supported**. If a client proposes an extension via `Sec-WebSocket-Extensions`, the server silently declines it — the negotiated connection always runs without extensions.
## Features
- **RFC 6455 compliant**: Full WebSocket protocol support
- **RFC 6455 compliant**: Full WebSocket protocol support (extensions are not implemented)
- **Server and Client**: Both sides included
- **SSL/TLS support**: `wss://` scheme for secure connections
- **Text and Binary**: Both message types supported
- **Automatic heartbeat**: Periodic Ping/Pong keeps connections alive
- **Unresponsive-peer detection**: Opt-in liveness check via `set_websocket_max_missed_pongs()`
- **Subprotocol negotiation**: `Sec-WebSocket-Protocol` support for GraphQL, MQTT, etc.
## Quick Start
@@ -352,6 +356,7 @@ if (ws.connect()) {
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) |
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
### Runtime Ping Interval
@@ -373,6 +378,20 @@ ws.set_websocket_ping_interval(10); // 10 seconds
ws.set_websocket_ping_interval(0);
```
### Unresponsive-Peer Detection (Pong Timeout)
By default the heartbeat only sends pings — it does not enforce that pongs come back. To detect a silently dropped connection faster, enable the max-missed-pongs check. Once `max_missed_pongs` consecutive pings go unanswered, the heartbeat thread closes the connection with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
```cpp
ws.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
```
The server side has the same `set_websocket_max_missed_pongs()`.
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. Even so, a dead connection does not linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. `max_missed_pongs` is the knob for detecting an unresponsive peer faster than that 5-minute fallback.
## Threading Model
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.

View File

@@ -8,7 +8,7 @@ It's extremely easy to set up. Just include the **[httplib.h](https://raw.github
Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/) (built with [docs-gen](https://github.com/yhirose/docs-gen)).
> [!IMPORTANT]
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want. Only **HTTP/1.1** is supported — HTTP/2 and HTTP/3 are not implemented.
> [!WARNING]
> 32-bit platforms are **NOT supported**. Use at your own risk. The library may compile on 32-bit targets, but no security review has been conducted for 32-bit environments. Integer truncation and other 32-bit-specific issues may exist. **Security reports that only affect 32-bit platforms will be closed without action.** The maintainer does not have access to 32-bit environments for testing or fixing issues. CI includes basic compile checks only, not functional or security testing.
@@ -73,6 +73,9 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
> [!NOTE]
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
> [!NOTE]
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).
```c++
// Use either OpenSSL, Mbed TLS, or wolfSSL
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
@@ -191,7 +194,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
| Platform | Behavior | Disable (compile time) |
| :------- | :------- | :--------------------- |
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`) | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
On Windows, verification can also be disabled at runtime:
@@ -239,6 +242,8 @@ int main(void)
if (req.has_param("key")) {
auto val = req.get_param_value("key");
}
// Get all values for a given key (e.g., ?tag=a&tag=b)
auto values = req.get_param_values("tag");
res.set_content(req.body, "text/plain");
});
@@ -445,6 +450,8 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
### Pre request handler
The pre-request handler runs after the route has been matched (so `req.matched_route` and `req.path_params` are available) but **before the request body is read**. This means you can reject a request — for example on a failed authentication or authorization check — without forcing the server to buffer a potentially large body.
```cpp
svr.set_pre_request_handler([](const auto& req, auto& res) {
if (req.matched_route == "/user/:user") {
@@ -459,6 +466,38 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
});
```
> [!NOTE]
> Because the body has not been read yet, `req.body` and form fields parsed from the body are not available in the pre-request handler. Inspect headers, the path, query parameters, or `req.matched_route` instead.
### Handler execution order
`set_start_handler` runs once when the server starts. For each request, handlers run in the following order:
```
Request received
├─ pre_routing_handler route not matched yet, body not read
│ └─ returns Handled → stop here
├─ file_request_handler (GET/HEAD, static file serving)
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
├─ route matching → req.matched_route is set
├─ pre_request_handler route matched, body NOT read yet
│ └─ returns Handled → stop here (route handler is skipped)
├─ route handler Get/Post/...; the request body is read first
└─ post_routing_handler after routing completes
On a thrown exception → exception_handler
On an error status (4xx/5xx) → error_handler
```
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
### Response user data
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
@@ -741,6 +780,10 @@ svr.set_keep_alive_timeout(10); // Default is 5
svr.set_read_timeout(5, 0); // 5 seconds
svr.set_write_timeout(5, 0); // 5 seconds
svr.set_idle_interval(0, 100000); // 100 milliseconds
// std::chrono is also supported
svr.set_read_timeout(std::chrono::seconds(5));
svr.set_keep_alive_timeout(std::chrono::seconds(10));
```
### Set maximum payload length for reading a request body
@@ -1051,6 +1094,12 @@ cli.set_write_timeout(5, 0); // 5 seconds
// This method works the same as curl's `--max-time` option
cli.set_max_timeout(5000); // 5 seconds
// std::chrono is also supported
cli.set_connection_timeout(std::chrono::milliseconds(300));
cli.set_read_timeout(std::chrono::seconds(5));
cli.set_write_timeout(std::chrono::seconds(5));
cli.set_max_timeout(std::chrono::seconds(5));
```
### Set maximum payload length for reading a response body
@@ -1166,6 +1215,17 @@ cli.set_proxy_bearer_token_auth("pass");
> [!NOTE]
> OpenSSL is required for Digest Authentication.
#### Bypass the proxy for specific hosts (`NO_PROXY`)
```cpp
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
```
Each pattern is `*`, a hostname suffix, an IP literal, or a CIDR block.
Hostname matching is case-insensitive with a dot-boundary rule. See the
[NO_PROXY cookbook](https://yhirose.github.io/cpp-httplib/en/cookbook/c16-proxy)
for details and for reading the variable from the environment.
### Range
```cpp
@@ -1450,10 +1510,26 @@ if (ws.connect()) {
SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.com/ws")`). Subprotocol negotiation (`Sec-WebSocket-Protocol`) is supported via `SubProtocolSelector` callback.
> **Note:** WebSocket connections occupy a thread for their entire lifetime. If you plan to handle many simultaneous WebSocket connections, consider using a dynamic thread pool: `svr.new_task_queue = [] { return new ThreadPool(8, 64); };`
> **Note:** WebSocket connections occupy a thread for their entire lifetime (plus an additional thread per connection for heartbeat pings). This thread-per-connection model is intended for small- to mid-scale workloads; large numbers of simultaneous WebSocket connections are outside the design target of this library. If you expect many concurrent WebSocket clients, configure a dynamic thread pool (`svr.new_task_queue = [] { return new ThreadPool(8, 64); };`) and measure carefully.
> **WebSocket extensions are not supported.** `permessage-deflate` and other RFC 6455 extensions are not implemented. If a client proposes them via `Sec-WebSocket-Extensions`, the server silently declines them in its handshake response.
> **Unresponsive-peer detection.** Heartbeat pings also serve as a liveness probe when `set_websocket_max_missed_pongs(n)` is set: if the client sends `n` consecutive pings without receiving a pong, it will close the connection. Disabled by default (`0`).
See [README-websocket.md](README-websocket.md) for more details.
## Socket Option Utility
`set_socket_opt` is a convenience wrapper around `setsockopt` for setting integer socket options:
```cpp
auto sock = svr.socket();
httplib::set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
```
> [!TIP]
> For most use cases, prefer `set_tcp_nodelay(true)` or `set_socket_options(callback)` on the Server/Client instead of calling `set_socket_opt` directly.
## Split httplib.h into .h and .cc
```console

View File

@@ -7,11 +7,12 @@ CPPHTTPLIB_FLAGS = -DCPPHTTPLIB_THREAD_POOL_COUNT=16
BENCH = bombardier -c 10 -d 5s localhost:8080
MONITOR = ali http://localhost:8080
WAIT = while ! nc -z localhost 8080 >/dev/null 2>&1; do sleep 0.05; done
# cpp-httplib
bench: server
@echo "--------------------\n cpp-httplib latest\n--------------------\n"
@./server & export PID=$$!; $(BENCH); kill $${PID}
@./server & export PID=$$!; $(WAIT); $(BENCH); kill $${PID}
@echo ""
monitor: server
@@ -26,7 +27,7 @@ server : cpp-httplib/main.cpp ../httplib.h
# crow
bench-crow: server-crow
@echo "-------------\n Crow v1.3.1\n-------------\n"
@./server-crow & export PID=$$!; $(BENCH); kill $${PID}
@./server-crow & export PID=$$!; $(WAIT); $(BENCH); kill $${PID}
@echo ""
monitor-crow: server-crow

View File

@@ -61,7 +61,7 @@ if(@HTTPLIB_IS_USING_ZSTD@)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
set(httplib_fd_zstd_required_arg REQUIRED)
endif()
find_package(zstd QUIET)
find_package(zstd 1.5.6 CONFIG QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
if(PKG_CONFIG_FOUND)

View File

@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.41.0"
version = "0.47.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."

View File

@@ -0,0 +1,60 @@
---
title: "C01. Get the Response Body / Save to a File"
order: 1
status: "draft"
---
## Get it as a string
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/hello");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
`res->body` is a `std::string`, ready to use as-is. The entire response is loaded into memory.
> **Warning:** If you fetch a large file with `res->body`, it all goes into memory. For large downloads, use a `ContentReceiver` as shown below.
## Save to a file
```cpp
httplib::Client cli("http://localhost:8080");
std::ofstream ofs("output.bin", std::ios::binary);
if (!ofs) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto res = cli.Get("/large-file",
[&](const char *data, size_t len) {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
```
With a `ContentReceiver`, data arrives in chunks. You can write each chunk straight to disk without buffering the whole body in memory — perfect for large file downloads.
Return `false` from the callback to abort the download. In the example above, if writing to `ofs` fails, the download stops automatically.
> **Detail:** Want to check response headers like Content-Length before downloading? Combine a `ResponseHandler` with a `ContentReceiver`.
>
> ```cpp
> auto res = cli.Get("/large-file",
> [](const httplib::Response &res) {
> auto len = res.get_header_value("Content-Length");
> std::cout << "Size: " << len << std::endl;
> return true; // return false to skip the download
> },
> [&](const char *data, size_t len) {
> ofs.write(data, len);
> return static_cast<bool>(ofs);
> });
> ```
>
> The `ResponseHandler` is called after headers arrive but before the body. Return `false` to skip the download entirely.
> To show download progress, see [C11. Use the progress callback](c11-progress-callback).

View File

@@ -0,0 +1,36 @@
---
title: "C02. Send and Receive JSON"
order: 2
status: "draft"
---
cpp-httplib doesn't include a JSON parser. Use a library like [nlohmann/json](https://github.com/nlohmann/json) to build and parse JSON. The examples here use `nlohmann/json`.
## Send JSON
```cpp
httplib::Client cli("http://localhost:8080");
nlohmann::json j = {{"name", "Alice"}, {"age", 30}};
auto res = cli.Post("/api/users", j.dump(), "application/json");
```
Pass the JSON string as the second argument to `Post()` and the Content-Type as the third. The same pattern works with `Put()` and `Patch()`.
> **Warning:** If you omit the Content-Type (the third argument), the server may not recognize the body as JSON. Always specify `"application/json"`.
## Receive a JSON response
```cpp
auto res = cli.Get("/api/users/1");
if (res && res->status == 200) {
auto j = nlohmann::json::parse(res->body);
std::cout << j["name"] << std::endl;
}
```
`res->body` is a `std::string`, so you can pass it straight to your JSON library.
> **Note:** Servers sometimes return HTML on errors. Check the status code before parsing to be safe. Some APIs also require an `Accept: application/json` header. If you're calling a JSON API repeatedly, [C03. Set default headers](c03-default-headers) can save you some boilerplate.
> For how to receive and return JSON on the server side, see [S02. Receive JSON requests and return JSON responses](s02-json-api).

View File

@@ -0,0 +1,55 @@
---
title: "C03. Set Default Headers"
order: 3
status: "draft"
---
When you want the same headers on every request, use `set_default_headers()`. Once set, they're attached automatically to every request sent from that client.
## Basic usage
```cpp
httplib::Client cli("https://api.example.com");
cli.set_default_headers({
{"Accept", "application/json"},
{"User-Agent", "my-app/1.0"},
});
auto res = cli.Get("/users");
```
Register the headers you need on every API call — like `Accept` or `User-Agent` — in one place. No need to repeat them on each request.
## Send a Bearer token on every request
```cpp
httplib::Client cli("https://api.example.com");
cli.set_default_headers({
{"Authorization", "Bearer " + token},
{"Accept", "application/json"},
});
auto res1 = cli.Get("/me");
auto res2 = cli.Get("/projects");
```
Set the auth token once, and every subsequent request carries it. Handy when you're writing an API client that hits multiple endpoints.
> **Note:** `set_default_headers()` **replaces** the existing default headers. Even if you only want to add one, pass the full set again.
## Combine with per-request headers
You can still pass extra headers on individual requests, even with defaults set.
```cpp
httplib::Headers headers = {
{"X-Request-ID", "abc-123"},
};
auto res = cli.Get("/users", headers);
```
Per-request headers are **added** on top of the defaults. Both are sent to the server.
> For details on Bearer token auth, see [C06. Call an API with a Bearer token](c06-bearer-token).

View File

@@ -0,0 +1,38 @@
---
title: "C04. Follow Redirects"
order: 4
status: "draft"
---
By default, cpp-httplib does not follow HTTP redirects (3xx). If the server returns `302 Found`, you'll get it as a response with status code 302 — nothing more.
To follow redirects automatically, call `set_follow_location(true)`.
## Follow redirects
```cpp
httplib::Client cli("http://example.com");
cli.set_follow_location(true);
auto res = cli.Get("/old-path");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
With `set_follow_location(true)`, the client reads the `Location` header and reissues the request to the new URL automatically. The final response ends up in `res`.
## Redirects from HTTP to HTTPS
```cpp
httplib::Client cli("http://example.com");
cli.set_follow_location(true);
auto res = cli.Get("/");
```
Many sites redirect HTTP traffic to HTTPS. With `set_follow_location(true)` on, this case is handled transparently — the client follows redirects even when the scheme or host changes.
> **Warning:** To follow redirects to HTTPS, you need to build cpp-httplib with OpenSSL (or another TLS backend). Without TLS support, redirects to HTTPS will fail.
> **Note:** Following redirects adds to the total request time. See [C12. Set timeouts](c12-timeouts) for timeout configuration.

View File

@@ -0,0 +1,46 @@
---
title: "C05. Use Basic Authentication"
order: 5
status: "draft"
---
For endpoints that require Basic authentication, pass the username and password to `set_basic_auth()`. cpp-httplib builds the `Authorization: Basic ...` header for you.
## Basic usage
```cpp
httplib::Client cli("https://api.example.com");
cli.set_basic_auth("alice", "s3cret");
auto res = cli.Get("/private");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
Set it once, and every request from that client carries the credentials. No need to build the header each time.
## Per-request usage
If you want credentials on only one specific request, pass headers directly.
```cpp
httplib::Headers headers = {
httplib::make_basic_authentication_header("alice", "s3cret"),
};
auto res = cli.Get("/private", headers);
```
`make_basic_authentication_header()` builds the Base64-encoded header for you.
> **Warning:** Basic authentication **encodes** credentials in Base64 — it does not encrypt them. Always use it over HTTPS. Over plain HTTP, your password travels the network in the clear.
## Digest authentication
For the more secure Digest authentication scheme, use `set_digest_auth()`. This is only available when cpp-httplib is built with OpenSSL (or another TLS backend).
```cpp
cli.set_digest_auth("alice", "s3cret");
```
> To call an API with a Bearer token, see [C06. Call an API with a Bearer token](c06-bearer-token).

View File

@@ -0,0 +1,50 @@
---
title: "C06. Call an API with a Bearer Token"
order: 6
status: "draft"
---
For Bearer token authentication — common in OAuth 2.0 and modern Web APIs — use `set_bearer_token_auth()`. Pass the token and cpp-httplib builds the `Authorization: Bearer <token>` header for you.
## Basic usage
```cpp
httplib::Client cli("https://api.example.com");
cli.set_bearer_token_auth("eyJhbGciOiJIUzI1NiIs...");
auto res = cli.Get("/me");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
Set it once and every subsequent request carries the token. This is the go-to pattern for token-based APIs like GitHub, Slack, or your own OAuth service.
## Per-request usage
When you want the token on only one request — or need a different token per request — pass it via headers.
```cpp
httplib::Headers headers = {
httplib::make_bearer_token_authentication_header(token),
};
auto res = cli.Get("/me", headers);
```
`make_bearer_token_authentication_header()` builds the `Authorization` header for you.
## Refresh the token
When a token expires, just call `set_bearer_token_auth()` again with the new one.
```cpp
if (res && res->status == 401) {
auto new_token = refresh_token();
cli.set_bearer_token_auth(new_token);
res = cli.Get("/me");
}
```
> **Warning:** A Bearer token is itself a credential. Always send it over HTTPS, and never hard-code it into source or config files.
> To set multiple headers at once, see [C03. Set default headers](c03-default-headers).

View File

@@ -0,0 +1,52 @@
---
title: "C07. Upload a File as Multipart Form Data"
order: 7
status: "draft"
---
When you want to send a file the same way an HTML `<input type="file">` does, use multipart form data (`multipart/form-data`). cpp-httplib offers two APIs — `UploadFormDataItems` and `FormDataProviderItems` — and you pick between them based on **file size**.
## Send a small file
Read the file into memory first, then send it. For small files, this is the simplest path.
```cpp
httplib::Client cli("http://localhost:8080");
std::ifstream ifs("avatar.png", std::ios::binary);
std::string content((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
httplib::UploadFormDataItems items = {
{"name", "Alice", "", ""},
{"avatar", content, "avatar.png", "image/png"},
};
auto res = cli.Post("/upload", items);
```
Each `UploadFormData` entry is `{name, content, filename, content_type}`. For plain text fields, leave `filename` and `content_type` empty.
## Stream a large file
To avoid loading the whole file into memory, use `make_file_provider()`. It reads the file in chunks as it sends — so even huge files won't blow up your memory footprint.
```cpp
httplib::Client cli("http://localhost:8080");
httplib::UploadFormDataItems items = {
{"name", "Alice", "", ""},
};
httplib::FormDataProviderItems provider_items = {
httplib::make_file_provider("video", "large-video.mp4", "", "video/mp4"),
};
auto res = cli.Post("/upload", httplib::Headers{}, items, provider_items);
```
The arguments to `make_file_provider()` are `(form name, file path, file name, content type)`. Leave the file name empty to use the file path as-is.
> **Note:** You can mix `UploadFormDataItems` and `FormDataProviderItems` in the same request. A clean split is: text fields in `UploadFormDataItems`, files in `FormDataProviderItems`.
> To show upload progress, see [C11. Use the progress callback](c11-progress-callback).

View File

@@ -0,0 +1,34 @@
---
title: "C08. POST a File as Raw Binary"
order: 8
status: "draft"
---
Sometimes you want to send a file's contents as the request body directly — no multipart wrapping. This is common for S3-compatible APIs or endpoints that take raw image data. For this, use `make_file_body()`.
## Basic usage
```cpp
httplib::Client cli("https://storage.example.com");
auto [size, provider] = httplib::make_file_body("backup.tar.gz");
if (size == 0) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto res = cli.Put("/bucket/backup.tar.gz", size,
provider, "application/gzip");
```
`make_file_body()` returns a pair of file size and a `ContentProvider`. Pass them to `Post()` or `Put()` and the file contents flow straight into the request body.
The `ContentProvider` reads the file in chunks, so even huge files never sit fully in memory.
## When the file can't be opened
If the file can't be opened, `make_file_body()` returns `size` as `0` and `provider` as an empty function object. Sending that would produce garbage — always check `size` first.
> **Warning:** `make_file_body()` needs to fix the Content-Length up front, so it reads the file size ahead of time. If the file size might change mid-upload, this API isn't the right fit.
> To send the file as multipart form data instead, see [C07. Upload a file as multipart form data](c07-multipart-upload).

View File

@@ -0,0 +1,47 @@
---
title: "C09. Send the Body with Chunked Transfer"
order: 9
status: "draft"
---
When you don't know the body size up front — for data generated on the fly or piped from another stream — use `ContentProviderWithoutLength`. The client sends the body with HTTP chunked transfer encoding.
## Basic usage
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Post("/stream",
[&](size_t offset, httplib::DataSink &sink) {
std::string chunk = produce_next_chunk();
if (chunk.empty()) {
sink.done(); // done sending
return true;
}
return sink.write(chunk.data(), chunk.size());
},
"application/octet-stream");
```
The lambda's job is just: produce the next chunk and send it with `sink.write()`. When there's no more data, call `sink.done()` and you're finished.
## When the size is known
If you **do** know the total size ahead of time, use the `ContentProvider` overload (taking `size_t offset, size_t length, DataSink &sink`) and pass the total size as well.
```cpp
size_t total_size = get_total_size();
auto res = cli.Post("/upload", total_size,
[&](size_t offset, size_t length, httplib::DataSink &sink) {
auto data = read_range(offset, length);
return sink.write(data.data(), data.size());
},
"application/octet-stream");
```
With a known size, the request carries a Content-Length header — so the server can show progress. Prefer this form when you can.
> **Detail:** `sink.write()` returns a `bool` indicating whether the write succeeded. If it returns `false`, the connection is gone — return `false` from the lambda to stop.
> If you're just sending a file, `make_file_body()` is easier. See [C08. POST a file as raw binary](c08-post-file-body).

View File

@@ -0,0 +1,52 @@
---
title: "C10. Receive a Response as a Stream"
order: 10
status: "draft"
---
To receive a response body chunk by chunk, use a `ContentReceiver`. It's the obvious choice for large files, but it's equally handy for NDJSON (newline-delimited JSON) or log streams where you want to start processing data as it arrives.
## Process each chunk
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/logs/stream",
[](const char *data, size_t len) {
std::cout.write(data, len);
std::cout.flush();
return true; // return false to stop receiving
});
```
Data arrives in the lambda in the order it's received from the server. Return `false` from the callback to stop the download partway through.
## Parse NDJSON line by line
Here's a buffered approach for processing newline-delimited JSON one line at a time.
```cpp
std::string buffer;
auto res = cli.Get("/events",
[&](const char *data, size_t len) {
buffer.append(data, len);
size_t pos;
while ((pos = buffer.find('\n')) != std::string::npos) {
auto line = buffer.substr(0, pos);
buffer.erase(0, pos + 1);
if (!line.empty()) {
auto j = nlohmann::json::parse(line);
handle_event(j);
}
}
return true;
});
```
Accumulate into a buffer, then pull out and parse one line each time you see a newline. This is the standard pattern for consuming a streaming API in real time.
> **Warning:** When you pass a `ContentReceiver`, `res->body` stays **empty**. Store or process the body inside the callback yourself.
> To track download progress, combine this with [C11. Use the progress callback](c11-progress-callback).
> For Server-Sent Events (SSE), see [E04. Receive SSE on the client](e04-sse-client).

View File

@@ -0,0 +1,59 @@
---
title: "C11. Use the Progress Callback"
order: 11
status: "draft"
---
To display download or upload progress, pass a `DownloadProgress` or `UploadProgress` callback. Both take two arguments: `(current, total)`.
## Download progress
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/large-file",
[](size_t current, size_t total) {
auto percent = (total > 0) ? (current * 100 / total) : 0;
std::cout << "\rDownloading: " << percent << "% ("
<< current << "/" << total << ")" << std::flush;
return true; // return false to abort
});
std::cout << std::endl;
```
The callback fires each time data arrives. `total` comes from the Content-Length header — if the server doesn't send one, it may be `0`. In that case, you can't compute a percentage, so just display bytes received.
## Upload progress
Uploads work the same way. Pass an `UploadProgress` as the last argument to `Post()` or `Put()`.
```cpp
httplib::Client cli("http://localhost:8080");
std::string body = load_large_body();
auto res = cli.Post("/upload", body, "application/octet-stream",
[](size_t current, size_t total) {
auto percent = current * 100 / total;
std::cout << "\rUploading: " << percent << "%" << std::flush;
return true;
});
std::cout << std::endl;
```
## Cancel mid-transfer
Return `false` from the callback to abort the transfer. This is how you wire up a "Cancel" button in a UI — flip a flag, and the next progress tick stops the transfer.
```cpp
std::atomic<bool> cancelled{false};
auto res = cli.Get("/large-file",
[&](size_t current, size_t total) {
return !cancelled.load();
});
```
> **Note:** `ContentReceiver` and the progress callback can be used together. When you want to stream to a file and show progress at the same time, pass both.
> For a concrete example of saving to a file, see [C01. Get the response body / save to a file](c01-get-response-body).

View File

@@ -0,0 +1,50 @@
---
title: "C12. Set Timeouts"
order: 12
status: "draft"
---
The client has three kinds of timeouts, each set independently.
| Kind | API | Default | Meaning |
| --- | --- | --- | --- |
| Connection | `set_connection_timeout` | 300s | Time to wait for the TCP connection to establish |
| Read | `set_read_timeout` | 300s | Time to wait for a single `recv` when receiving the response |
| Write | `set_write_timeout` | 5s | Time to wait for a single `send` when sending the request |
## Basic usage
```cpp
httplib::Client cli("http://localhost:8080");
cli.set_connection_timeout(5, 0); // 5 seconds
cli.set_read_timeout(10, 0); // 10 seconds
cli.set_write_timeout(10, 0); // 10 seconds
auto res = cli.Get("/api/data");
```
Pass seconds and microseconds as two arguments. If you don't need the sub-second part, you can omit the second argument.
## Use `std::chrono`
There's also an overload that takes a `std::chrono` duration directly. It's easier to read — recommended.
```cpp
using namespace std::chrono_literals;
cli.set_connection_timeout(5s);
cli.set_read_timeout(10s);
cli.set_write_timeout(500ms);
```
## Watch out for the long 300s default
Connection and read timeouts default to **300 seconds (5 minutes)**. If the server hangs, you'll be waiting five minutes by default. Shorter values are usually a better idea.
```cpp
cli.set_connection_timeout(3s);
cli.set_read_timeout(10s);
```
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use [C13. Set an overall timeout](c13-max-timeout).

View File

@@ -0,0 +1,42 @@
---
title: "C13. Set an Overall Timeout"
order: 13
status: "draft"
---
The three timeouts from [C12. Set timeouts](c12-timeouts) all apply to a single `send` or `recv` call. To cap the total time a request can take, use `set_max_timeout()`.
## Basic usage
```cpp
httplib::Client cli("http://localhost:8080");
cli.set_max_timeout(5000); // 5 seconds (in milliseconds)
auto res = cli.Get("/slow-endpoint");
```
The value is in milliseconds. Connection, send, and receive together — the whole request is aborted if it exceeds the limit.
## Use `std::chrono`
There's also an overload that takes a `std::chrono` duration.
```cpp
using namespace std::chrono_literals;
cli.set_max_timeout(5s);
```
## When to use which
`set_read_timeout` fires when no data arrives for a while. If data keeps trickling in bit by bit, it will never fire. An endpoint that sends one byte per second can make `set_read_timeout` useless no matter how short you set it.
`set_max_timeout` caps elapsed time, so it handles those cases cleanly. It's great for calls to external APIs or anywhere you don't want users waiting forever.
```cpp
cli.set_connection_timeout(3s);
cli.set_read_timeout(10s);
cli.set_max_timeout(30s); // abort if the whole request takes over 30s
```
> **Note:** `set_max_timeout()` works alongside the regular timeouts. Short stalls get caught by `set_read_timeout`; long-running requests get capped by `set_max_timeout`. Use both for a safety net.

View File

@@ -0,0 +1,53 @@
---
title: "C14. Understand Connection Reuse and Keep-Alive"
order: 14
status: "draft"
---
When you send multiple requests through the same `httplib::Client` instance, the TCP connection is reused automatically. HTTP/1.1 Keep-Alive does the work for you — you don't pay the TCP and TLS handshake cost on every call.
## Connections are reused automatically
```cpp
httplib::Client cli("https://api.example.com");
auto res1 = cli.Get("/users/1");
auto res2 = cli.Get("/users/2"); // reuses the same connection
auto res3 = cli.Get("/users/3"); // reuses the same connection
```
No special config required. Just hold on to `cli` — internally, the socket stays open across calls. The effect is especially noticeable over HTTPS, where the TLS handshake is expensive.
## Disable Keep-Alive explicitly
To force a fresh connection every time, call `set_keep_alive(false)`. Mostly useful for testing.
```cpp
cli.set_keep_alive(false);
```
For normal use, leave it on (the default).
## Don't create a `Client` per request
If you create a `Client` inside a loop and let it fall out of scope each iteration, you lose the reuse benefit. Create the instance outside the loop.
```cpp
// Bad: a new connection every iteration
for (auto id : ids) {
httplib::Client cli("https://api.example.com");
cli.Get("/users/" + id);
}
// Good: the connection is reused
httplib::Client cli("https://api.example.com");
for (auto id : ids) {
cli.Get("/users/" + id);
}
```
## Concurrent requests
If you want to send requests in parallel from multiple threads, give each thread its own `Client` instance. A single `Client` uses a single TCP connection, so firing concurrent requests at the same instance ends up serializing them anyway.
> **Note:** If the server closes the connection after its Keep-Alive timeout, cpp-httplib reconnects and retries transparently. You don't need to handle this in application code.

View File

@@ -0,0 +1,47 @@
---
title: "C15. Enable Compression"
order: 15
status: "draft"
---
cpp-httplib supports compression when sending and decompression when receiving. You just need to build it with zlib or Brotli enabled.
## Build-time setup
To use compression, define these macros before including `httplib.h`:
```cpp
#define CPPHTTPLIB_ZLIB_SUPPORT // gzip / deflate
#define CPPHTTPLIB_BROTLI_SUPPORT // brotli
#include <httplib.h>
```
You'll also need to link against `zlib` or `brotli`.
## Compress the request body
```cpp
httplib::Client cli("https://api.example.com");
cli.set_compress(true);
std::string big_payload = build_payload();
auto res = cli.Post("/api/data", big_payload, "application/json");
```
With `set_compress(true)`, the body of POST or PUT requests gets gzipped before sending. The server needs to handle compressed bodies too.
## Decompress the response
```cpp
httplib::Client cli("https://api.example.com");
cli.set_decompress(true); // on by default
auto res = cli.Get("/api/data");
std::cout << res->body << std::endl;
```
With `set_decompress(true)`, the client automatically decompresses responses that arrive with `Content-Encoding: gzip` or similar. `res->body` contains the decompressed data.
It's on by default, so normally you don't need to do anything. Set it to `false` only if you want the raw compressed bytes.
> **Warning:** If you build without `CPPHTTPLIB_ZLIB_SUPPORT`, calling `set_compress()` or `set_decompress()` does nothing. If compression isn't working, check the macro definition first.

View File

@@ -0,0 +1,87 @@
---
title: "C16. Send Requests Through a Proxy"
order: 16
status: "draft"
---
To route traffic through a corporate network or a specific path, send requests via an HTTP proxy. Just pass the proxy host and port to `set_proxy()`.
## Basic usage
```cpp
httplib::Client cli("https://api.example.com");
cli.set_proxy("proxy.internal", 8080);
auto res = cli.Get("/users");
```
The request goes through the proxy. For HTTPS, the client uses the CONNECT method to tunnel through — no extra setup required.
## Proxy authentication
If the proxy itself requires authentication, use `set_proxy_basic_auth()` or `set_proxy_bearer_token_auth()`.
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_proxy_basic_auth("user", "password");
```
```cpp
cli.set_proxy_bearer_token_auth("token");
```
If cpp-httplib is built with OpenSSL (or another TLS backend), you can also use Digest authentication for the proxy.
```cpp
cli.set_proxy_digest_auth("user", "password");
```
## Combine with end-server authentication
Proxy authentication is separate from authenticating to the end server ([C05. Use Basic authentication](c05-basic-auth), [C06. Call an API with a Bearer token](c06-bearer-token)). When both are needed, set both.
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_proxy_basic_auth("proxy-user", "proxy-pass");
cli.set_bearer_token_auth("api-token"); // for the end server
```
`Proxy-Authorization` is sent to the proxy, `Authorization` to the end server.
## Bypass the proxy for specific hosts
You often want internal endpoints to skip the proxy. Configure a bypass list with `set_no_proxy()`.
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
```
Each entry is one of:
- `*` — bypass the proxy for all hosts
- a hostname suffix (e.g. `example.com`) — matches `example.com` itself and any subdomain (`foo.example.com`). A leading dot is permitted but informational; both forms are equivalent.
- a single IP literal (e.g. `192.168.1.1`, `::1`)
- a CIDR block (e.g. `10.0.0.0/8`, `fe80::/10`)
Hostname matching is case-insensitive and uses a dot-boundary rule, so an entry of `example.com` does **not** match `evilexample.com`. IP comparisons are normalized through `inet_pton`, so `127.0.0.1` cannot be bypassed via alternate string forms (e.g. `127.000.000.001`). When an entry matches, the `Proxy-Authorization` header is suppressed as well.
Malformed entries are silently dropped. Port-specific entries such as `example.com:8080` are not supported (cpp-httplib's other host-keyed APIs are also keyed on hostname only).
## Read proxy settings from the environment
cpp-httplib doesn't touch `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` on its own — the config API is always explicit, the same way `set_ca_cert_path()` is. If you'd like that behavior, read the variables in your application and feed them to `set_proxy()` and `set_no_proxy()`.
```cpp
if (const char *v = std::getenv("no_proxy")) {
std::vector<std::string> patterns;
std::stringstream ss(v);
for (std::string item; std::getline(ss, item, ',');) {
if (!item.empty()) { patterns.push_back(item); }
}
cli.set_no_proxy(patterns);
}
```
If you also read `HTTP_PROXY` yourself, honor the lowercase `http_proxy` only. The uppercase form is poisoned in CGI/FastCGI environments by the `Proxy:` request header ([CVE-2016-5385 / "httpoxy"](https://httpoxy.org/)). `HTTPS_PROXY` and `NO_PROXY` are safe in either case because their names don't begin with `HTTP_`.

View File

@@ -0,0 +1,63 @@
---
title: "C17. Handle Error Codes"
order: 17
status: "draft"
---
`cli.Get()`, `cli.Post()`, and friends return a `Result`. When the request fails — can't reach the server, times out, etc. — the result is "falsy". To get the specific reason, use `Result::error()`.
## Basic check
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/api/data");
if (res) {
// the request was sent and a response came back
std::cout << "status: " << res->status << std::endl;
} else {
// the network layer failed
std::cerr << "error: " << httplib::to_string(res.error()) << std::endl;
}
```
Use `if (res)` to check success. On failure, `res.error()` returns a `httplib::Error` enum value. Pass it to `to_string()` to get a human-readable description.
## Common errors
| Value | Meaning |
| --- | --- |
| `Error::Connection` | Couldn't connect to the server |
| `Error::ConnectionTimeout` | Connection timeout (`set_connection_timeout`) |
| `Error::Read` / `Error::Write` | Error during send or receive |
| `Error::Timeout` | Overall timeout set via `set_max_timeout` |
| `Error::ExceedRedirectCount` | Too many redirects |
| `Error::SSLConnection` | TLS handshake failed |
| `Error::SSLServerVerification` | Server certificate verification failed |
| `Error::Canceled` | A progress callback returned `false` |
## Network errors vs. HTTP errors
Even when `res` is truthy, the HTTP status code can still be 4xx or 5xx. These are two different things.
```cpp
auto res = cli.Get("/api/data");
if (!res) {
// network error (no response received at all)
std::cerr << "network error: " << httplib::to_string(res.error()) << std::endl;
return 1;
}
if (res->status >= 400) {
// HTTP error (response received, but the status is bad)
std::cerr << "http error: " << res->status << std::endl;
return 1;
}
// success
std::cout << res->body << std::endl;
```
Keep them separated in your head: network-layer errors go through `res.error()`, HTTP-level errors through `res->status`.
> To dig deeper into SSL-related errors, see [C18. Handle SSL errors](c18-ssl-errors).

View File

@@ -0,0 +1,51 @@
---
title: "C18. Handle SSL Errors"
order: 18
status: "draft"
---
When an HTTPS request fails, `res.error()` returns values like `Error::SSLConnection` or `Error::SSLServerVerification`. Sometimes that's not enough to pinpoint the cause. That's where `Result::ssl_error()` and `Result::ssl_backend_error()` help.
## Get the SSL error details
```cpp
httplib::Client cli("https://api.example.com");
auto res = cli.Get("/");
if (!res) {
auto err = res.error();
std::cerr << "error: " << httplib::to_string(err) << std::endl;
if (err == httplib::Error::SSLConnection ||
err == httplib::Error::SSLServerVerification) {
std::cerr << "ssl_error: " << res.ssl_error() << std::endl;
std::cerr << "ssl_backend_error: " << res.ssl_backend_error() << std::endl;
}
}
```
`ssl_error()` returns the error code from the SSL library (e.g., OpenSSL's `SSL_get_error()`). `ssl_backend_error()` gives you the backend's more detailed error value — for OpenSSL, that's `ERR_get_error()`.
## Format OpenSSL errors as strings
When you have a value from `ssl_backend_error()`, pass it to OpenSSL's `ERR_error_string()` to get a readable message.
```cpp
#include <openssl/err.h>
if (res.ssl_backend_error() != 0) {
char buf[256];
ERR_error_string_n(res.ssl_backend_error(), buf, sizeof(buf));
std::cerr << "openssl: " << buf << std::endl;
}
```
## Common causes
| Symptom | Usual suspect |
| --- | --- |
| `SSLServerVerification` | CA certificate path isn't configured, or the cert is self-signed |
| `SSLServerHostnameVerification` | The cert's CN/SAN doesn't match the host |
| `SSLConnection` | TLS version mismatch, no shared cipher suite |
> To change certificate verification settings, see [T02. Control SSL certificate verification](t02-cert-verification).

View File

@@ -0,0 +1,57 @@
---
title: "C19. Set a Logger on the Client"
order: 19
status: "draft"
---
To log requests sent and responses received by the client, use `set_logger()`. If you only care about errors, there's a separate `set_error_logger()`.
## Log requests and responses
```cpp
httplib::Client cli("https://api.example.com");
cli.set_logger([](const httplib::Request &req, const httplib::Response &res) {
std::cout << req.method << " " << req.path
<< " -> " << res.status << std::endl;
});
auto res = cli.Get("/users");
```
The callback you pass to `set_logger()` fires once for each completed request. You get both the request and the response as arguments — so you can log the method, path, status, headers, body, or whatever else you need.
## Catch errors only
When a network-layer error happens (like `Error::Connection`), `set_logger()` is **not** called — there's no response to log. For those cases, use `set_error_logger()`.
```cpp
cli.set_error_logger([](const httplib::Error &err, const httplib::Request *req) {
std::cerr << "error: " << httplib::to_string(err);
if (req) {
std::cerr << " (" << req->method << " " << req->path << ")";
}
std::cerr << std::endl;
});
```
The second argument `req` can be null — it happens when the failure occurred before the request was built. Always null-check before dereferencing.
## Use both together
A nice pattern is to log successes through one, failures through the other.
```cpp
cli.set_logger([](const auto &req, const auto &res) {
std::cout << "[ok] " << req.method << " " << req.path
<< " " << res.status << std::endl;
});
cli.set_error_logger([](const auto &err, const auto *req) {
std::cerr << "[ng] " << httplib::to_string(err);
if (req) std::cerr << " " << req->method << " " << req->path;
std::cerr << std::endl;
});
```
> **Note:** The log callbacks run synchronously on the same thread as the request. Heavy work inside them slows the request down — push it to a background queue if you need to do anything expensive.

View File

@@ -0,0 +1,87 @@
---
title: "E01. Implement an SSE Server"
order: 47
status: "draft"
---
Server-Sent Events (SSE) is a simple protocol for pushing events one-way from server to client. The connection stays open, and the server can send data whenever it wants. It's lighter than WebSocket and fits entirely within HTTP — a nice combination.
cpp-httplib doesn't have a dedicated SSE server API, but you can implement one with `set_chunked_content_provider()` and `text/event-stream`.
## Basic SSE server
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[](size_t offset, httplib::DataSink &sink) {
std::string message = "data: hello\n\n";
sink.write(message.data(), message.size());
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
Three things matter here:
1. Content-Type is `text/event-stream`
2. Messages follow the format `data: <content>\n\n` (the double newline separates events)
3. Each `sink.write()` delivers data to the client
The provider lambda keeps being called as long as the connection is alive.
## A continuous stream
Here's a simple example that sends the current time once per second.
```cpp
svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&req](size_t offset, httplib::DataSink &sink) {
if (req.is_connection_closed()) {
sink.done();
return true;
}
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::string msg = "data: " + std::string(std::ctime(&t)) + "\n";
sink.write(msg.data(), msg.size());
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
When the client disconnects, call `sink.done()` to stop. Details in [S16. Detect client disconnection](s16-disconnect).
## Heartbeats via comment lines
Lines starting with `:` are SSE comments — clients ignore them, but they **keep the connection alive**. Handy for preventing proxies and load balancers from closing idle connections.
```cpp
// heartbeat every 30 seconds
if (tick_count % 30 == 0) {
std::string ping = ": ping\n\n";
sink.write(ping.data(), ping.size());
}
```
## Relationship with the thread pool
SSE connections stay open, so each client holds a worker thread. For lots of concurrent connections, enable dynamic scaling on the thread pool.
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(8, 128);
};
```
See [S21. Configure the thread pool](s21-thread-pool).
> **Note:** When `data:` contains newlines, split it into multiple `data:` lines — one per line. This is how the SSE spec requires multiline data to be transmitted.
> For event names, see [E02. Use named events in SSE](e02-sse-event-names). For the client side, see [E04. Receive SSE on the client](e04-sse-client).

View File

@@ -0,0 +1,84 @@
---
title: "E02. Use Named Events in SSE"
order: 48
status: "draft"
---
SSE lets you send multiple kinds of events over the same stream. Give each one a name with the `event:` field, and the client can dispatch to a different handler per type. Great for things like "new message", "user joined", "user left" in a chat app.
## Send events with names
```cpp
auto send_event = [](httplib::DataSink &sink,
const std::string &event,
const std::string &data) {
std::string msg = "event: " + event + "\n"
+ "data: " + data + "\n\n";
sink.write(msg.data(), msg.size());
};
svr.Get("/chat/stream", [&](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&, send_event](size_t offset, httplib::DataSink &sink) {
send_event(sink, "message", "Hello!");
std::this_thread::sleep_for(std::chrono::seconds(2));
send_event(sink, "join", "alice");
std::this_thread::sleep_for(std::chrono::seconds(2));
send_event(sink, "leave", "bob");
std::this_thread::sleep_for(std::chrono::seconds(2));
return true;
});
});
```
A message is `event:``data:` → blank line. If you omit `event:`, the client treats it as a default `"message"` event.
## Attach IDs for reconnect
When you include an `id:` field, the client automatically sends it back as `Last-Event-ID` on reconnect, telling the server "here's how far I got."
```cpp
auto send_event = [](httplib::DataSink &sink,
const std::string &event,
const std::string &data,
const std::string &id) {
std::string msg = "id: " + id + "\n"
+ "event: " + event + "\n"
+ "data: " + data + "\n\n";
sink.write(msg.data(), msg.size());
};
send_event(sink, "message", "Hello!", "42");
```
The ID format is up to you. Monotonic counters or UUIDs both work — just pick something unique and orderable on the server side. See [E03. Handle SSE reconnection](e03-sse-reconnect) for details.
## JSON payloads in data
For structured data, the usual move is to put JSON in `data:`.
```cpp
nlohmann::json payload = {
{"user", "alice"},
{"text", "Hello!"},
};
send_event(sink, "message", payload.dump(), "42");
```
On the client, parse the incoming `data` as JSON to get the original object back.
## Data with newlines
If the data value contains newlines, split it across multiple `data:` lines.
```cpp
std::string msg = "data: line1\n"
"data: line2\n"
"data: line3\n\n";
sink.write(msg.data(), msg.size());
```
On the client side, these come back as a single `data` string with newlines.
> **Note:** Using `event:` makes client-side dispatch cleaner, but it also helps in the browser DevTools — events are easier to filter by type. That matters more than you'd expect while debugging.

View File

@@ -0,0 +1,85 @@
---
title: "E03. Handle SSE Reconnection"
order: 49
status: "draft"
---
SSE connections drop for all sorts of network reasons. Clients automatically try to reconnect, so it's a good idea to make your server resume from where it left off.
## Read `Last-Event-ID`
When the client reconnects, it sends the ID of the last event it received in the `Last-Event-ID` header. The server reads that and picks up from the next one.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
auto last_id = req.get_header_value("Last-Event-ID");
int start = last_id.empty() ? 0 : std::stoi(last_id) + 1;
res.set_chunked_content_provider(
"text/event-stream",
[start](size_t offset, httplib::DataSink &sink) mutable {
static int next_id = 0;
if (next_id < start) { next_id = start; }
std::string msg = "id: " + std::to_string(next_id) + "\n"
+ "data: event " + std::to_string(next_id) + "\n\n";
sink.write(msg.data(), msg.size());
++next_id;
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
On the first connect, `Last-Event-ID` is empty, so start from `0`. On reconnect, resume from the next ID. Event history is the server's responsibility — you need to keep recent events around somewhere.
## Set the reconnect interval
Sending a `retry:` field tells the client how long to wait before reconnecting, in milliseconds.
```cpp
std::string msg = "retry: 5000\n\n"; // reconnect after 5 seconds
sink.write(msg.data(), msg.size());
```
Usually you send this once at the start. During peak load or maintenance windows, a longer retry interval helps reduce reconnect storms.
## Buffer recent events
To support reconnection, keep a rolling buffer of recent events on the server.
```cpp
struct EventBuffer {
std::mutex mu;
std::deque<std::pair<int, std::string>> events; // {id, data}
int next_id = 0;
void push(const std::string &data) {
std::lock_guard<std::mutex> lock(mu);
events.push_back({next_id++, data});
if (events.size() > 1000) { events.pop_front(); }
}
std::vector<std::pair<int, std::string>> since(int id) {
std::lock_guard<std::mutex> lock(mu);
std::vector<std::pair<int, std::string>> out;
for (const auto &e : events) {
if (e.first >= id) { out.push_back(e); }
}
return out;
}
};
```
When a client reconnects, call `since(last_id)` to send any events it missed.
## How much to keep
The buffer size is a tradeoff between memory and how far back a client can resume. It depends on the use case:
- Real-time chat: a few minutes to half an hour
- Notifications: the last N items
- Trading data: persist to a database and pull from there
> **Warning:** `Last-Event-ID` is a client-provided value — don't trust it blindly. If you read it as a number, validate the range. If it's a string, sanitize it.

View File

@@ -0,0 +1,99 @@
---
title: "E04. Receive SSE on the Client"
order: 50
status: "draft"
---
cpp-httplib ships a dedicated `sse::SSEClient` class. It handles auto-reconnect, per-event-name dispatch, and `Last-Event-ID` tracking for you — so receiving SSE is painless.
## Basic usage
```cpp
#include <httplib.h>
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << "data: " << msg.data << std::endl;
});
sse.start(); // blocking
```
Build an `SSEClient` with a `Client` and a path, register a callback with `on_message()`, and call `start()`. The event loop kicks in and automatically reconnects if the connection drops.
## Dispatch by event name
When the server sends events with an `event:` field, register a handler per name via `on_event()`.
```cpp
sse.on_event("message", [](const auto &msg) {
std::cout << "chat: " << msg.data << std::endl;
});
sse.on_event("join", [](const auto &msg) {
std::cout << msg.data << " joined" << std::endl;
});
sse.on_event("leave", [](const auto &msg) {
std::cout << msg.data << " left" << std::endl;
});
```
`on_message()` serves as a generic fallback for unnamed events (the default `message` type).
## Connection lifecycle and errors
```cpp
sse.on_open([] {
std::cout << "connected" << std::endl;
});
sse.on_error([](httplib::Error err) {
std::cerr << "error: " << httplib::to_string(err) << std::endl;
});
```
Hook into connection open and error events. Even when the error handler fires, `SSEClient` keeps trying to reconnect in the background.
## Run asynchronously
If you don't want to block the main thread, use `start_async()`.
```cpp
sse.start_async();
// main thread continues to do other things
do_other_work();
// when you're done, stop it
sse.stop();
```
`start_async()` spawns a background thread to run the event loop. Use `stop()` to shut it down cleanly.
## Configure reconnection
You can tune the reconnect interval and maximum retries.
```cpp
sse.set_reconnect_interval(5000); // 5 seconds
sse.set_max_reconnect_attempts(10); // up to 10 (0 = unlimited)
```
If the server sends a `retry:` field, that takes precedence.
## Automatic Last-Event-ID
`SSEClient` tracks the `id` of each received event internally and sends it back as `Last-Event-ID` on reconnect. As long as the server sends events with `id:`, this all works automatically.
```cpp
std::cout << "last id: " << sse.last_event_id() << std::endl;
```
Use `last_event_id()` to read the current value.
> **Note:** `SSEClient::start()` blocks, which is fine for a one-off command-line tool. For GUI apps or embedded in a server, the `start_async()` + `stop()` pair is the usual pattern.
> For the server side, see [E01. Implement an SSE server](e01-sse-server).

View File

@@ -4,93 +4,93 @@ order: 0
status: "draft"
---
A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need.
A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need. For an introduction to the basics, see the [Tour](../tour/).
## Client
### Basics
- Get the response body as a string / save to a file
- Send and receive JSON
- Set default headers (`set_default_headers`)
- Follow redirects (`set_follow_location`)
- [C01. Get the response body / save to a file](c01-get-response-body)
- [C02. Send and receive JSON](c02-json)
- [C03. Set default headers](c03-default-headers)
- [C04. Follow redirects](c04-follow-location)
### Authentication
- Use Basic authentication (`set_basic_auth`)
- Call an API with a Bearer token
- [C05. Use Basic authentication](c05-basic-auth)
- [C06. Call an API with a Bearer token](c06-bearer-token)
### File Upload
- Upload a file as multipart form data (`make_file_provider`)
- POST a file as raw binary (`make_file_body`)
- Send the body with chunked transfer (Content Provider)
- [C07. Upload a file as multipart form data](c07-multipart-upload)
- [C08. POST a file as raw binary](c08-post-file-body)
- [C09. Send the body with chunked transfer](c09-chunked-upload)
### Streaming & Progress
- Receive a response as a stream
- Use the progress callback (`DownloadProgress` / `UploadProgress`)
- [C10. Receive a response as a stream](c10-stream-response)
- [C11. Use the progress callback](c11-progress-callback)
### Connection & Performance
- Set timeouts (`set_connection_timeout` / `set_read_timeout`)
- Set an overall timeout (`set_max_timeout`)
- Understand connection reuse and Keep-Alive behavior
- Enable compression (`set_compress` / `set_decompress`)
- Send requests through a proxy (`set_proxy`)
- [C12. Set timeouts](c12-timeouts)
- [C13. Set an overall timeout](c13-max-timeout)
- [C14. Understand connection reuse and Keep-Alive behavior](c14-keep-alive)
- [C15. Enable compression](c15-compression)
- [C16. Send requests through a proxy](c16-proxy)
### Error Handling & Debugging
- Handle error codes (`Result::error()`)
- Handle SSL errors (`ssl_error()` / `ssl_backend_error()`)
- Set up client logging (`set_logger` / `set_error_logger`)
- [C17. Handle error codes](c17-error-codes)
- [C18. Handle SSL errors](c18-ssl-errors)
- [C19. Set up client logging](c19-client-logger)
## Server
### Basics
- Register GET / POST / PUT / DELETE handlers
- Receive JSON requests and return JSON responses
- Use path parameters (`/users/:id`)
- Set up a static file server (`set_mount_point`)
- [S01. Register GET / POST / PUT / DELETE handlers](s01-handlers)
- [S02. Receive JSON requests and return JSON responses](s02-json-api)
- [S03. Use path parameters](s03-path-params)
- [S04. Set up a static file server](s04-static-files)
### Streaming & Files
- Stream a large file in the response (`ContentProvider`)
- Return a file download response (`Content-Disposition`)
- Receive multipart data as a stream (`ContentReader`)
- Compress responses (gzip)
- [S05. Stream a large file in the response](s05-stream-response)
- [S06. Return a file download response](s06-download-response)
- [S07. Receive multipart data as a stream](s07-multipart-reader)
- [S08. Return a compressed response](s08-compress-response)
### Handler Chain
- Add pre-processing to all routes (Pre-routing handler)
- Add response headers with a Post-routing handler (CORS, etc.)
- Authenticate per route with a Pre-request handler (`matched_route`)
- Pass data between handlers with `res.user_data`
- [S09. Add pre-processing to all routes](s09-pre-routing)
- [S10. Add response headers with a post-routing handler](s10-post-routing)
- [S11. Authenticate per route with a pre-request handler](s11-pre-request)
- [S12. Pass data between handlers with `res.user_data`](s12-user-data)
### Error Handling & Debugging
- Return custom error pages (`set_error_handler`)
- Catch exceptions (`set_exception_handler`)
- Log requests (Logger)
- Detect client disconnection (`req.is_connection_closed()`)
- [S13. Return custom error pages](s13-error-handler)
- [S14. Catch exceptions](s14-exception-handler)
- [S15. Log requests](s15-server-logger)
- [S16. Detect client disconnection](s16-disconnect)
### Operations & Tuning
- Assign a port dynamically (`bind_to_any_port`)
- Control startup order with `listen_after_bind`
- Shut down gracefully (`stop()` and signal handling)
- Tune Keep-Alive (`set_keep_alive_max_count` / `set_keep_alive_timeout`)
- Configure the thread pool (`new_task_queue`)
- [S17. Bind to any available port](s17-bind-any-port)
- [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind)
- [S19. Shut down gracefully](s19-graceful-shutdown)
- [S20. Tune Keep-Alive](s20-keep-alive)
- [S21. Configure the thread pool](s21-thread-pool)
- [S22. Talk over a Unix domain socket](s22-unix-socket)
## TLS / Security
- Choosing between OpenSSL, mbedTLS, and wolfSSL (build-time `#define` differences)
- Control SSL certificate verification (disable, custom CA, custom callback)
- Use a custom certificate verification callback (`set_server_certificate_verifier`)
- Set up an SSL/TLS server (certificate and private key)
- Configure mTLS (mutual TLS with client certificates)
- Access the peer certificate on the server (`req.peer_cert()` / SNI)
- [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends)
- [T02. Control SSL certificate verification](t02-cert-verification)
- [T03. Start an SSL/TLS server](t03-ssl-server)
- [T04. Configure mTLS](t04-mtls)
- [T05. Access the peer certificate on the server](t05-peer-cert)
## SSE
- Implement an SSE server
- Use event names to distinguish event types
- Handle reconnection (`Last-Event-ID`)
- Receive SSE events on the client
- [E01. Implement an SSE server](e01-sse-server)
- [E02. Use named events in SSE](e02-sse-event-names)
- [E03. Handle SSE reconnection](e03-sse-reconnect)
- [E04. Receive SSE on the client](e04-sse-client)
## WebSocket
- Implement a WebSocket echo server and client
- Configure heartbeats (`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`)
- Handle connection close
- Send and receive binary frames
- [W01. Implement a WebSocket echo server and client](w01-websocket-echo)
- [W02. Set a WebSocket heartbeat](w02-websocket-ping)
- [W03. Handle connection close](w03-websocket-close)
- [W04. Send and receive binary frames](w04-websocket-binary)

View File

@@ -0,0 +1,66 @@
---
title: "S01. Register GET / POST / PUT / DELETE Handlers"
order: 20
status: "draft"
---
With `httplib::Server`, you register a handler per HTTP method. Just pass a pattern and a lambda to `Get()`, `Post()`, `Put()`, or `Delete()`.
## Basic usage
```cpp
#include <httplib.h>
int main() {
httplib::Server svr;
svr.Get("/hello", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Hello, World!", "text/plain");
});
svr.Post("/api/items", [](const httplib::Request &req, httplib::Response &res) {
// req.body holds the request body
res.status = 201;
res.set_content("Created", "text/plain");
});
svr.Put("/api/items/1", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Updated", "text/plain");
});
svr.Delete("/api/items/1", [](const httplib::Request &req, httplib::Response &res) {
res.status = 204;
});
svr.listen("0.0.0.0", 8080);
}
```
Handlers take `(const Request&, Response&)`. Use `res.set_content()` to set the body and Content-Type, and `res.status` for the status code. `listen()` starts the server and blocks.
## Read query parameters
```cpp
svr.Get("/search", [](const httplib::Request &req, httplib::Response &res) {
auto q = req.get_param_value("q");
auto limit = req.get_param_value("limit");
res.set_content("q=" + q + ", limit=" + limit, "text/plain");
});
```
`req.get_param_value()` pulls a value from the query string. Use `req.has_param("q")` if you want to check existence first.
## Read request headers
```cpp
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto ua = req.get_header_value("User-Agent");
res.set_content("UA: " + ua, "text/plain");
});
```
To add a response header, use `res.set_header("Name", "Value")`.
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind).
> To use path parameters like `/users/:id`, see [S03. Use path parameters](s03-path-params).

View File

@@ -0,0 +1,74 @@
---
title: "S02. Receive a JSON Request and Return a JSON Response"
order: 21
status: "draft"
---
cpp-httplib doesn't include a JSON parser. On the server side, combine it with something like [nlohmann/json](https://github.com/nlohmann/json). The examples below use `nlohmann/json`.
## Receive and return JSON
```cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
int main() {
httplib::Server svr;
svr.Post("/api/users", [](const httplib::Request &req, httplib::Response &res) {
try {
auto in = nlohmann::json::parse(req.body);
nlohmann::json out = {
{"id", 42},
{"name", in["name"]},
{"created_at", "2026-04-10T12:00:00Z"},
};
res.status = 201;
res.set_content(out.dump(), "application/json");
} catch (const std::exception &e) {
res.status = 400;
res.set_content("{\"error\":\"invalid json\"}", "application/json");
}
});
svr.listen("0.0.0.0", 8080);
}
```
`req.body` is a plain `std::string`, so you pass it straight to your JSON library. For the response, `dump()` to a string and set the Content-Type to `application/json`.
## Check the Content-Type
```cpp
svr.Post("/api/users", [](const httplib::Request &req, httplib::Response &res) {
auto content_type = req.get_header_value("Content-Type");
if (content_type.find("application/json") == std::string::npos) {
res.status = 415; // Unsupported Media Type
return;
}
// ...
});
```
When you strictly want JSON only, verify the Content-Type up front.
## A helper for JSON responses
If you're writing the same pattern repeatedly, a small helper saves typing.
```cpp
auto send_json = [](httplib::Response &res, int status, const nlohmann::json &j) {
res.status = status;
res.set_content(j.dump(), "application/json");
};
svr.Get("/api/health", [&](const auto &req, auto &res) {
send_json(res, 200, {{"status", "ok"}});
});
```
> **Note:** A large JSON body ends up entirely in `req.body`, which means it all sits in memory. For huge payloads, consider streaming reception — see [S07. Receive multipart data as a stream](s07-multipart-reader).
> For the client side, see [C02. Send and receive JSON](c02-json).

View File

@@ -0,0 +1,53 @@
---
title: "S03. Use Path Parameters"
order: 22
status: "draft"
---
For dynamic URLs like `/users/:id` — the staple of REST APIs — just put `:name` in the path pattern. The matched values end up in `req.path_params`.
## Basic usage
```cpp
svr.Get("/users/:id", [](const httplib::Request &req, httplib::Response &res) {
auto id = req.path_params.at("id");
res.set_content("user id: " + id, "text/plain");
});
```
A request to `/users/42` fills `req.path_params["id"]` with `"42"`. `path_params` is a `std::unordered_map<std::string, std::string>`, so use `at()` to read it.
## Multiple parameters
You can have as many as you need.
```cpp
svr.Get("/orgs/:org/repos/:repo", [](const httplib::Request &req, httplib::Response &res) {
auto org = req.path_params.at("org");
auto repo = req.path_params.at("repo");
res.set_content(org + "/" + repo, "text/plain");
});
```
This matches paths like `/orgs/anthropic/repos/cpp-httplib`.
## Regex patterns
For more flexible matching, use a `std::regex`-based pattern.
```cpp
svr.Get(R"(/users/(\d+))", [](const httplib::Request &req, httplib::Response &res) {
auto id = req.matches[1];
res.set_content("user id: " + std::string(id), "text/plain");
});
```
Parentheses in the pattern become captures in `req.matches`. `req.matches[0]` is the full match; `req.matches[1]` onward are the captures.
## Which to use
- For plain IDs or slugs, `:name` is enough — readable, and the shape is obvious
- Use regex when you want to constrain the URL to, say, numbers only or a UUID format
- Mixing both can get confusing — stick with one style per project
> **Note:** Path parameters come in as strings. If you need an integer, convert with `std::stoi()` and don't forget to handle conversion errors.

View File

@@ -0,0 +1,55 @@
---
title: "S04. Serve Static Files"
order: 23
status: "draft"
---
To serve static files like HTML, CSS, and images, use `set_mount_point()`. Just map a URL path to a local directory, and the whole directory becomes accessible.
## Basic usage
```cpp
httplib::Server svr;
svr.set_mount_point("/", "./public");
svr.listen("0.0.0.0", 8080);
```
`./public/index.html` is now reachable at `http://localhost:8080/index.html`, and `./public/css/style.css` at `http://localhost:8080/css/style.css`. The directory layout maps directly to URLs.
## Multiple mount points
You can register more than one mount point.
```cpp
svr.set_mount_point("/", "./public");
svr.set_mount_point("/assets", "./dist/assets");
svr.set_mount_point("/uploads", "./var/uploads");
```
You can even mount multiple directories at the same path — they're searched in registration order, and the first hit wins.
## Combine with API handlers
Static files and API handlers coexist happily. Handlers registered with `Get()` and friends take priority; the mount points are searched only when nothing matches.
```cpp
svr.Get("/api/users", [](const auto &req, auto &res) {
res.set_content("[]", "application/json");
});
svr.set_mount_point("/", "./public");
```
This gives you an SPA-friendly setup: `/api/*` hits the handlers, everything else is served from `./public/`.
## Add MIME types
cpp-httplib ships with a built-in extension-to-Content-Type map, but you can add your own.
```cpp
svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
```
> **Warning:** The static file server methods are **not thread-safe**. Don't call them after `listen()` — configure everything before starting the server.
> For download-style responses, see [S06. Return a file download response](s06-download-response).

View File

@@ -0,0 +1,63 @@
---
title: "S05. Stream a Large File in the Response"
order: 24
status: "draft"
---
When the response is a huge file or data generated on the fly, loading the whole thing into memory isn't realistic. Use `Response::set_content_provider()` to produce data in chunks as you send it.
## When the size is known
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
size_t total_size = get_file_size("large.bin");
res.set_content_provider(
total_size, "application/octet-stream",
[](size_t offset, size_t length, httplib::DataSink &sink) {
auto data = read_range_from_file("large.bin", offset, length);
sink.write(data.data(), data.size());
return true;
});
});
```
The lambda is called repeatedly with `offset` and `length`. Read just that range and write it to `sink`. Only a small chunk sits in memory at any given time.
## Just send a file
If you only want to serve a file, `set_file_content()` is far simpler.
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
res.set_file_content("large.bin", "application/octet-stream");
});
```
It streams internally, so even huge files are safe. Omit the Content-Type and it's guessed from the extension.
## When the size is unknown — chunked transfer
For data generated on the fly, where you don't know the total size up front, use `set_chunked_content_provider()`. It's sent with HTTP chunked transfer encoding.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, httplib::DataSink &sink) {
auto chunk = produce_next_chunk();
if (chunk.empty()) {
sink.done(); // done sending
return true;
}
sink.write(chunk.data(), chunk.size());
return true;
});
});
```
Call `sink.done()` to signal the end.
> **Note:** The provider lambda is called multiple times. Watch out for the lifetime of captured variables — wrap them in a `std::shared_ptr` if needed.
> To serve the file as a download, see [S06. Return a file download response](s06-download-response).

View File

@@ -0,0 +1,50 @@
---
title: "S06. Return a File Download Response"
order: 25
status: "draft"
---
To force a browser to show a **download dialog** instead of rendering inline, send a `Content-Disposition` header. There's no special cpp-httplib API for this — it's just a header.
## Basic usage
```cpp
svr.Get("/download/report", [](const httplib::Request &req, httplib::Response &res) {
res.set_header("Content-Disposition", "attachment; filename=\"report.pdf\"");
res.set_file_content("reports/2026-04.pdf", "application/pdf");
});
```
`Content-Disposition: attachment` makes the browser pop up a "Save As" dialog. The `filename=` parameter becomes the default save name.
## Non-ASCII file names
For file names with non-ASCII characters or spaces, use the RFC 5987 `filename*` form.
```cpp
svr.Get("/download/report", [](const httplib::Request &req, httplib::Response &res) {
res.set_header(
"Content-Disposition",
"attachment; filename=\"report.pdf\"; "
"filename*=UTF-8''%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88.pdf");
res.set_file_content("reports/2026-04.pdf", "application/pdf");
});
```
The part after `filename*=UTF-8''` is URL-encoded UTF-8. Keep the ASCII `filename=` too, as a fallback for older browsers.
## Download dynamically generated data
You don't need a real file — you can serve a generated string as a download directly.
```cpp
svr.Get("/export.csv", [](const httplib::Request &req, httplib::Response &res) {
std::string csv = build_csv();
res.set_header("Content-Disposition", "attachment; filename=\"export.csv\"");
res.set_content(csv, "text/csv");
});
```
This is the classic pattern for CSV exports.
> **Note:** Some browsers will trigger a download based on Content-Type alone, even without `Content-Disposition`. Conversely, setting `inline` tries to render the content in the browser when possible.

View File

@@ -0,0 +1,71 @@
---
title: "S07. Receive Multipart Data as a Stream"
order: 26
status: "draft"
---
A naive upload handler puts the whole request into `req.body`, which blows up memory for large files. Use `HandlerWithContentReader` to receive the body chunk by chunk.
## Basic usage
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
if (req.is_multipart_form_data()) {
content_reader(
// headers of each part
[&](const httplib::FormData &file) {
std::cout << "name: " << file.name
<< ", filename: " << file.filename << std::endl;
return true;
},
// body of each part (called multiple times)
[&](const char *data, size_t len) {
// write to disk here, for example
return true;
});
} else {
// plain request body
content_reader([&](const char *data, size_t len) {
return true;
});
}
res.set_content("ok", "text/plain");
});
```
The `content_reader` has two call shapes. For multipart data, pass two callbacks (one for headers, one for body). For plain bodies, pass just one.
## Write directly to disk
Here's how to stream an uploaded file to disk.
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
std::ofstream ofs;
content_reader(
[&](const httplib::FormData &file) {
if (!file.filename.empty()) {
ofs.open("uploads/" + file.filename, std::ios::binary);
}
return static_cast<bool>(ofs);
},
[&](const char *data, size_t len) {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
res.set_content("uploaded", "text/plain");
});
```
Only a small chunk sits in memory at any moment, so gigabyte-scale files are no problem.
> **Warning:** When you use `HandlerWithContentReader`, `req.body` stays **empty**. Handle the body yourself inside the callbacks.
> For the client side of multipart uploads, see [C07. Upload a file as multipart form data](c07-multipart-upload).

View File

@@ -0,0 +1,53 @@
---
title: "S08. Return a Compressed Response"
order: 27
status: "draft"
---
cpp-httplib automatically compresses response bodies when the client indicates support via `Accept-Encoding`. The handler doesn't need to do anything special. Supported encodings are gzip, Brotli, and Zstd.
## Build-time setup
To enable compression, define the relevant macros before including `httplib.h`:
```cpp
#define CPPHTTPLIB_ZLIB_SUPPORT // gzip
#define CPPHTTPLIB_BROTLI_SUPPORT // brotli
#define CPPHTTPLIB_ZSTD_SUPPORT // zstd
#include <httplib.h>
```
You'll also need to link `zlib`, `brotli`, and `zstd` respectively. Enable only what you need.
## Usage
```cpp
svr.Get("/api/data", [](const httplib::Request &req, httplib::Response &res) {
std::string body = build_large_response();
res.set_content(body, "application/json");
});
```
That's it. If the client sent `Accept-Encoding: gzip`, cpp-httplib compresses the response with gzip automatically. `Content-Encoding: gzip` and `Vary: Accept-Encoding` are added for you.
## Encoding priority
When the client accepts multiple encodings, cpp-httplib picks in this order (among those enabled at build time): Brotli → Zstd → gzip. Your code doesn't need to care — you always get the most efficient option available.
## Streaming responses are compressed too
Streaming responses via `set_chunked_content_provider()` get the same automatic compression.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, httplib::DataSink &sink) {
// ...
});
});
```
> **Note:** Tiny responses barely benefit from compression and just waste CPU time. cpp-httplib skips compression for bodies that are too small to bother with.
> For the client-side counterpart, see [C15. Enable compression](c15-compression).

View File

@@ -0,0 +1,54 @@
---
title: "S09. Add Pre-Processing to All Routes"
order: 28
status: "draft"
---
Sometimes you want the same logic to run before every request — auth checks, logging, rate limiting. Register those with `set_pre_routing_handler()`.
## Basic usage
```cpp
svr.set_pre_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
std::cout << req.method << " " << req.path << std::endl;
return httplib::Server::HandlerResponse::Unhandled;
});
```
The pre-routing handler runs **before routing**. It catches every request — including ones that don't match any handler.
The `HandlerResponse` return value is key:
- Return `Unhandled` → continue normally (routing and the actual handler run)
- Return `Handled` → the response is considered complete, skip the rest
## Use it for authentication
Put your shared auth check in one place.
```cpp
svr.set_pre_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
if (req.path.rfind("/public", 0) == 0) {
return httplib::Server::HandlerResponse::Unhandled; // no auth needed
}
auto auth = req.get_header_value("Authorization");
if (auth.empty()) {
res.status = 401;
res.set_content("unauthorized", "text/plain");
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
If auth fails, return `Handled` to respond with 401 immediately. If it passes, return `Unhandled` and let routing take over.
## For per-route auth
If you want different auth rules per route rather than a single global check, `set_pre_request_handler()` is a better fit. See [S11. Authenticate per route with a pre-request handler](s11-pre-request).
> **Note:** If all you want is to modify the response, `set_post_routing_handler()` is the right tool. See [S10. Add response headers with a post-routing handler](s10-post-routing).

View File

@@ -0,0 +1,56 @@
---
title: "S10. Add Response Headers with a Post-Routing Handler"
order: 29
status: "draft"
---
Sometimes you want to add shared headers to the response after the handler has run — CORS headers, security headers, a request ID, and so on. That's what `set_post_routing_handler()` is for.
## Basic usage
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Request-ID", generate_request_id());
});
```
The post-routing handler runs **after the route handler, before the response is sent**. From here you can call `res.set_header()` or `res.headers.erase()` to add or remove headers across every response in one place.
## Add CORS headers
CORS is a classic use case.
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
});
```
For the preflight `OPTIONS` requests, register a separate handler — or handle them in the pre-routing handler.
```cpp
svr.Options("/.*", [](const auto &req, auto &res) {
res.status = 204;
});
```
## Bundle your security headers
Manage browser security headers in one spot.
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Content-Type-Options", "nosniff");
res.set_header("X-Frame-Options", "DENY");
res.set_header("Referrer-Policy", "strict-origin-when-cross-origin");
});
```
No matter which handler produced the response, the same headers get attached.
> **Note:** The post-routing handler also runs for responses that didn't match any route and for responses from error handlers. That's exactly what you want when you need certain headers on every response, guaranteed.

View File

@@ -0,0 +1,49 @@
---
title: "S11. Authenticate Per Route with a Pre-Request Handler"
order: 30
status: "draft"
---
The `set_pre_routing_handler()` from [S09. Add pre-processing to all routes](s09-pre-routing) runs **before routing**, so it has no idea which route matched. When you want per-route behavior, `set_pre_request_handler()` is what you need.
## Pre-routing vs. pre-request
| Hook | When it runs | Route info | Request body |
| --- | --- | --- | --- |
| `set_pre_routing_handler` | Before routing | Not available | Not read yet |
| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` | Not read yet |
In a pre-request handler, `req.matched_route` holds the **pattern string** that matched. You can vary behavior based on the route definition itself.
Because the body has not been read when the pre-request handler runs, you can reject a request — for example on a failed auth check — without consuming a (potentially large) request body. Note that this also means `req.body` and form fields parsed from the body are not available here; inspect headers, the path, query parameters, or `req.matched_route` instead.
## Switch auth per route
```cpp
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
// require auth for routes starting with /admin
if (req.matched_route.rfind("/admin", 0) == 0) {
auto token = req.get_header_value("Authorization");
if (!is_admin_token(token)) {
res.status = 403;
res.set_content("forbidden", "text/plain");
return httplib::Server::HandlerResponse::Handled;
}
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
## Return values
Same as pre-routing — return `HandlerResponse`.
- `Unhandled`: continue (the route handler runs)
- `Handled`: we're done, skip the route handler
## Passing auth info to the route handler
To pass decoded user info into the route handler, use `res.user_data`. See [S12. Pass data between handlers with `res.user_data`](s12-user-data).

View File

@@ -0,0 +1,56 @@
---
title: "S12. Pass Data Between Handlers with res.user_data"
order: 31
status: "draft"
---
Say your pre-request handler decodes an auth token and you want the route handler to use the result. That "data handoff between handlers" is what `res.user_data` is for — it holds values of arbitrary types.
## Basic usage
```cpp
struct AuthUser {
std::string id;
std::string name;
bool is_admin;
};
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
auto token = req.get_header_value("Authorization");
auto user = decode_token(token); // decode the auth token
res.user_data.set("user", user);
return httplib::Server::HandlerResponse::Unhandled;
});
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto *user = res.user_data.get<AuthUser>("user");
if (!user) {
res.status = 401;
return;
}
res.set_content("Hello, " + user->name, "text/plain");
});
```
`user_data.set()` stores a value of any type, and `user_data.get<T>()` retrieves it. If you give the wrong type you get `nullptr` back — so be careful.
## Typical value types
Strings, numbers, structs, `std::shared_ptr` — anything copyable or movable works.
```cpp
res.user_data.set("user_id", std::string{"42"});
res.user_data.set("is_admin", true);
res.user_data.set("started_at", std::chrono::steady_clock::now());
```
## Where to set, where to read
The usual flow is: set it in `set_pre_routing_handler()` or `set_pre_request_handler()`, read it in the route handler. Pre-request runs after routing, so you can combine it with `req.matched_route` to set values only for specific routes.
## A gotcha
`user_data` lives on `Response`, not `Request`. That's because handlers get `Response&` (mutable) but only `const Request&`. It looks odd at first, but it makes sense once you think of it as "the mutable context shared between handlers."
> **Warning:** `user_data.get<T>()` returns `nullptr` when the type doesn't match. Use the exact same type on set and get. Storing as `AuthUser` and fetching as `const AuthUser` won't work.

View File

@@ -0,0 +1,51 @@
---
title: "S13. Return a Custom Error Page"
order: 32
status: "draft"
---
To customize the response for 4xx or 5xx errors, use `set_error_handler()`. You can replace the plain default error page with your own HTML or JSON.
## Basic usage
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
auto body = "<h1>Error " + std::to_string(res.status) + "</h1>";
res.set_content(body, "text/html");
});
```
The error handler runs right before an error response is sent — any time `res.status` is 4xx or 5xx. Replace the body with `res.set_content()` and every error response uses the same template.
## Branch by status code
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
if (res.status == 404) {
res.set_content("<h1>Not Found</h1><p>" + req.path + "</p>", "text/html");
} else if (res.status >= 500) {
res.set_content("<h1>Server Error</h1>", "text/html");
}
});
```
Checking `res.status` lets you show a custom message for 404s and a "contact support" link for 5xx errors.
## JSON error responses
For an API server, you probably want errors as JSON.
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
nlohmann::json j = {
{"error", true},
{"status", res.status},
{"path", req.path},
};
res.set_content(j.dump(), "application/json");
});
```
Now every error comes back in a consistent JSON shape.
> **Note:** `set_error_handler()` also fires for 500 responses caused by exceptions thrown from a route handler. To get at the exception itself, combine it with `set_exception_handler()`. See [S14. Catch exceptions](s14-exception-handler).

View File

@@ -0,0 +1,67 @@
---
title: "S14. Catch Exceptions"
order: 33
status: "draft"
---
When a route handler throws, cpp-httplib keeps the server running and responds with 500. By default, though, very little of the error information reaches the client. `set_exception_handler()` lets you intercept exceptions and build your own response.
## Basic usage
```cpp
svr.set_exception_handler(
[](const httplib::Request &req, httplib::Response &res,
std::exception_ptr ep) {
try {
std::rethrow_exception(ep);
} catch (const std::exception &e) {
res.status = 500;
res.set_content(std::string("error: ") + e.what(), "text/plain");
} catch (...) {
res.status = 500;
res.set_content("unknown error", "text/plain");
}
});
```
The handler receives a `std::exception_ptr`. The idiomatic move is to rethrow it with `std::rethrow_exception()` and catch by type. You can vary status code and message based on the exception type.
## Branch on custom exception types
If you throw your own exception types, you can map them to 400 or 404 responses.
```cpp
struct NotFound : std::runtime_error {
using std::runtime_error::runtime_error;
};
struct BadRequest : std::runtime_error {
using std::runtime_error::runtime_error;
};
svr.set_exception_handler(
[](const auto &req, auto &res, std::exception_ptr ep) {
try {
std::rethrow_exception(ep);
} catch (const NotFound &e) {
res.status = 404;
res.set_content(e.what(), "text/plain");
} catch (const BadRequest &e) {
res.status = 400;
res.set_content(e.what(), "text/plain");
} catch (const std::exception &e) {
res.status = 500;
res.set_content("internal error", "text/plain");
}
});
```
Now throwing `NotFound("user not found")` inside a handler is enough to return 404. No per-handler try/catch needed.
## Relationship with set_error_handler
`set_exception_handler()` runs the moment the exception is thrown. After that, if `res.status` is 4xx or 5xx, `set_error_handler()` also runs. The order is `exception_handler``error_handler`. Think of their roles as:
- **Exception handler**: interpret the exception, set the status and message
- **Error handler**: see the status and wrap it in the shared template
> **Note:** Without an exception handler, cpp-httplib returns a default 500 response and the exception details never make it to logs. Always set one for anything you want to debug.

View File

@@ -0,0 +1,64 @@
---
title: "S15. Log Requests on the Server"
order: 34
status: "draft"
---
To log the requests the server receives and the responses it returns, use `Server::set_logger()`. The callback fires once per completed request, making it the foundation for access logs and metrics collection.
## Basic usage
```cpp
svr.set_logger([](const httplib::Request &req, const httplib::Response &res) {
std::cout << req.remote_addr << " "
<< req.method << " " << req.path
<< " -> " << res.status << std::endl;
});
```
The log callback receives both the `Request` and the `Response`. You can grab the method, path, status, client IP, headers, body — whatever you need.
## Access-log style format
Here's an Apache/Nginx-ish access log format.
```cpp
svr.set_logger([](const auto &req, const auto &res) {
auto now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
std::localtime(&now));
std::cout << timebuf << " "
<< req.remote_addr << " "
<< "\"" << req.method << " " << req.path << "\" "
<< res.status << " "
<< res.body.size() << "B"
<< std::endl;
});
```
## Measure request time
To include request duration in the log, stash a start timestamp in `res.user_data` from a pre-routing handler, then subtract in the logger.
```cpp
svr.set_pre_routing_handler([](const auto &req, auto &res) {
res.user_data.set("start", std::chrono::steady_clock::now());
return httplib::Server::HandlerResponse::Unhandled;
});
svr.set_logger([](const auto &req, const auto &res) {
auto *start = res.user_data.get<std::chrono::steady_clock::time_point>("start");
auto elapsed = start
? std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - *start).count()
: 0;
std::cout << req.method << " " << req.path
<< " " << res.status << " " << elapsed << "ms" << std::endl;
});
```
For more on `user_data`, see [S12. Pass data between handlers with `res.user_data`](s12-user-data).
> **Note:** The logger runs synchronously on the same thread as request processing. Heavy work inside it hurts throughput — push it to a queue and process asynchronously if you need anything expensive.

View File

@@ -0,0 +1,55 @@
---
title: "S16. Detect When the Client Has Disconnected"
order: 35
status: "draft"
---
During a long-running response, the client might close the connection. There's no point continuing to do work no one's waiting for. In cpp-httplib, check `req.is_connection_closed()`.
## Basic usage
```cpp
svr.Get("/long-task", [](const httplib::Request &req, httplib::Response &res) {
for (int i = 0; i < 1000; ++i) {
if (req.is_connection_closed()) {
std::cout << "client disconnected" << std::endl;
return;
}
do_heavy_work(i);
}
res.set_content("done", "text/plain");
});
```
`is_connection_closed` is a `std::function<bool()>`, so call it with `()`. It returns `true` when the client is gone.
## With a streaming response
The same check works inside `set_chunked_content_provider()`. Capture the request by reference.
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&req](size_t offset, httplib::DataSink &sink) {
if (req.is_connection_closed()) {
sink.done();
return true;
}
auto event = generate_next_event();
sink.write(event.data(), event.size());
return true;
});
});
```
When you detect a disconnect, call `sink.done()` to stop the provider from being called again.
## How often should you check?
The call itself is cheap, but calling it in a tight inner loop doesn't add much value. Check at **boundaries where interrupting is safe** — after producing a chunk, after a database query, etc.
> **Warning:** `is_connection_closed()` is not guaranteed to reflect reality instantly. Because of how TCP works, sometimes you only notice the disconnect when you try to send. Don't expect pixel-perfect real-time detection — think of it as "we'll notice eventually."

View File

@@ -0,0 +1,52 @@
---
title: "S17. Bind to Any Available Port"
order: 36
status: "draft"
---
Standing up a test server often hits port conflicts. With `bind_to_any_port()`, you let the OS pick a free port and then read back which one it gave you.
## Basic usage
```cpp
httplib::Server svr;
svr.Get("/", [](const auto &req, auto &res) {
res.set_content("hello", "text/plain");
});
int port = svr.bind_to_any_port("0.0.0.0");
std::cout << "listening on port " << port << std::endl;
svr.listen_after_bind();
```
`bind_to_any_port()` is equivalent to passing `0` as the port — the OS assigns a free one. The return value is the port actually used.
After that, call `listen_after_bind()` to start accepting. You can't combine bind and listen into a single call here, so you work in two steps.
## Useful in tests
This pattern is great for tests that spin up a server and hit it.
```cpp
httplib::Server svr;
svr.Get("/ping", [](const auto &, auto &res) { res.set_content("pong", "text/plain"); });
int port = svr.bind_to_any_port("127.0.0.1");
std::thread t([&] { svr.listen_after_bind(); });
// run the test while the server is up on another thread
httplib::Client cli("127.0.0.1", port);
auto res = cli.Get("/ping");
assert(res && res->body == "pong");
svr.stop();
t.join();
```
Because the port is assigned at runtime, parallel test runs don't collide.
> **Note:** `bind_to_any_port()` returns `-1` on failure (permission errors, no available ports, etc.). Always check the return value.
> To stop the server, see [S19. Shut down gracefully](s19-graceful-shutdown).

View File

@@ -0,0 +1,57 @@
---
title: "S18. Control Startup Order with listen_after_bind"
order: 37
status: "draft"
---
Normally `svr.listen("0.0.0.0", 8080)` handles bind and listen in one shot. When you need to do something between the two, split them into two calls.
## Separate bind and listen
```cpp
httplib::Server svr;
svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "bind failed" << std::endl;
return 1;
}
// bind is done here. accept hasn't started yet.
drop_privileges();
signal_ready_to_parent_process();
svr.listen_after_bind(); // start the accept loop
```
`bind_to_port()` reserves the port; `listen_after_bind()` actually starts accepting. Splitting them gives you a window between the two steps.
## Common use cases
**Privilege drop**: Binding to a port under 1024 requires root. Bind as root, drop to a normal user, and all subsequent request handling runs with reduced privileges.
```cpp
svr.bind_to_port("0.0.0.0", 80);
drop_privileges();
svr.listen_after_bind();
```
**Startup notification**: Tell the parent process or systemd "I'm ready" before starting to accept connections.
**Test synchronization**: In tests, you can reliably catch "the moment the server is bound" and start the client after that.
## Check the return values
`bind_to_port()` returns `false` on failure — typically when the port is already taken. Always check it.
```cpp
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "port already in use" << std::endl;
return 1;
}
```
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.

View File

@@ -0,0 +1,57 @@
---
title: "S19. Shut Down Gracefully"
order: 38
status: "draft"
---
To stop the server, call `Server::stop()`. It's safe to call even while requests are in flight, so you can wire it to SIGINT or SIGTERM for a graceful shutdown.
## Basic usage
```cpp
httplib::Server svr;
svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });
std::thread t([&] { svr.listen("0.0.0.0", 8080); });
// wait for input on the main thread, or whatever
std::cin.get();
svr.stop();
t.join();
```
`listen()` blocks, so the typical pattern is: run the server on a background thread and call `stop()` from the main thread. After `stop()`, `listen()` returns and you can `join()`.
## Shut down on a signal
Here's how to stop the server on SIGINT (Ctrl+C) or SIGTERM.
```cpp
#include <csignal>
httplib::Server svr;
// global so the signal handler can reach it
httplib::Server *g_svr = nullptr;
int main() {
svr.Get("/", [](const auto &, auto &res) { res.set_content("ok", "text/plain"); });
g_svr = &svr;
std::signal(SIGINT, [](int) { if (g_svr) g_svr->stop(); });
std::signal(SIGTERM, [](int) { if (g_svr) g_svr->stop(); });
svr.listen("0.0.0.0", 8080);
std::cout << "server stopped" << std::endl;
}
```
`stop()` is thread-safe and signal-safe — you can call it from a signal handler. Even when `listen()` is running on the main thread, the signal pulls it out cleanly.
## What happens to in-flight requests
When you call `stop()`, new connections are refused, but requests already being processed are **allowed to finish**. Once all workers drain, `listen()` returns. That's what makes it graceful.
> **Warning:** There's a wait between calling `stop()` and `listen()` returning — it's the time in-flight requests take to finish. To enforce a timeout, you'll need to add your own shutdown timer in application code.

View File

@@ -0,0 +1,57 @@
---
title: "S20. Tune Keep-Alive"
order: 39
status: "draft"
---
`httplib::Server` enables HTTP/1.1 Keep-Alive automatically. From the client's perspective, connections are reused — so they don't pay the TCP handshake cost on every request. When you need to tune the behavior, there are two setters.
## What you can configure
| API | Default | Meaning |
| --- | --- | --- |
| `set_keep_alive_max_count` | 100 | Max requests served over a single connection |
| `set_keep_alive_timeout` | 5s | How long an idle connection is kept before closing |
## Basic usage
```cpp
httplib::Server svr;
svr.set_keep_alive_max_count(20);
svr.set_keep_alive_timeout(10); // 10 seconds
svr.listen("0.0.0.0", 8080);
```
`set_keep_alive_timeout()` also has a `std::chrono` overload.
```cpp
using namespace std::chrono_literals;
svr.set_keep_alive_timeout(10s);
```
## Tuning ideas
**Too many idle connections eating resources**
Shorten the timeout so idle connections drop and release their worker threads.
```cpp
svr.set_keep_alive_timeout(2s);
```
**API is hammered and you want max reuse**
Raising the per-connection request cap improves benchmark numbers.
```cpp
svr.set_keep_alive_max_count(1000);
```
**Never reuse connections**
Set `set_keep_alive_max_count(1)` and every request gets its own connection. Mostly only useful for debugging or compatibility testing.
## Relationship with the thread pool
A Keep-Alive connection holds a worker thread for its entire lifetime. If `connections × concurrent requests` exceeds the thread pool size, new requests wait. For thread counts, see [S21. Configure the thread pool](s21-thread-pool).
> **Note:** For the client side, see [C14. Understand connection reuse and Keep-Alive behavior](c14-keep-alive). Even when the server closes the connection on timeout, the client reconnects automatically.

View File

@@ -0,0 +1,69 @@
---
title: "S21. Configure the Thread Pool"
order: 40
status: "draft"
---
cpp-httplib serves requests from a thread pool. By default, the base thread count is the greater of `std::thread::hardware_concurrency() - 1` and `8`, and it can scale up dynamically to 4× that. To set thread counts explicitly, provide your own factory via `new_task_queue`.
## Set thread counts
```cpp
httplib::Server svr;
svr.new_task_queue = [] {
return new httplib::ThreadPool(/*base_threads=*/8, /*max_threads=*/64);
};
svr.listen("0.0.0.0", 8080);
```
The factory is a lambda returning a `TaskQueue*`. Pass `base_threads` and `max_threads` to `ThreadPool` and the pool scales between them based on load. Idle threads exit after a timeout (3 seconds by default).
## Also cap the queue
The pending queue can eat memory if it grows unchecked. You can cap it too.
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(
/*base_threads=*/12,
/*max_threads=*/0, // disable dynamic scaling
/*max_queued_requests=*/18);
};
```
`max_threads=0` disables dynamic scaling — you get a fixed `base_threads`. Requests that don't fit in `max_queued_requests` are rejected.
## Use your own thread pool
You can plug in a fully custom thread pool by subclassing `TaskQueue` and returning it from the factory.
```cpp
class MyTaskQueue : public httplib::TaskQueue {
public:
MyTaskQueue(size_t n) { pool_.start_with_thread_count(n); }
bool enqueue(std::function<void()> fn) override { return pool_.post(std::move(fn)); }
void shutdown() override { pool_.shutdown(); }
private:
MyThreadPool pool_;
};
svr.new_task_queue = [] { return new MyTaskQueue(12); };
```
Handy when you already have a thread pool in your project and want to keep thread management unified.
## Compile-time tuning
You can set the defaults with macros if you want compile-time configuration.
```cpp
#define CPPHTTPLIB_THREAD_POOL_COUNT 16 // base thread count
#define CPPHTTPLIB_THREAD_POOL_MAX_COUNT 128 // max thread count
#define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 5 // seconds before idle threads exit
#include <httplib.h>
```
> **Note:** A WebSocket connection holds a worker thread for its entire lifetime. For lots of simultaneous WebSocket connections, enable dynamic scaling (e.g. `ThreadPool(8, 64)`).

View File

@@ -0,0 +1,64 @@
---
title: "S22. Talk Over a Unix Domain Socket"
order: 41
status: "draft"
---
When you want to talk only to other processes on the same host, a Unix domain socket is a nice fit. It avoids TCP overhead and uses filesystem permissions for access control. Local IPC and services sitting behind a reverse proxy are classic use cases.
## Server side
```cpp
httplib::Server svr;
svr.set_address_family(AF_UNIX);
svr.Get("/", [](const auto &, auto &res) {
res.set_content("hello from unix socket", "text/plain");
});
svr.listen("/tmp/httplib.sock", 80);
```
Call `set_address_family(AF_UNIX)` first, then pass the socket file path as the first argument to `listen()`. The port number is unused but required by the signature — pass any value.
## Client side
```cpp
httplib::Client cli("/tmp/httplib.sock");
cli.set_address_family(AF_UNIX);
auto res = cli.Get("/");
if (res) {
std::cout << res->body << std::endl;
}
```
Pass the socket file path to the `Client` constructor and call `set_address_family(AF_UNIX)`. Everything else works like a normal HTTP request.
## When to use it
- **Behind a reverse proxy**: An nginx-to-backend setup over a Unix socket is faster than TCP and sidesteps port management
- **Local-only APIs**: IPC between tools that shouldn't be reachable from outside
- **In-container IPC**: Process-to-process communication within the same pod or container
- **Dev environments**: No more worrying about port conflicts
## Clean up the socket file
A Unix domain socket creates a real file in the filesystem. It doesn't get removed on shutdown, so delete it before starting if needed.
```cpp
std::remove("/tmp/httplib.sock");
svr.listen("/tmp/httplib.sock", 80);
```
## Permissions
You control who can connect via the socket file's permissions.
```cpp
svr.listen("/tmp/httplib.sock", 80);
// from another process or thread
chmod("/tmp/httplib.sock", 0660); // owner and group only
```
> **Warning:** Some Windows versions support AF_UNIX, but the implementation and behavior differ by platform. Test thoroughly before running cross-platform in production.

View File

@@ -0,0 +1,49 @@
---
title: "T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL"
order: 42
status: "draft"
---
cpp-httplib doesn't ship its own TLS implementation — it uses one of three backends that you pick at build time via a macro.
| Backend | Macro | Character |
| --- | --- | --- |
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | Most widely used, richest feature set |
| mbedTLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | Lightweight, aimed at embedded |
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | Embedded-friendly, commercial support available |
## Build-time selection
Define the macro for your chosen backend before including `httplib.h`:
```cpp
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>
```
You'll also need to link against the backend's libraries (`libssl`, `libcrypto`, `libmbedtls`, `libwolfssl`, etc.).
## Which to pick
**When in doubt, OpenSSL**
It has the most features and the best documentation. For normal server use or Linux desktop apps, start here — you probably won't need anything else.
**To shrink binary size or target embedded**
mbedTLS or wolfSSL are a better fit. They're far more compact than OpenSSL and run on memory-constrained devices.
**When you need commercial support**
wolfSSL offers commercial licensing and support. If you're shipping in a product, it's worth considering.
## Supporting multiple backends
The usual approach is to treat each backend as a build variant and recompile the same source with different macros. cpp-httplib smooths over most of the API differences, but the backends are not 100% identical — always test.
## APIs that work across all backends
Certificate verification control, standing up an SSLServer, reading the peer certificate — these all share the same API across backends:
- [T02. Control SSL certificate verification](t02-cert-verification)
- [T03. Start an SSL/TLS server](t03-ssl-server)
- [T05. Access the peer certificate on the server](t05-peer-cert)
> **Note:** On macOS with an OpenSSL-family backend, cpp-httplib automatically loads root certificates from the system keychain (via `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`, on by default). To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.

View File

@@ -0,0 +1,53 @@
---
title: "T02. Control SSL Certificate Verification"
order: 43
status: "draft"
---
By default, an HTTPS client verifies the server certificate — it uses the OS root certificate store to check the chain and the hostname. Here are the APIs for changing that behavior.
## Specify a custom CA certificate
When connecting to a server whose certificate is signed by an internal CA, use `set_ca_cert_path()`.
```cpp
httplib::Client cli("https://internal.example.com");
cli.set_ca_cert_path("/etc/ssl/certs/internal-ca.pem");
auto res = cli.Get("/");
```
The first argument is the CA certificate file; the second is an optional CA directory. With the OpenSSL backend, you can also pass an `X509_STORE*` directly via `set_ca_cert_store()`.
## Disable certificate verification (not recommended)
For development servers or self-signed certificates, you can skip verification entirely.
```cpp
httplib::Client cli("https://self-signed.example.com");
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/");
```
That's all it takes to disable chain verification.
> **Warning:** Disabling certificate verification removes protection against man-in-the-middle attacks. **Never do this in production.** If you find yourself needing it outside of dev/test, pause and make sure you're not doing something wrong.
## Disable hostname verification only
There's an in-between option: verify the certificate chain, but skip the hostname check. Useful when you need to reach a server whose cert CN/SAN doesn't match the request's hostname.
```cpp
cli.enable_server_hostname_verification(false);
```
The certificate itself is still validated, so this is safer than fully disabling verification — but still not recommended in production.
## Use the OS cert store as-is
On most Linux distributions, root certificates live in a single file like `/etc/ssl/certs/ca-certificates.crt`. cpp-httplib reads the OS default store at startup, so for most servers you don't need to configure anything.
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends).
> For details on diagnosing failures, see [C18. Handle SSL errors](c18-ssl-errors).

View File

@@ -0,0 +1,78 @@
---
title: "T03. Start an SSL/TLS Server"
order: 44
status: "draft"
---
To stand up an HTTPS server, use `httplib::SSLServer` instead of `httplib::Server`. Pass a certificate and private key to the constructor, and you get back something that works exactly like `Server`.
## Basic usage
```cpp
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>
int main() {
httplib::SSLServer svr("cert.pem", "key.pem");
svr.Get("/", [](const auto &req, auto &res) {
res.set_content("hello over TLS", "text/plain");
});
svr.listen("0.0.0.0", 443);
}
```
Pass the server certificate (PEM format) and private key file paths to the constructor. That's all you need for a TLS-enabled server. Registering handlers and calling `listen()` work the same as with `Server`.
## Password-protected private keys
The fifth argument is the private key password.
```cpp
httplib::SSLServer svr("cert.pem", "key.pem",
nullptr, nullptr, "password");
```
The third and fourth arguments are for client certificate verification (mTLS, see [T04. Configure mTLS](t04-mtls)). For now, pass `nullptr`.
## Load PEM data from memory
When you want to load certs from memory instead of files, use the `PemMemory` struct.
```cpp
httplib::SSLServer::PemMemory pem{};
pem.cert_pem = cert_data.data();
pem.cert_pem_len = cert_data.size();
pem.key_pem = key_data.data();
pem.key_pem_len = key_data.size();
httplib::SSLServer svr(pem);
```
Handy when you pull certificates from environment variables or a secrets manager.
## Rotate certificates
Before a certificate expires, you may want to swap it out without restarting the server. That's what `update_certs_pem()` is for.
```cpp
svr.update_certs_pem(new_cert_pem, new_key_pem);
```
Existing connections keep using the old cert; new connections use the new one.
## Generating a test certificate
For a throwaway self-signed cert, use the `openssl` CLI.
```sh
openssl req -x509 -newkey rsa:2048 -days 365 -nodes \
-keyout key.pem -out cert.pem -subj "/CN=localhost"
```
In production, use certificates from Let's Encrypt or your internal CA.
> **Warning:** Binding an HTTPS server to port 443 requires root. For a safe way to do that, see the privilege-drop pattern in [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind).
> For mutual TLS (client certificates), see [T04. Configure mTLS](t04-mtls).

View File

@@ -0,0 +1,70 @@
---
title: "T04. Configure mTLS"
order: 45
status: "draft"
---
Regular TLS verifies the server certificate only. **mTLS** (mutual TLS) adds the other direction: the client presents a certificate too, and the server verifies it. It's common for zero-trust API-to-API traffic and internal system authentication.
## Server side
Pass the CA used to verify client certificates as the third (and fourth) argument to `SSLServer`.
```cpp
httplib::SSLServer svr(
"server-cert.pem", // server certificate
"server-key.pem", // server private key
"client-ca.pem", // CA that signs valid client certs
nullptr // CA directory (none)
);
svr.Get("/", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("authenticated", "text/plain");
});
svr.listen("0.0.0.0", 443);
```
With this, any connection whose client certificate isn't signed by `client-ca.pem` is rejected at the handshake. By the time a handler runs, the client is already authenticated.
## Configure with in-memory PEM
```cpp
httplib::SSLServer::PemMemory pem{};
pem.cert_pem = server_cert.data();
pem.cert_pem_len = server_cert.size();
pem.key_pem = server_key.data();
pem.key_pem_len = server_key.size();
pem.client_ca_pem = client_ca.data();
pem.client_ca_pem_len = client_ca.size();
httplib::SSLServer svr(pem);
```
This is the clean way when you load certificates from environment variables or a secrets manager.
## Client side
On the client side, pass the client certificate and key to `SSLClient`.
```cpp
httplib::SSLClient cli("api.example.com", 443,
"client-cert.pem",
"client-key.pem");
auto res = cli.Get("/");
```
Note you're using `SSLClient` directly, not `Client`. If the private key has a password, pass it as the fifth argument.
## Read client info from a handler
To see which client connected from inside a handler, use `req.peer_cert()`. Details in [T05. Access the peer certificate on the server](t05-peer-cert).
## Use cases
- **Microservice-to-microservice calls**: Issue a cert per service, use the cert as identity
- **IoT device management**: Burn a cert into each device and use it to gate API access
- **An alternative to internal VPN**: Put cert-based auth in front of public endpoints so internal resources can be reached safely
> **Note:** Issuing and revoking client certificates is more operational work than password-based auth. You'll need either an internal PKI setup or an automated flow using ACME-family tools.

View File

@@ -0,0 +1,88 @@
---
title: "T05. Access the Peer Certificate on the Server Side"
order: 46
status: "draft"
---
In an mTLS setup, you can read the client's certificate from inside a handler. Pull out the CN or SAN to identify the user or log the request.
## Basic usage
```cpp
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto cert = req.peer_cert();
if (!cert) {
res.status = 401;
res.set_content("no client certificate", "text/plain");
return;
}
auto cn = cert.subject_cn();
res.set_content("hello, " + cn, "text/plain");
});
```
`req.peer_cert()` returns a `tls::PeerCert`. It's convertible to `bool`, so check whether a cert is present before using it.
## Available fields
From a `PeerCert`, you can get:
```cpp
auto cert = req.peer_cert();
std::string cn = cert.subject_cn(); // CN
std::string issuer = cert.issuer_name(); // issuer
std::string serial = cert.serial(); // serial number
time_t not_before, not_after;
cert.validity(not_before, not_after); // validity period
auto sans = cert.sans(); // SANs
for (const auto &san : sans) {
std::cout << san.value << std::endl;
}
```
There's also a helper to check if a hostname is covered by the SAN list:
```cpp
if (cert.check_hostname("alice.corp.example.com")) {
// matches
}
```
## Cert-based authorization
You can gate routes by CN or SAN.
```cpp
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
auto cert = req.peer_cert();
if (!cert) {
res.status = 401;
return httplib::Server::HandlerResponse::Handled;
}
if (req.matched_route.rfind("/admin", 0) == 0) {
auto cn = cert.subject_cn();
if (!is_admin_cn(cn)) {
res.status = 403;
return httplib::Server::HandlerResponse::Handled;
}
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
Combined with a pre-request handler, you can keep all authorization logic in one place. See [S11. Authenticate per route with a pre-request handler](s11-pre-request).
## SNI (Server Name Indication)
cpp-httplib handles SNI automatically. If one server hosts multiple domains, SNI is used under the hood — but normally handlers don't need to care.
> **Warning:** `req.peer_cert()` only returns a meaningful value when mTLS is enabled and the client actually presented a certificate. For plain TLS, you get an empty `PeerCert`. Always do the `bool` check before using it.
> To set up mTLS, see [T04. Configure mTLS](t04-mtls).

View File

@@ -0,0 +1,88 @@
---
title: "W01. Implement a WebSocket Echo Server and Client"
order: 51
status: "draft"
---
WebSocket is a protocol for **two-way** messaging between client and server. cpp-httplib provides APIs for both sides. Let's start with the simplest example: an echo server.
## Server: echo server
```cpp
#include <httplib.h>
int main() {
httplib::Server svr;
svr.WebSocket("/echo", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.is_open()) {
auto result = ws.read(msg);
if (result == httplib::ws::ReadResult::Fail) {
break;
}
ws.send(msg); // echo back what we received
}
});
svr.listen("0.0.0.0", 8080);
}
```
Register a WebSocket handler with `svr.WebSocket()`. By the time the handler runs, the WebSocket handshake is already complete. Inside the loop, just `ws.read()` and `ws.send()` to get a working echo.
The `read()` return value is a `ReadResult` enum:
- `ReadResult::Text`: received a text message
- `ReadResult::Binary`: received a binary message
- `ReadResult::Fail`: error, or connection closed
## Client: talk to the echo server
```cpp
#include <httplib.h>
int main() {
httplib::ws::WebSocketClient cli("ws://localhost:8080/echo");
if (!cli.connect()) {
std::cerr << "failed to connect" << std::endl;
return 1;
}
cli.send("Hello, WebSocket!");
std::string msg;
if (cli.read(msg) != httplib::ws::ReadResult::Fail) {
std::cout << "received: " << msg << std::endl;
}
cli.close();
}
```
Use a `ws://` (plain) or `wss://` (TLS) URL. Call `connect()` to do the handshake, then `send()` and `read()` work the same as on the server side.
## Text vs. binary
`send()` has two overloads that let you choose the frame type.
```cpp
ws.send("Hello"); // text frame
ws.send(binary_data, binary_data_size); // binary frame
```
The `std::string` overload sends as **text**; the `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it's intuitive. See [W04. Send and receive binary frames](w04-websocket-binary) for details.
## Thread pool implications
A WebSocket handler holds its worker thread for the entire life of the connection — one connection per thread. For many concurrent clients, configure a dynamic thread pool.
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(8, 128);
};
```
See [S21. Configure the thread pool](s21-thread-pool).
> **Note:** To run WebSocket over HTTPS, use `httplib::SSLServer` instead of `httplib::Server` — the same `WebSocket()` handler just works. On the client side, use a `wss://` URL.

View File

@@ -0,0 +1,80 @@
---
title: "W02. Set a WebSocket Heartbeat"
order: 52
status: "draft"
---
WebSocket connections stay open for a long time, and proxies or load balancers will sometimes drop them for being "idle." To prevent that, you periodically send Ping frames to keep the connection alive. cpp-httplib can do this for you automatically.
## Server side
```cpp
svr.set_websocket_ping_interval(30); // ping every 30 seconds
svr.WebSocket("/chat", [](const auto &req, auto &ws) {
// ...
});
```
Just pass the interval in seconds. Every WebSocket connection this server accepts will be pinged on that interval.
There's a `std::chrono` overload too.
```cpp
using namespace std::chrono_literals;
svr.set_websocket_ping_interval(30s);
```
## Client side
The client has the same API.
```cpp
httplib::ws::WebSocketClient cli("ws://localhost:8080/chat");
cli.set_websocket_ping_interval(30);
cli.connect();
```
Call it before `connect()`.
## The default
The default interval is set by the build-time macro `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`. Usually you won't need to change it, but adjust downward if you're dealing with an aggressive proxy.
## What about Pong?
The WebSocket protocol requires that Ping frames are answered with Pong frames. cpp-httplib responds to Pings automatically — you don't need to think about it in application code.
## Picking an interval
| Environment | Suggested |
| --- | --- |
| Normal internet | 3060s |
| Strict proxies (e.g. AWS ALB) | 1530s |
| Mobile networks | 60s+ (too short drains battery) |
Too short wastes bandwidth; too long and connections get dropped. As a rule of thumb, target about **half the idle timeout** of whatever's between you and the client.
> **Warning:** A very short ping interval spawns background work per connection and increases CPU usage. For servers with many connections, keep the interval modest.
## Detecting an unresponsive peer
Sending pings alone doesn't tell you anything if the peer just silently dies — the TCP socket might still look open while the process on the other end is long gone. To catch that, enable the max-missed-pongs check: if N consecutive pings go unanswered, the connection is closed.
```cpp
cli.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
```
The server side has the same `set_websocket_max_missed_pongs()`.
With a 30-second ping interval and `max_missed_pongs = 2`, a dead peer is detected within roughly 60 seconds and the connection is closed with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
The counter is reset whenever `read()` consumes an incoming Pong frame, so this only works if your code is actively calling `read()` in a loop — which is what a normal WebSocket client does anyway.
### Why the default is 0
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
Even with `0`, a dead connection won't linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. Think of `max_missed_pongs` as the knob for detecting an unresponsive peer **faster** than that.
> For handling a closed connection, see [W03. Handle connection close](w03-websocket-close).

View File

@@ -0,0 +1,91 @@
---
title: "W03. Handle Connection Close"
order: 53
status: "draft"
---
A WebSocket ends when either side closes it explicitly, or when the network drops. Handle close cleanly, and your cleanup and reconnect logic stays tidy.
## Detect a closed connection
When `ws.read()` returns `ReadResult::Fail`, the connection is gone — either cleanly or with an error. Break out of the loop and the handler will finish.
```cpp
svr.WebSocket("/chat", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.is_open()) {
auto result = ws.read(msg);
if (result == httplib::ws::ReadResult::Fail) {
std::cout << "disconnected" << std::endl;
break;
}
handle_message(ws, msg);
}
// cleanup runs once we're out of the loop
cleanup_user_session(req);
});
```
You can also check `ws.is_open()` — it's the same signal from a different angle.
## Close from the server side
To close explicitly, call `close()`.
```cpp
ws.close(httplib::ws::CloseStatus::Normal, "bye");
```
The first argument is the close status; the second is an optional reason. Common `CloseStatus` values:
| Value | Meaning |
| --- | --- |
| `Normal` (1000) | Normal closure |
| `GoingAway` (1001) | Server is shutting down |
| `ProtocolError` (1002) | Protocol violation detected |
| `UnsupportedData` (1003) | Received data that can't be handled |
| `PolicyViolation` (1008) | Violated a policy |
| `MessageTooBig` (1009) | Message too large |
| `InternalError` (1011) | Server-side error |
## Close from the client side
The client API is identical.
```cpp
cli.close(httplib::ws::CloseStatus::Normal);
```
Destroying the client also closes the connection, but calling `close()` explicitly makes the intent clearer.
## Graceful shutdown
To notify in-flight clients that the server is going down, use `GoingAway`.
```cpp
ws.close(httplib::ws::CloseStatus::GoingAway, "server restarting");
```
The client can inspect that status and decide whether to reconnect.
## Example: a tiny chat with quit
```cpp
svr.WebSocket("/chat", [](const auto &req, auto &ws) {
std::string msg;
while (ws.is_open()) {
if (ws.read(msg) == httplib::ws::ReadResult::Fail) break;
if (msg == "/quit") {
ws.send("goodbye");
ws.close(httplib::ws::CloseStatus::Normal, "user quit");
break;
}
ws.send("echo: " + msg);
}
});
```
> **Note:** On a sudden network drop, `read()` returns `Fail` with no chance to call `close()`. Put your cleanup at the end of the handler, and both paths — clean close and abrupt disconnect — end up in the same place.

View File

@@ -0,0 +1,85 @@
---
title: "W04. Send and Receive Binary Frames"
order: 54
status: "draft"
---
WebSocket has two frame types: text and binary. JSON and plain text go in text frames; images and raw protocol bytes go in binary. In cpp-httplib, `send()` picks the right type via overload.
## How to pick a frame type
```cpp
ws.send(std::string("Hello")); // text
ws.send("Hello", 5); // binary
ws.send(binary_data, binary_data_size); // binary
```
The `std::string` overload sends as **text**. The `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it sticks.
If you have a `std::string` and want to send it as binary, pass `.data()` and `.size()` explicitly.
```cpp
std::string raw = build_binary_payload();
ws.send(raw.data(), raw.size()); // binary frame
```
## Detect frame type on receive
The return value of `ws.read()` tells you whether the received frame was text or binary.
```cpp
std::string msg;
auto result = ws.read(msg);
switch (result) {
case httplib::ws::ReadResult::Text:
std::cout << "text: " << msg << std::endl;
break;
case httplib::ws::ReadResult::Binary:
std::cout << "binary: " << msg.size() << " bytes" << std::endl;
handle_binary(msg.data(), msg.size());
break;
case httplib::ws::ReadResult::Fail:
// error or closed
break;
}
```
Binary frames still come back in a `std::string`, but treat its contents as raw bytes — use `msg.data()` and `msg.size()`.
## When binary is the right call
- **Images, video, audio**: No Base64 overhead
- **Custom protocols**: protobuf, MessagePack, or any structured binary format
- **Game networking**: When latency matters
- **Sensor data streams**: Push numeric arrays directly
## Ping is binary-ish, but hidden
WebSocket Ping/Pong frames are close cousins of binary frames at the opcode level, but cpp-httplib handles them automatically — you don't touch them. See [W02. Set a WebSocket heartbeat](w02-websocket-ping).
## Example: send an image
```cpp
// Server: push an image
svr.WebSocket("/image", [](const auto &req, auto &ws) {
auto img = read_image_file("logo.png");
ws.send(img.data(), img.size());
});
```
```cpp
// Client: receive and save
httplib::ws::WebSocketClient cli("ws://localhost:8080/image");
cli.connect();
std::string buf;
if (cli.read(buf) == httplib::ws::ReadResult::Binary) {
std::ofstream ofs("received.png", std::ios::binary);
ofs.write(buf.data(), buf.size());
}
```
You can mix text and binary in the same connection. A common pattern: JSON for control messages, binary for the actual data — you get efficient handling of metadata and payload both.
> **Note:** WebSocket frames don't have an infinite size limit. For very large data, chunk it in your application code. cpp-httplib can handle a big frame in one shot, but it does load it all into memory at once.

View File

@@ -0,0 +1,60 @@
---
title: "C01. レスポンスボディを取得する / ファイルに保存する"
order: 1
status: "draft"
---
## 文字列として取得する
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/hello");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
`res->body``std::string`なので、そのまま使えます。レスポンス全体がメモリに読み込まれます。
> **Warning:** 大きなファイルを`res->body`で受け取ると、まるごとメモリに載ってしまいます。サイズが大きい場合は次の`ContentReceiver`を使いましょう。
## ファイルに保存する
```cpp
httplib::Client cli("http://localhost:8080");
std::ofstream ofs("output.bin", std::ios::binary);
if (!ofs) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto res = cli.Get("/large-file",
[&](const char *data, size_t len) {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
```
`ContentReceiver`を使うと、データをチャンクごとに受け取れます。ボディ全体をメモリに溜めずにファイルへ書き出せるので、大きなファイルのダウンロードにぴったりです。
コールバックから`false`を返すと、ダウンロードを途中で止められます。上の例では`ofs`への書き込みが失敗したら自動的に中断します。
> **Detail:** ダウンロード前にContent-Lengthなどのレスポンスヘッダーを確認したいときは、`ResponseHandler`を組み合わせましょう。
>
> ```cpp
> auto res = cli.Get("/large-file",
> [](const httplib::Response &res) {
> auto len = res.get_header_value("Content-Length");
> std::cout << "Size: " << len << std::endl;
> return true; // falseを返すとダウンロードを中止
> },
> [&](const char *data, size_t len) {
> ofs.write(data, len);
> return static_cast<bool>(ofs);
> });
> ```
>
> `ResponseHandler`はヘッダー受信後、ボディ受信前に呼ばれます。`false`を返せばダウンロード自体をスキップできます。
> ダウンロードの進捗を表示したい場合は[C11. 進捗コールバックを使う](c11-progress-callback)を参照してください。

View File

@@ -0,0 +1,36 @@
---
title: "C02. JSONを送受信する"
order: 2
status: "draft"
---
cpp-httplibにはJSONパーサーが含まれていません。JSONの組み立てや解析には[nlohmann/json](https://github.com/nlohmann/json)などのライブラリを使ってください。ここでは`nlohmann/json`を例に説明します。
## JSONを送信する
```cpp
httplib::Client cli("http://localhost:8080");
nlohmann::json j = {{"name", "Alice"}, {"age", 30}};
auto res = cli.Post("/api/users", j.dump(), "application/json");
```
`Post()`の第2引数にJSON文字列、第3引数にContent-Typeを渡します。`Put()``Patch()`でも同じ形です。
> **Warning:** 第3引数のContent-Typeを省略すると、サーバー側でJSONとして認識されないことがあります。`"application/json"`を必ず指定しましょう。
## JSONレスポンスを受け取る
```cpp
auto res = cli.Get("/api/users/1");
if (res && res->status == 200) {
auto j = nlohmann::json::parse(res->body);
std::cout << j["name"] << std::endl;
}
```
`res->body``std::string`なので、そのままJSONライブラリに渡せます。
> **Note:** サーバーがエラー時にHTMLを返すことがあります。ステータスコードを確認してからパースすると安全です。また、APIによっては`Accept: application/json`ヘッダーが必要です。JSON APIを繰り返し呼ぶなら[C03. デフォルトヘッダーを設定する](c03-default-headers)が便利です。
> サーバー側でJSONを受け取って返す方法は[S02. JSONリクエストを受け取りJSONレスポンスを返す](s02-json-api)を参照してください。

View File

@@ -0,0 +1,55 @@
---
title: "C03. デフォルトヘッダーを設定する"
order: 3
status: "draft"
---
同じヘッダーを毎回のリクエストに付けたいときは、`set_default_headers()`を使います。一度設定すれば、そのクライアントから送るすべてのリクエストに自動で付与されます。
## 基本の使い方
```cpp
httplib::Client cli("https://api.example.com");
cli.set_default_headers({
{"Accept", "application/json"},
{"User-Agent", "my-app/1.0"},
});
auto res = cli.Get("/users");
```
`Accept``User-Agent`のように、APIを呼ぶたびに必要なヘッダーをまとめて登録できます。リクエストごとに指定する手間が省けます。
## Bearerトークンを毎回付ける
```cpp
httplib::Client cli("https://api.example.com");
cli.set_default_headers({
{"Authorization", "Bearer " + token},
{"Accept", "application/json"},
});
auto res1 = cli.Get("/me");
auto res2 = cli.Get("/projects");
```
認証トークンを一度セットしておけば、以降のリクエストで自動的に送られます。複数のエンドポイントを叩くAPIクライアントを書くときに便利です。
> **Note:** `set_default_headers()`は既存のデフォルトヘッダーを**上書き**します。あとから1つだけ追加したい場合でも、全体を渡し直してください。
## リクエスト単位のヘッダーと組み合わせる
デフォルトヘッダーを設定していても、個別のリクエストで追加のヘッダーを渡せます。
```cpp
httplib::Headers headers = {
{"X-Request-ID", "abc-123"},
};
auto res = cli.Get("/users", headers);
```
リクエスト単位で渡したヘッダーはデフォルトヘッダーに**追加**されます。両方がサーバーに送られます。
> Bearerトークンを使った認証の詳細は[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)を参照してください。

View File

@@ -0,0 +1,38 @@
---
title: "C04. リダイレクトを追従する"
order: 4
status: "draft"
---
cpp-httplibはデフォルトではリダイレクトHTTP 3xxを追従しません。サーバーから`302 Found`が返ってきても、そのままステータスコード302のレスポンスとして受け取ります。
自動で追従してほしいときは、`set_follow_location(true)`を呼びましょう。
## リダイレクトを追従する
```cpp
httplib::Client cli("http://example.com");
cli.set_follow_location(true);
auto res = cli.Get("/old-path");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
`set_follow_location(true)`を設定すると、`Location`ヘッダーを見て新しいURLに自動でリクエストを投げ直します。最終的なレスポンスが`res`に入ります。
## HTTPからHTTPSへのリダイレクト
```cpp
httplib::Client cli("http://example.com");
cli.set_follow_location(true);
auto res = cli.Get("/");
```
多くのサイトはHTTPアクセスをHTTPSへリダイレクトします。`set_follow_location(true)`を有効にしておけば、こうしたケースも透過的に扱えます。スキームやホストが変わっても自動で追従します。
> **Warning:** HTTPSへのリダイレクトを追従するには、cpp-httplibをOpenSSLまたは他のTLSバックエンド付きでビルドしておく必要があります。TLSサポートがないと、HTTPSへのリダイレクトは失敗します。
> **Note:** リダイレクトを追従すると、リクエストの実行時間は伸びます。タイムアウトの設定は[C12. タイムアウトを設定する](c12-timeouts)を参照してください。

View File

@@ -0,0 +1,46 @@
---
title: "C05. Basic認証を使う"
order: 5
status: "draft"
---
Basic認証が必要なエンドポイントには、`set_basic_auth()`でユーザー名とパスワードを渡します。cpp-httplibが自動で`Authorization: Basic ...`ヘッダーを組み立ててくれます。
## 基本の使い方
```cpp
httplib::Client cli("https://api.example.com");
cli.set_basic_auth("alice", "s3cret");
auto res = cli.Get("/private");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
一度設定すれば、そのクライアントから送るすべてのリクエストに認証情報が付きます。毎回ヘッダーを組み立てる必要はありません。
## リクエスト単位で使う
特定のリクエストだけに認証情報を付けたいときは、Headersを直接渡す方法もあります。
```cpp
httplib::Headers headers = {
httplib::make_basic_authentication_header("alice", "s3cret"),
};
auto res = cli.Get("/private", headers);
```
`make_basic_authentication_header()`がBase64エンコード済みのヘッダーを作ってくれます。
> **Warning:** Basic認証は資格情報をBase64で**エンコード**するだけで、暗号化しません。必ずHTTPS経由で使ってください。平文HTTPで使うと、パスワードがネットワーク上を丸見えで流れます。
## Digest認証
より安全なDigest認証を使いたいときは`set_digest_auth()`を使います。こちらはOpenSSLまたは他のTLSバックエンド付きでビルドしたときだけ利用できます。
```cpp
cli.set_digest_auth("alice", "s3cret");
```
> BearerトークンでAPIを呼びたい場合は[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)を参照してください。

View File

@@ -0,0 +1,50 @@
---
title: "C06. BearerトークンでAPIを呼ぶ"
order: 6
status: "draft"
---
OAuth 2.0やモダンなWeb APIでよく使われるBearerトークン認証には、`set_bearer_token_auth()`を使います。トークンを渡すと、cpp-httplibが`Authorization: Bearer <token>`ヘッダーを自動で組み立ててくれます。
## 基本の使い方
```cpp
httplib::Client cli("https://api.example.com");
cli.set_bearer_token_auth("eyJhbGciOiJIUzI1NiIs...");
auto res = cli.Get("/me");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
一度設定すれば、以降のリクエストすべてにトークンが付きます。GitHub APIやSlack API、自前のOAuthサービスなど、トークンベースのAPIを叩くときの定番です。
## リクエスト単位で使う
特定のリクエストだけにトークンを付けたい、あるいはリクエストごとに違うトークンを使いたいときは、Headersで直接渡せます。
```cpp
httplib::Headers headers = {
httplib::make_bearer_token_authentication_header(token),
};
auto res = cli.Get("/me", headers);
```
`make_bearer_token_authentication_header()``Authorization`ヘッダーを組み立ててくれます。
## トークンをリフレッシュする
トークンの有効期限が切れたら、新しいトークンで`set_bearer_token_auth()`を呼び直すだけで更新できます。
```cpp
if (res && res->status == 401) {
auto new_token = refresh_token();
cli.set_bearer_token_auth(new_token);
res = cli.Get("/me");
}
```
> **Warning:** Bearerトークンはそれ自体が認証情報です。必ずHTTPS経由で送ってください。また、ソースコードや設定ファイルにトークンをハードコードしないようにしましょう。
> 複数のヘッダーをまとめて設定したいときは[C03. デフォルトヘッダーを設定する](c03-default-headers)も便利です。

View File

@@ -0,0 +1,52 @@
---
title: "C07. ファイルをマルチパートフォームとしてアップロードする"
order: 7
status: "draft"
---
HTMLフォームの`<input type="file">`と同じ形式でファイルを送りたいときは、マルチパートフォーム(`multipart/form-data`を使います。cpp-httplibは`UploadFormDataItems``FormDataProviderItems`の2種類のAPIを用意しています。使い分けの基準は**ファイルサイズ**です。
## 小さなファイルを送る
ファイル内容をメモリに読み込んでから送る方法です。サイズが小さいなら、これが一番シンプルです。
```cpp
httplib::Client cli("http://localhost:8080");
std::ifstream ifs("avatar.png", std::ios::binary);
std::string content((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
httplib::UploadFormDataItems items = {
{"name", "Alice", "", ""},
{"avatar", content, "avatar.png", "image/png"},
};
auto res = cli.Post("/upload", items);
```
`UploadFormData`の各要素は`{name, content, filename, content_type}`の4つです。テキストフィールドなら`filename``content_type`を空文字にしておきます。
## 大きなファイルをストリーミングで送る
ファイル全体をメモリに載せずに、チャンクごとに送りたいときは`make_file_provider()`を使います。内部でファイルを少しずつ読みながら送信するので、巨大なファイルでもメモリを圧迫しません。
```cpp
httplib::Client cli("http://localhost:8080");
httplib::UploadFormDataItems items = {
{"name", "Alice", "", ""},
};
httplib::FormDataProviderItems provider_items = {
httplib::make_file_provider("video", "large-video.mp4", "", "video/mp4"),
};
auto res = cli.Post("/upload", httplib::Headers{}, items, provider_items);
```
`make_file_provider()`の引数は`(フォーム名, ファイルパス, ファイル名, Content-Type)`です。ファイル名を空にするとファイルパスがそのまま使われます。
> **Note:** `UploadFormDataItems`と`FormDataProviderItems`は同じリクエスト内で併用できます。テキストフィールドは`UploadFormDataItems`、ファイルは`FormDataProviderItems`、という使い分けがきれいです。
> アップロードの進捗を表示したい場合は[C11. 進捗コールバックを使う](c11-progress-callback)を参照してください。

View File

@@ -0,0 +1,34 @@
---
title: "C08. ファイルを生バイナリとしてPOSTする"
order: 8
status: "draft"
---
マルチパートではなく、ファイルの中身をそのままリクエストボディとして送りたいときがあります。S3互換APIへのアップロードや、生の画像データを受け付けるエンドポイントなどです。このときは`make_file_body()`を使いましょう。
## 基本の使い方
```cpp
httplib::Client cli("https://storage.example.com");
auto [size, provider] = httplib::make_file_body("backup.tar.gz");
if (size == 0) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto res = cli.Put("/bucket/backup.tar.gz", size,
provider, "application/gzip");
```
`make_file_body()`はファイルサイズと`ContentProvider`のペアを返します。そのまま`Post()``Put()`に渡せば、ファイルの中身がリクエストボディとしてそのまま送られます。
`ContentProvider`はファイルをチャンクごとに読み込むので、巨大なファイルでもメモリに全体を載せません。
## ファイルが開けなかったとき
`make_file_body()`は開けなかった場合、`size``0``provider`を空の関数オブジェクトとして返します。そのまま送信するとおかしな結果になるので、必ず`size`をチェックしてください。
> **Warning:** `make_file_body()`はContent-Lengthを最初に確定させる必要があるため、ファイルサイズをあらかじめ取得します。送信中にファイルサイズが変わる可能性がある場合は、このAPIには向きません。
> マルチパート形式で送りたい場合は[C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)を参照してください。

View File

@@ -0,0 +1,47 @@
---
title: "C09. チャンク転送でボディを送る"
order: 9
status: "draft"
---
送信するボディのサイズが事前にわからないとき、たとえばリアルタイムに生成されるデータや別のストリームから流し込むデータを送りたいときは、`ContentProviderWithoutLength`を使います。HTTPのチャンク転送エンコーディングchunked transfer-encodingとして送信されます。
## 基本の使い方
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Post("/stream",
[&](size_t offset, httplib::DataSink &sink) {
std::string chunk = produce_next_chunk();
if (chunk.empty()) {
sink.done(); // 送信終了
return true;
}
return sink.write(chunk.data(), chunk.size());
},
"application/octet-stream");
```
ラムダは「次のチャンクを作って`sink.write()`で送る」だけです。データがもう無くなったら`sink.done()`を呼べば送信が完了します。
## サイズがわかっている場合
送信するボディの**合計サイズが事前にわかっている**ときは、`ContentProvider``size_t offset, size_t length, DataSink &sink`を取るタイプ)と合計サイズを渡す別のオーバーロードを使います。
```cpp
size_t total_size = get_total_size();
auto res = cli.Post("/upload", total_size,
[&](size_t offset, size_t length, httplib::DataSink &sink) {
auto data = read_range(offset, length);
return sink.write(data.data(), data.size());
},
"application/octet-stream");
```
サイズがわかっているとContent-Lengthヘッダーが付くので、サーバー側で進捗を把握しやすくなります。可能ならこちらを使いましょう。
> **Detail:** `sink.write()`は書き込みが成功したかどうかを`bool`で返します。`false`が返ったら回線が切れています。ラムダはそのまま`false`を返して終了しましょう。
> ファイルをそのまま送るだけなら、`make_file_body()`が便利です。[C08. ファイルを生バイナリとしてPOSTする](c08-post-file-body)を参照してください。

View File

@@ -0,0 +1,52 @@
---
title: "C10. レスポンスをストリーミングで受信する"
order: 10
status: "draft"
---
レスポンスのボディをチャンクごとに受け取りたいときは、`ContentReceiver`を使います。大きなファイルを扱うときはもちろん、NDJSON改行区切りJSONやログストリームのように「届いた分だけ先に処理したい」ケースでも便利です。
## チャンクごとに処理する
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/logs/stream",
[](const char *data, size_t len) {
std::cout.write(data, len);
std::cout.flush();
return true; // falseを返すと受信を中止
});
```
サーバーから届いたデータが、到着した順にラムダに渡されます。コールバックが`false`を返すとダウンロードを途中で止められます。
## NDJSONを行単位でパースする
バッファを使って改行区切りのJSONを1行ずつ処理する例です。
```cpp
std::string buffer;
auto res = cli.Get("/events",
[&](const char *data, size_t len) {
buffer.append(data, len);
size_t pos;
while ((pos = buffer.find('\n')) != std::string::npos) {
auto line = buffer.substr(0, pos);
buffer.erase(0, pos + 1);
if (!line.empty()) {
auto j = nlohmann::json::parse(line);
handle_event(j);
}
}
return true;
});
```
バッファに貯めながら、改行が見つかるたびに1行を取り出してパースします。ストリーミングAPIをリアルタイムに処理する基本パターンです。
> **Warning:** `ContentReceiver`を渡すと、`res->body`は**空のまま**になります。ボディは自分でコールバック内で保存するか処理するかしてください。
> ダウンロードの進捗を知りたい場合は[C11. 進捗コールバックを使う](c11-progress-callback)と組み合わせましょう。
> Server-Sent EventsSSEを扱うときは[E04. SSEをクライアントで受信する](e04-sse-client)も参考になります。

View File

@@ -0,0 +1,59 @@
---
title: "C11. 進捗コールバックを使う"
order: 11
status: "draft"
---
ダウンロードやアップロードの進捗を表示したいときは、`DownloadProgress`または`UploadProgress`コールバックを渡します。どちらも`(current, total)`の2引数を取る関数オブジェクトです。
## ダウンロードの進捗
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/large-file",
[](size_t current, size_t total) {
auto percent = (total > 0) ? (current * 100 / total) : 0;
std::cout << "\rDownloading: " << percent << "% ("
<< current << "/" << total << ")" << std::flush;
return true; // falseを返すとダウンロードを中止
});
std::cout << std::endl;
```
コールバックはデータを受信するたびに呼ばれます。`total`はContent-Lengthから取得した値で、サーバーが送ってこない場合は`0`になることがあります。その場合は進捗率が計算できないので、受信済みバイト数だけを表示するのが無難です。
## アップロードの進捗
アップロード側も同じ形です。`Post()``Put()`の最後の引数に`UploadProgress`を渡します。
```cpp
httplib::Client cli("http://localhost:8080");
std::string body = load_large_body();
auto res = cli.Post("/upload", body, "application/octet-stream",
[](size_t current, size_t total) {
auto percent = current * 100 / total;
std::cout << "\rUploading: " << percent << "%" << std::flush;
return true;
});
std::cout << std::endl;
```
## 中断する
コールバックから`false`を返すと、転送を中止できます。UI側で「キャンセル」ボタンが押されたら`false`を返す、といった使い方ができます。
```cpp
std::atomic<bool> cancelled{false};
auto res = cli.Get("/large-file",
[&](size_t current, size_t total) {
return !cancelled.load();
});
```
> **Note:** `ContentReceiver`と進捗コールバックは同時に使えます。ファイルに書き出しながら進捗を表示したいときは、両方を渡しましょう。
> ファイル保存と組み合わせる具体例は[C01. レスポンスボディを取得する / ファイルに保存する](c01-get-response-body)も参照してください。

View File

@@ -0,0 +1,50 @@
---
title: "C12. タイムアウトを設定する"
order: 12
status: "draft"
---
クライアントには3種類のタイムアウトがあります。それぞれ別々に設定できます。
| 種類 | API | デフォルト | 意味 |
| --- | --- | --- | --- |
| 接続タイムアウト | `set_connection_timeout` | 300秒 | TCP接続の確立までの待ち時間 |
| 読み取りタイムアウト | `set_read_timeout` | 300秒 | レスポンスを受信する際の1回の`recv`待ち時間 |
| 書き込みタイムアウト | `set_write_timeout` | 5秒 | リクエストを送信する際の1回の`send`待ち時間 |
## 基本の使い方
```cpp
httplib::Client cli("http://localhost:8080");
cli.set_connection_timeout(5, 0); // 5秒
cli.set_read_timeout(10, 0); // 10秒
cli.set_write_timeout(10, 0); // 10秒
auto res = cli.Get("/api/data");
```
秒数とマイクロ秒を2引数で渡します。細かい指定が不要なら第2引数は省略できます。
## `std::chrono`で指定する
`std::chrono`の期間を直接渡すオーバーロードもあります。こちらのほうが読みやすいのでおすすめです。
```cpp
using namespace std::chrono_literals;
cli.set_connection_timeout(5s);
cli.set_read_timeout(10s);
cli.set_write_timeout(500ms);
```
## デフォルトでは300秒と長めな点に注意
接続タイムアウトと読み取りタイムアウトはデフォルトで**300秒5分**です。サーバーが反応しない場合、このままだと5分待たされます。短めに設定したほうが良いことが多いです。
```cpp
cli.set_connection_timeout(3s);
cli.set_read_timeout(10s);
```
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合は[C13. 全体タイムアウトを設定する](c13-max-timeout)を使ってください。

View File

@@ -0,0 +1,42 @@
---
title: "C13. 全体タイムアウトを設定する"
order: 13
status: "draft"
---
[C12. タイムアウトを設定する](c12-timeouts)で紹介した3種類のタイムアウトは、いずれも「1回の`send``recv`」に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
## 基本の使い方
```cpp
httplib::Client cli("http://localhost:8080");
cli.set_max_timeout(5000); // 5秒ミリ秒単位
auto res = cli.Get("/slow-endpoint");
```
ミリ秒単位で指定します。接続、送信、受信をすべて含めて、リクエスト全体が指定時間を超えたら打ち切られます。
## `std::chrono`で指定する
こちらも`std::chrono`の期間を受け取るオーバーロードがあります。
```cpp
using namespace std::chrono_literals;
cli.set_max_timeout(5s);
```
## どう使い分けるか
`set_read_timeout`は「データが来ない時間」のタイムアウトなので、少しずつデータが流れ続ける状況では発火しません。たとえば1秒ごとに1バイト届くようなエンドポイントは、`set_read_timeout`をいくら短くしてもタイムアウトしません。
一方、`set_max_timeout`は「経過時間」に対する上限なので、こうしたケースでも確実に止められます。外部APIを叩くときや、ユーザーを待たせすぎたくないときに重宝します。
```cpp
cli.set_connection_timeout(3s);
cli.set_read_timeout(10s);
cli.set_max_timeout(30s); // 全体で30秒を超えたら中断
```
> **Note:** `set_max_timeout()`は通常のタイムアウトと併用できます。短期的な無反応は`set_read_timeout`で、長時間の処理は`set_max_timeout`で、という二段構えにするのが安全です。

View File

@@ -0,0 +1,53 @@
---
title: "C14. 接続の再利用とKeep-Aliveの挙動を理解する"
order: 14
status: "draft"
---
`httplib::Client`は同じインスタンスで複数回リクエストを送ると、TCP接続を自動的に再利用します。HTTP/1.1のKeep-Aliveが有効に働くので、TCPハンドシェイクやTLSハンドシェイクのオーバーヘッドを毎回払わずに済みます。
## 接続は自動で使い回される
```cpp
httplib::Client cli("https://api.example.com");
auto res1 = cli.Get("/users/1");
auto res2 = cli.Get("/users/2"); // 同じ接続を再利用
auto res3 = cli.Get("/users/3"); // 同じ接続を再利用
```
特別な設定は要りません。`cli`を使い回すだけで、内部的には同じソケットで通信が続きます。とくにHTTPSでは、TLSハンドシェイクのコストが大きいので効果が顕著です。
## Keep-Aliveを明示的にオフにする
毎回新しい接続を張り直したい場合は、`set_keep_alive(false)`を呼びます。テスト目的などで使うことがあります。
```cpp
cli.set_keep_alive(false);
```
ただし、普段はオン(デフォルト)のままで問題ありません。
## リクエストごとに`Client`を作らない
1回のリクエストのたびに`Client`をスコープから抜けて破棄すると、接続の再利用は効きません。ループの外でインスタンスを作り、中で使い回しましょう。
```cpp
// NG: 毎回接続が切れる
for (auto id : ids) {
httplib::Client cli("https://api.example.com");
cli.Get("/users/" + id);
}
// OK: 接続が再利用される
httplib::Client cli("https://api.example.com");
for (auto id : ids) {
cli.Get("/users/" + id);
}
```
## 並行リクエスト
複数のスレッドから並行にリクエストを送りたいときは、スレッドごとに別々の`Client`インスタンスを持つのが無難です。1つの`Client`は1本のTCP接続を使い回すので、同じインスタンスに複数スレッドから同時にリクエストを投げると、結局どこかで直列化されます。
> **Note:** サーバー側のKeep-Aliveタイムアウトを超えると、サーバーが接続を切ります。その場合cpp-httplibは自動で再接続して再試行するので、アプリケーションコードで気にする必要はありません。

View File

@@ -0,0 +1,47 @@
---
title: "C15. 圧縮を有効にする"
order: 15
status: "draft"
---
cpp-httplibは送信時の圧縮と受信時の解凍をサポートしています。ただし、zlibまたはBrotliを有効にしてビルドしておく必要があります。
## ビルド時の準備
圧縮機能を使うには、`httplib.h`をインクルードする前に次のマクロを定義しておきます。
```cpp
#define CPPHTTPLIB_ZLIB_SUPPORT // gzip / deflate
#define CPPHTTPLIB_BROTLI_SUPPORT // brotli
#include <httplib.h>
```
リンク時に`zlib``brotli`のライブラリも必要です。
## リクエストボディを圧縮して送る
```cpp
httplib::Client cli("https://api.example.com");
cli.set_compress(true);
std::string big_payload = build_payload();
auto res = cli.Post("/api/data", big_payload, "application/json");
```
`set_compress(true)`を呼んでおくと、POSTやPUTのリクエストボディがgzipで圧縮されて送信されます。サーバー側が対応している必要があります。
## レスポンスを解凍する
```cpp
httplib::Client cli("https://api.example.com");
cli.set_decompress(true); // デフォルトで有効
auto res = cli.Get("/api/data");
std::cout << res->body << std::endl;
```
`set_decompress(true)`を呼ぶと、サーバーが`Content-Encoding: gzip`などで圧縮したレスポンスを自動で解凍してくれます。`res->body`には解凍済みのデータが入ります。
デフォルトで有効なので、通常は何もしなくても解凍されます。あえて生の圧縮データを触りたいときだけ`set_decompress(false)`にしましょう。
> **Warning:** `CPPHTTPLIB_ZLIB_SUPPORT`を定義せずにビルドすると、`set_compress()`や`set_decompress()`を呼んでも何も起こりません。マクロの定義を忘れていないか、最初に確認しましょう。

View File

@@ -0,0 +1,87 @@
---
title: "C16. プロキシを経由してリクエストを送る"
order: 16
status: "draft"
---
社内ネットワークや特定の経路を通したい場合、HTTPプロキシを経由してリクエストを送れます。`set_proxy()`でプロキシのホストとポートを指定するだけです。
## 基本の使い方
```cpp
httplib::Client cli("https://api.example.com");
cli.set_proxy("proxy.internal", 8080);
auto res = cli.Get("/users");
```
プロキシ経由でリクエストが送られます。HTTPSの場合はCONNECTメソッドでトンネルが張られるので、cpp-httplib側で特別な設定は要りません。
## プロキシに認証を設定する
プロキシ自体が認証を要求する場合は、`set_proxy_basic_auth()``set_proxy_bearer_token_auth()`を使います。
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_proxy_basic_auth("user", "password");
```
```cpp
cli.set_proxy_bearer_token_auth("token");
```
OpenSSLまたは他のTLSバックエンド付きでビルドしていれば、Digest認証も使えます。
```cpp
cli.set_proxy_digest_auth("user", "password");
```
## エンドのサーバー認証と組み合わせる
プロキシ認証と、エンドサーバーへの認証([C05. Basic認証を使う](c05-basic-auth)や[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token))は別物です。両方が必要なら、両方設定します。
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_proxy_basic_auth("proxy-user", "proxy-pass");
cli.set_bearer_token_auth("api-token"); // エンドサーバー向け
```
プロキシには`Proxy-Authorization`、エンドサーバーには`Authorization`ヘッダーが送られます。
## 特定のホストだけプロキシをバイパスする
社内エンドポイントなどはプロキシを経由させたくないことがあります。`set_no_proxy()`で除外リストを指定できます。
```cpp
cli.set_proxy("proxy.internal", 8080);
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
```
エントリは次のいずれかです。
- `*` — すべてのホストでバイパス
- ホスト名サフィックス(例: `example.com`)— `example.com`本体と任意のサブドメイン(`foo.example.com`)にマッチ。先頭にドットを付けても同じ意味です(`.example.com`)。
- 単一のIPリテラル例: `192.168.1.1``::1`
- CIDRブロック例: `10.0.0.0/8``fe80::/10`
ホスト名のマッチは大文字小文字を区別せず、ドット境界でしか一致しません。たとえば`example.com`というエントリは`evilexample.com`にはマッチしません。IPの比較は`inet_pton`で正規化されるので、`127.0.0.1``127.000.000.001`のような別表記でバイパスすることはできません。マッチした場合、`Proxy-Authorization`ヘッダーも自動的に外れます。
不正な書式のエントリは黙って捨てられます。`example.com:8080`のようなポート指定エントリはサポート外ですcpp-httplibの他のホストキーAPIもホスト名のみを扱う設計のため
## 環境変数からプロキシ設定を読み込む
cpp-httplib本体は`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`を読みません。`set_ca_cert_path()`と同じで、設定APIは常に明示的にしています。環境変数を反映させたい場合は、アプリ側で読んで`set_proxy()``set_no_proxy()`に渡してください。
```cpp
if (const char *v = std::getenv("no_proxy")) {
std::vector<std::string> patterns;
std::stringstream ss(v);
for (std::string item; std::getline(ss, item, ',');) {
if (!item.empty()) { patterns.push_back(item); }
}
cli.set_no_proxy(patterns);
}
```
`HTTP_PROXY`も自分で読むなら、小文字の`http_proxy`だけを採用してください。大文字の方はCGI/FastCGI環境で`Proxy:`リクエストヘッダーから汚染される可能性があります([CVE-2016-5385 / "httpoxy"](https://httpoxy.org/))。`HTTPS_PROXY``NO_PROXY`は名前が`HTTP_`で始まらないので、どちらの大文字小文字でも安全です。

View File

@@ -0,0 +1,63 @@
---
title: "C17. エラーコードをハンドリングする"
order: 17
status: "draft"
---
`cli.Get()``cli.Post()``Result`型を返します。リクエストが失敗したときサーバーに到達できなかった、タイムアウトしたなど、返り値は「falsy」になります。詳しい原因を知りたい場合は`Result::error()`を使います。
## 基本の判定
```cpp
httplib::Client cli("http://localhost:8080");
auto res = cli.Get("/api/data");
if (res) {
// リクエストが送れて、レスポンスも受け取れた
std::cout << "status: " << res->status << std::endl;
} else {
// ネットワーク層で失敗した
std::cerr << "error: " << httplib::to_string(res.error()) << std::endl;
}
```
`if (res)`で成功・失敗を判定し、失敗時は`res.error()``httplib::Error`列挙値を取り出せます。`to_string()`に渡すと人間が読める文字列になります。
## 代表的なエラー
| 値 | 意味 |
| --- | --- |
| `Error::Connection` | サーバーに接続できなかった |
| `Error::ConnectionTimeout` | 接続タイムアウト(`set_connection_timeout` |
| `Error::Read` / `Error::Write` | 送受信中のエラー |
| `Error::Timeout` | `set_max_timeout`で設定した全体タイムアウト |
| `Error::ExceedRedirectCount` | リダイレクト回数が上限を超えた |
| `Error::SSLConnection` | TLSハンドシェイクに失敗 |
| `Error::SSLServerVerification` | サーバー証明書の検証に失敗 |
| `Error::Canceled` | 進捗コールバックから`false`が返された |
## ステータスコードとの使い分け
`res`が truthy でも、HTTPステータスコードが4xxや5xxのこともあります。この2つは別物です。
```cpp
auto res = cli.Get("/api/data");
if (!res) {
// ネットワークエラー(そもそもレスポンスを受け取れていない)
std::cerr << "network error: " << httplib::to_string(res.error()) << std::endl;
return 1;
}
if (res->status >= 400) {
// HTTPエラーレスポンスは受け取った
std::cerr << "http error: " << res->status << std::endl;
return 1;
}
// 正常系
std::cout << res->body << std::endl;
```
ネットワーク層のエラーは`res.error()`、HTTPのエラーは`res->status`、と頭の中で分けておきましょう。
> SSL関連のエラーをさらに詳しく調べたい場合は[C18. SSLエラーをハンドリングする](c18-ssl-errors)を参照してください。

View File

@@ -0,0 +1,51 @@
---
title: "C18. SSLエラーをハンドリングする"
order: 18
status: "draft"
---
HTTPSリクエストで失敗したとき、`res.error()``Error::SSLConnection``Error::SSLServerVerification`といった値を返します。ただ、これだけだと原因の切り分けが難しいこともあります。そんなときは`Result::ssl_error()``Result::ssl_backend_error()`が役に立ちます。
## SSLエラーの詳細を取得する
```cpp
httplib::Client cli("https://api.example.com");
auto res = cli.Get("/");
if (!res) {
auto err = res.error();
std::cerr << "error: " << httplib::to_string(err) << std::endl;
if (err == httplib::Error::SSLConnection ||
err == httplib::Error::SSLServerVerification) {
std::cerr << "ssl_error: " << res.ssl_error() << std::endl;
std::cerr << "ssl_backend_error: " << res.ssl_backend_error() << std::endl;
}
}
```
`ssl_error()`はSSLライブラリが返したエラーコードOpenSSLの`SSL_get_error()`の値など)、`ssl_backend_error()`はバックエンドがさらに詳しく提供するエラー値です。OpenSSLなら`ERR_get_error()`の値が入ります。
## OpenSSLのエラーを文字列化する
`ssl_backend_error()`で取得した値を、OpenSSLの`ERR_error_string()`で文字列にするとデバッグに便利です。
```cpp
#include <openssl/err.h>
if (res.ssl_backend_error() != 0) {
char buf[256];
ERR_error_string_n(res.ssl_backend_error(), buf, sizeof(buf));
std::cerr << "openssl: " << buf << std::endl;
}
```
## よくある原因
| 症状 | ありがちな原因 |
| --- | --- |
| `SSLServerVerification` | CA証明書のパスが通っていない、自己署名証明書 |
| `SSLServerHostnameVerification` | 証明書のCN/SANとホスト名が一致しない |
| `SSLConnection` | TLSバージョンの不一致、対応スイートが無い |
> 証明書の検証設定を変えたい場合は[T02. SSL証明書の検証を制御する](t02-cert-verification)を参照してください。

View File

@@ -0,0 +1,57 @@
---
title: "C19. クライアントにログを設定する"
order: 19
status: "draft"
---
クライアントから送ったリクエストと受け取ったレスポンスをログに残したいときは、`set_logger()`を使います。エラーだけを拾いたいなら`set_error_logger()`が別に用意されています。
## リクエストとレスポンスをログに残す
```cpp
httplib::Client cli("https://api.example.com");
cli.set_logger([](const httplib::Request &req, const httplib::Response &res) {
std::cout << req.method << " " << req.path
<< " -> " << res.status << std::endl;
});
auto res = cli.Get("/users");
```
`set_logger()`に渡したコールバックは、リクエストが完了するたびに呼ばれます。リクエストとレスポンスの両方を引数で受け取れるので、メソッドやパス、ステータスコード、ヘッダー、ボディなど好きな情報をログに残せます。
## エラーだけを拾う
ネットワーク層のエラーが起きたとき(`Error::Connection`など)は、`set_logger()`は呼ばれません。レスポンスが無いからです。こうしたエラーを拾いたいときは`set_error_logger()`を使います。
```cpp
cli.set_error_logger([](const httplib::Error &err, const httplib::Request *req) {
std::cerr << "error: " << httplib::to_string(err);
if (req) {
std::cerr << " (" << req->method << " " << req->path << ")";
}
std::cerr << std::endl;
});
```
第2引数の`req`はヌルポインタのこともあります。リクエストを組み立てる前の段階で失敗した場合です。使う前に必ずヌルチェックしてください。
## 両方を組み合わせる
成功時は通常のログ、失敗時はエラーログ、という2本立てにすると便利です。
```cpp
cli.set_logger([](const auto &req, const auto &res) {
std::cout << "[ok] " << req.method << " " << req.path
<< " " << res.status << std::endl;
});
cli.set_error_logger([](const auto &err, const auto *req) {
std::cerr << "[ng] " << httplib::to_string(err);
if (req) std::cerr << " " << req->method << " " << req->path;
std::cerr << std::endl;
});
```
> **Note:** ログコールバックはリクエスト処理と同じスレッドで同期的に呼ばれます。重い処理を入れるとリクエストがその分遅くなるので、必要なら別スレッドのキューに流しましょう。

View File

@@ -0,0 +1,87 @@
---
title: "E01. SSEサーバーを実装する"
order: 47
status: "draft"
---
Server-Sent EventsSSEは、サーバーからクライアントへイベントを一方向にプッシュするためのシンプルなプロトコルです。長時間の接続を保ったまま、サーバーが好きなタイミングでデータを送れます。WebSocketより軽量で、HTTPの範囲で完結するのが魅力です。
cpp-httplibにはSSE専用のサーバーAPIはありませんが、`set_chunked_content_provider()``text/event-stream`を組み合わせれば実装できます。
## 基本のSSEサーバー
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[](size_t offset, httplib::DataSink &sink) {
std::string message = "data: hello\n\n";
sink.write(message.data(), message.size());
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
ポイントは3つです。
1. Content-Typeを`text/event-stream`にする
2. メッセージは`data: <内容>\n\n`の形式で書く(`\n\n`で1イベントの区切り
3. `sink.write()`で送るたびに、クライアントが受け取る
接続が生きている限り、プロバイダラムダが繰り返し呼ばれ続けます。
## イベントを送り続ける例
サーバーの現在時刻を1秒ごとに送るシンプルな例です。
```cpp
svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&req](size_t offset, httplib::DataSink &sink) {
if (req.is_connection_closed()) {
sink.done();
return true;
}
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::string msg = "data: " + std::string(std::ctime(&t)) + "\n";
sink.write(msg.data(), msg.size());
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
クライアントが切断したら`sink.done()`で終了します。詳しくは[S16. クライアントが切断したか検出する](s16-disconnect)を参照してください。
## コメント行でハートビート
`:`で始まる行はSSEのコメントで、クライアントは無視しますが、**接続を生かしておく**役割があります。プロキシやロードバランサが無通信接続を切ってしまうのを防げます。
```cpp
// 30秒ごとにハートビート
if (tick_count % 30 == 0) {
std::string ping = ": ping\n\n";
sink.write(ping.data(), ping.size());
}
```
## スレッドプールとの関係
SSEは接続がつなぎっぱなしなので、1クライアントあたり1ワーカースレッドを消費します。同時接続数が多くなりそうなら、スレッドプールを動的スケーリングにしておきましょう。
```cpp
svr.new_task_queue = [] {
return new httplib::ThreadPool(8, 128);
};
```
詳しくは[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
> **Note:** `data:`の後ろに改行が含まれる場合、各行の先頭に`data: `を付けて複数の`data:`行として送ります。SSEの仕様で決まっているフォーマットです。
> イベント名を使い分けたい場合は[E02. SSEでイベント名を使い分ける](e02-sse-event-names)を、クライアント側は[E04. SSEをクライアントで受信する](e04-sse-client)を参照してください。

View File

@@ -0,0 +1,84 @@
---
title: "E02. SSEでイベント名を使い分ける"
order: 48
status: "draft"
---
SSEでは、1本のストリームで複数の種類のイベントを送れます。`event:`フィールドで名前を付けると、クライアント側で種類ごとに別々のハンドラを呼べます。チャットの「新規メッセージ」「入室」「退室」のような場面で便利です。
## イベント名付きで送る
```cpp
auto send_event = [](httplib::DataSink &sink,
const std::string &event,
const std::string &data) {
std::string msg = "event: " + event + "\n"
+ "data: " + data + "\n\n";
sink.write(msg.data(), msg.size());
};
svr.Get("/chat/stream", [&](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/event-stream",
[&, send_event](size_t offset, httplib::DataSink &sink) {
send_event(sink, "message", "Hello!");
std::this_thread::sleep_for(std::chrono::seconds(2));
send_event(sink, "join", "alice");
std::this_thread::sleep_for(std::chrono::seconds(2));
send_event(sink, "leave", "bob");
std::this_thread::sleep_for(std::chrono::seconds(2));
return true;
});
});
```
1メッセージは`event:``data:` → 空行、の形式です。`event:`を書かないと、クライアント側ではデフォルトの`"message"`イベントとして扱われます。
## IDを付けて再接続に備える
`id:`フィールドを一緒に送ると、クライアントが切断→再接続したときに`Last-Event-ID`ヘッダーで「どこまで受け取ったか」を教えてくれます。
```cpp
auto send_event = [](httplib::DataSink &sink,
const std::string &event,
const std::string &data,
const std::string &id) {
std::string msg = "id: " + id + "\n"
+ "event: " + event + "\n"
+ "data: " + data + "\n\n";
sink.write(msg.data(), msg.size());
};
send_event(sink, "message", "Hello!", "42");
```
IDの付け方は自由です。連番でもUUIDでも、サーバー側で重複せず順序が追えるものを選びましょう。再接続の詳細は[E03. SSEの再接続を処理する](e03-sse-reconnect)を参照してください。
## JSONをdataに乗せる
構造化されたデータを送りたいときは、`data:`の中身をJSONにするのが定番です。
```cpp
nlohmann::json payload = {
{"user", "alice"},
{"text", "Hello!"},
};
send_event(sink, "message", payload.dump(), "42");
```
クライアント側では受け取った`data`をそのままJSONパースすれば、元のオブジェクトに戻せます。
## データに改行が含まれる場合
`data:`の値に改行が入るときは、各行の先頭に`data: `を付けて複数行に分けて送ります。
```cpp
std::string msg = "data: line1\n"
"data: line2\n"
"data: line3\n\n";
sink.write(msg.data(), msg.size());
```
クライアント側では、これらが改行でつながった1つの`data`として復元されます。
> **Note:** `event:`を使うとクライアント側のハンドリングがきれいになりますが、ブラウザのDevToolsで見たときに種類別で識別しやすくなるというメリットもあります。デバッグ時に効いてきます。

View File

@@ -0,0 +1,85 @@
---
title: "E03. SSEの再接続を処理する"
order: 49
status: "draft"
---
SSE接続はネットワークの都合で切れることがあります。クライアントは自動的に再接続を試みるので、サーバー側では「再接続してきたクライアントに、途中から配信を再開する」仕組みを用意しておくと親切です。
## `Last-Event-ID`を受け取る
クライアントが再接続すると、最後に受け取ったイベントのIDを`Last-Event-ID`ヘッダーに入れて送ってきます。サーバー側ではこれを読んで、その続きから配信を再開できます。
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
auto last_id = req.get_header_value("Last-Event-ID");
int start = last_id.empty() ? 0 : std::stoi(last_id) + 1;
res.set_chunked_content_provider(
"text/event-stream",
[start](size_t offset, httplib::DataSink &sink) mutable {
static int next_id = 0;
if (next_id < start) { next_id = start; }
std::string msg = "id: " + std::to_string(next_id) + "\n"
+ "data: event " + std::to_string(next_id) + "\n\n";
sink.write(msg.data(), msg.size());
++next_id;
std::this_thread::sleep_for(std::chrono::seconds(1));
return true;
});
});
```
初回接続では`Last-Event-ID`が無いので`0`から送り始め、再接続時は続きのIDから再開します。イベントの保存はサーバー側の責任なので、直近のイベントをキャッシュしておく必要があります。
## 再接続間隔を指定する
`retry:`フィールドを送ると、クライアント側の再接続間隔を指定できます。単位はミリ秒です。
```cpp
std::string msg = "retry: 5000\n\n"; // 5秒後に再接続
sink.write(msg.data(), msg.size());
```
通常は最初に1回送っておけば十分です。混雑時やサーバーメンテナンス時に、リトライ間隔を長めに指定して負荷を減らすといった使い方もできます。
## イベントのバッファリング
再接続のために、直近のイベントをサーバー側でバッファしておく実装が必要です。
```cpp
struct EventBuffer {
std::mutex mu;
std::deque<std::pair<int, std::string>> events; // {id, data}
int next_id = 0;
void push(const std::string &data) {
std::lock_guard<std::mutex> lock(mu);
events.push_back({next_id++, data});
if (events.size() > 1000) { events.pop_front(); }
}
std::vector<std::pair<int, std::string>> since(int id) {
std::lock_guard<std::mutex> lock(mu);
std::vector<std::pair<int, std::string>> out;
for (const auto &e : events) {
if (e.first >= id) { out.push_back(e); }
}
return out;
}
};
```
再接続してきたクライアントに`since(last_id)`で未送信分をまとめて送ると、取りこぼしを防げます。
## 保存期間のバランス
バッファをどれだけ持つかは、メモリと「どれだけさかのぼって再送できるか」のトレードオフです。用途によって決めましょう。
- リアルタイムチャット: 数分〜数十分
- 通知: 直近のN件
- 取引データ: 永続化して、必要ならDBから取得
> **Warning:** `Last-Event-ID`はクライアントが送ってくる値なので、サーバー側で信用しすぎないようにしましょう。数値として読むなら範囲チェックを、文字列ならサニタイズを忘れずに。

View File

@@ -0,0 +1,99 @@
---
title: "E04. SSEをクライアントで受信する"
order: 50
status: "draft"
---
cpp-httplibには`sse::SSEClient`という専用のクラスが用意されています。自動再接続、イベント名別のハンドラ、`Last-Event-ID`の管理まで面倒を見てくれるので、SSEを受信するときはこれを使うのが一番ラクです。
## 基本の使い方
```cpp
#include <httplib.h>
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << "data: " << msg.data << std::endl;
});
sse.start(); // ブロッキング
```
`Client`と接続先パスを渡して`SSEClient`を作り、`on_message()`でコールバックを登録します。`start()`を呼ぶとイベントループが走り、接続が切れると自動で再接続を試みます。
## イベント名で分岐する
サーバー側で`event:`を付けて送られてくる場合は、`on_event()`で名前ごとにハンドラを登録できます。
```cpp
sse.on_event("message", [](const auto &msg) {
std::cout << "chat: " << msg.data << std::endl;
});
sse.on_event("join", [](const auto &msg) {
std::cout << msg.data << " joined" << std::endl;
});
sse.on_event("leave", [](const auto &msg) {
std::cout << msg.data << " left" << std::endl;
});
```
`on_message()`は、名前なし(デフォルトの`message`イベント)を受け取る汎用ハンドラとして使えます。
## 接続イベントとエラーハンドリング
```cpp
sse.on_open([] {
std::cout << "connected" << std::endl;
});
sse.on_error([](httplib::Error err) {
std::cerr << "error: " << httplib::to_string(err) << std::endl;
});
```
接続確立時やエラー発生時にもフックを挟めます。エラーハンドラが呼ばれても、`SSEClient`は内部で再接続を試みます。
## 非同期で動かす
メインスレッドを塞ぎたくない場合は`start_async()`を使います。
```cpp
sse.start_async();
// メインスレッドは別の仕事を続ける
do_other_work();
// 終わったら止める
sse.stop();
```
`start_async()`は裏でスレッドを立ち上げてイベントループを回します。`stop()`でクリーンに止められます。
## 再接続の設定
再接続間隔や最大試行回数を調整できます。
```cpp
sse.set_reconnect_interval(5000); // 5秒
sse.set_max_reconnect_attempts(10); // 10回まで0=無制限)
```
サーバー側で`retry:`フィールドを送っていると、そちらが優先されます。
## Last-Event-IDの自動管理
`SSEClient`は受信したイベントの`id`を内部で保持していて、再接続時に`Last-Event-ID`ヘッダーとして送ってくれます。この挙動はサーバー側で`id:`付きイベントを送っていれば自動で有効になります。
```cpp
std::cout << "last id: " << sse.last_event_id() << std::endl;
```
現在のIDは`last_event_id()`で参照できます。
> **Note:** SSEClientの`start()`はブロッキングなので、単発のツールならそのまま使えますが、GUIアプリやサーバーに組み込むときは`start_async()` + `stop()`の組み合わせが基本です。
> サーバー側の実装は[E01. SSEサーバーを実装する](e01-sse-server)を参照してください。

View File

@@ -4,93 +4,93 @@ order: 0
status: "draft"
---
「〇〇をするには?」という問いに答えるレシピ集です。各レシピは独立しているので、必要なページだけ読めます。
「〇〇をするには?」という問いに答えるレシピ集です。各レシピは独立しているので、必要なページだけ読めます。基本的な使い方は[Tour](../tour/)で紹介しています。
## クライアント
### 基本
- レスポンスボディを文字列で取得する / ファイルに保存する
- JSON を送受信する
- デフォルトヘッダーを設定する`set_default_headers`
- リダイレクトを追従する`set_follow_location`
- [C01. レスポンスボディを取得する / ファイルに保存する](c01-get-response-body)
- [C02. JSONを送受信する](c02-json)
- [C03. デフォルトヘッダーを設定する](c03-default-headers)
- [C04. リダイレクトを追従する](c04-follow-location)
### 認証
- Basic 認証を使う`set_basic_auth`
- Bearer トークンで API を呼ぶ
- [C05. Basic認証を使う](c05-basic-auth)
- [C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)
### ファイル送信
- ファイルをマルチパートフォームとしてアップロードする`make_file_provider`
- ファイルを生バイナリとして POST する`make_file_body`
- チャンク転送でボディを送るContent Provider
- [C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)
- [C08. ファイルを生バイナリとしてPOSTする](c08-post-file-body)
- [C09. チャンク転送でボディを送る](c09-chunked-upload)
### ストリーミング・進捗
- レスポンスをストリーミングで受信する
- 進捗コールバックを使う`DownloadProgress` / `UploadProgress`
- [C10. レスポンスをストリーミングで受信する](c10-stream-response)
- [C11. 進捗コールバックを使う](c11-progress-callback)
### 接続・パフォーマンス
- タイムアウトを設定する`set_connection_timeout` / `set_read_timeout`
- 全体タイムアウトを設定する`set_max_timeout`
- 接続の再利用と Keep-Alive の挙動を理解する
- 圧縮を有効にする`set_compress` / `set_decompress`
- プロキシを経由してリクエストを送る`set_proxy`
- [C12. タイムアウトを設定する](c12-timeouts)
- [C13. 全体タイムアウトを設定する](c13-max-timeout)
- [C14. 接続の再利用とKeep-Aliveの挙動を理解する](c14-keep-alive)
- [C15. 圧縮を有効にする](c15-compression)
- [C16. プロキシを経由してリクエストを送る](c16-proxy)
### エラー処理・デバッグ
- エラーコードをハンドリングする`Result::error()`
- SSL エラーをハンドリングする`ssl_error()` / `ssl_backend_error()`
- クライアントにログを設定する`set_logger` / `set_error_logger`
- [C17. エラーコードをハンドリングする](c17-error-codes)
- [C18. SSLエラーをハンドリングする](c18-ssl-errors)
- [C19. クライアントにログを設定する](c19-client-logger)
## サーバー
### 基本
- GET / POST / PUT / DELETE ハンドラを登録する
- JSON リクエストを受け取り JSON レスポンスを返す
- パスパラメーターを使う`/users/:id`
- 静的ファイルサーバーを設定する`set_mount_point`
- [S01. GET / POST / PUT / DELETEハンドラを登録する](s01-handlers)
- [S02. JSONリクエストを受け取りJSONレスポンスを返す](s02-json-api)
- [S03. パスパラメーターを使う](s03-path-params)
- [S04. 静的ファイルサーバーを設定する](s04-static-files)
### ストリーミング・ファイル
- 大きなファイルをストリーミングで返す`ContentProvider`
- ファイルダウンロードレスポンスを返す`Content-Disposition`
- マルチパートデータをストリーミングで受け取る`ContentReader`
- レスポンスを圧縮して返すgzip
- [S05. 大きなファイルをストリーミングで返す](s05-stream-response)
- [S06. ファイルダウンロードレスポンスを返す](s06-download-response)
- [S07. マルチパートデータをストリーミングで受け取る](s07-multipart-reader)
- [S08. レスポンスを圧縮して返す](s08-compress-response)
### ハンドラチェーン
- 全ルートに共通の前処理をするPre-routing handler
- Post-routing handler でレスポンスヘッダーを追加するCORS など)
- Pre-request handler でルート単位の認証を行う`matched_route`
- `res.user_data` でハンドラ間データを渡す
- [S09. 全ルートに共通の前処理をする](s09-pre-routing)
- [S10. Post-routing handlerでレスポンスヘッダーを追加する](s10-post-routing)
- [S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)
- [S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)
### エラー処理・デバッグ
- カスタムエラーページを返す`set_error_handler`
- 例外をキャッチする`set_exception_handler`
- リクエストをログに記録するLogger
- クライアントが切断したか検出する`req.is_connection_closed()`
- [S13. カスタムエラーページを返す](s13-error-handler)
- [S14. 例外をキャッチする](s14-exception-handler)
- [S15. リクエストをログに記録する](s15-server-logger)
- [S16. クライアントが切断したか検出する](s16-disconnect)
### 運用・チューニング
- ポートを動的に割り当てる`bind_to_any_port`
- `listen_after_bind` で起動順序を制御する
- グレースフルシャットダウンする`stop()` とシグナルハンドリング)
- Keep-Alive を調整する`set_keep_alive_max_count` / `set_keep_alive_timeout`
- マルチスレッド数を設定する`new_task_queue`
- [S17. ポートを動的に割り当てる](s17-bind-any-port)
- [S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)
- [S19. グレースフルシャットダウンする](s19-graceful-shutdown)
- [S20. Keep-Aliveを調整する](s20-keep-alive)
- [S21. マルチスレッド数を設定する](s21-thread-pool)
- [S22. Unix domain socketで通信する](s22-unix-socket)
## TLS / セキュリティ
- OpenSSL・mbedTLS・wolfSSL の選択指針(ビルド時の `#define` の違い)
- SSL 証明書の検証を制御する(証明書の無効化・カスタム CA・カスタムコールバック
- カスタム証明書検証コールバックを使う(`set_server_certificate_verifier`
- SSL/TLS サーバーを立ち上げる(証明書・秘密鍵の設定)
- mTLSクライアント証明書による相互認証を設定する
- サーバー側でピア証明書を参照する(`req.peer_cert()` / SNI
- [T01. OpenSSL・mbedTLS・wolfSSLの選択指針](t01-tls-backends)
- [T02. SSL証明書の検証を制御する](t02-cert-verification)
- [T03. SSL/TLSサーバーを立ち上げる](t03-ssl-server)
- [T04. mTLSを設定する](t04-mtls)
- [T05. サーバー側でピア証明書を参照する](t05-peer-cert)
## SSE
- SSE サーバーを実装する
- SSE でイベント名を使い分ける
- SSE の再接続を処理する`Last-Event-ID`
- SSE をクライアントで受信する
- [E01. SSEサーバーを実装する](e01-sse-server)
- [E02. SSEでイベント名を使い分ける](e02-sse-event-names)
- [E03. SSEの再接続を処理する](e03-sse-reconnect)
- [E04. SSEをクライアントで受信する](e04-sse-client)
## WebSocket
- WebSocket エコーサーバー/クライアントを実装する
- ハートビートを設定する`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`
- 接続クローズをハンドリングする
- バイナリフレームを送受信する
- [W01. WebSocketエコーサーバークライアントを実装する](w01-websocket-echo)
- [W02. ハートビートを設定する](w02-websocket-ping)
- [W03. 接続クローズをハンドリングする](w03-websocket-close)
- [W04. バイナリフレームを送受信する](w04-websocket-binary)

View File

@@ -0,0 +1,66 @@
---
title: "S01. GET / POST / PUT / DELETEハンドラを登録する"
order: 20
status: "draft"
---
`httplib::Server`では、HTTPメソッドごとにハンドラを登録します。`Get()``Post()``Put()``Delete()`の各メソッドにパターンとラムダを渡すだけです。
## 基本の使い方
```cpp
#include <httplib.h>
int main() {
httplib::Server svr;
svr.Get("/hello", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Hello, World!", "text/plain");
});
svr.Post("/api/items", [](const httplib::Request &req, httplib::Response &res) {
// req.bodyにリクエストボディが入っている
res.status = 201;
res.set_content("Created", "text/plain");
});
svr.Put("/api/items/1", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Updated", "text/plain");
});
svr.Delete("/api/items/1", [](const httplib::Request &req, httplib::Response &res) {
res.status = 204;
});
svr.listen("0.0.0.0", 8080);
}
```
ハンドラは`(const Request&, Response&)`の2引数を受け取ります。`res.set_content()`でレスポンスボディとContent-Typeを設定し、`res.status`でステータスコードを指定します。`listen()`を呼ぶとサーバーが起動し、ブロックされます。
## クエリパラメーターを取得する
```cpp
svr.Get("/search", [](const httplib::Request &req, httplib::Response &res) {
auto q = req.get_param_value("q");
auto limit = req.get_param_value("limit");
res.set_content("q=" + q + ", limit=" + limit, "text/plain");
});
```
`req.get_param_value()`でクエリ文字列の値を取り出せます。存在するかどうかを先に調べたいなら`req.has_param("q")`を使います。
## リクエストヘッダーを読む
```cpp
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto ua = req.get_header_value("User-Agent");
res.set_content("UA: " + ua, "text/plain");
});
```
レスポンスヘッダーを追加したいときは`res.set_header("Name", "Value")`です。
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要なら[S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)を参照してください。
> パスパラメーター(`/users/:id`)を使いたい場合は[S03. パスパラメーターを使う](s03-path-params)を参照してください。

View File

@@ -0,0 +1,74 @@
---
title: "S02. JSONリクエストを受け取りJSONレスポンスを返す"
order: 21
status: "draft"
---
cpp-httplibにはJSONパーサーが含まれていません。サーバー側でも[nlohmann/json](https://github.com/nlohmann/json)などを組み合わせて使います。ここでは`nlohmann/json`を例に説明します。
## JSONを受け取って返す
```cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
int main() {
httplib::Server svr;
svr.Post("/api/users", [](const httplib::Request &req, httplib::Response &res) {
try {
auto in = nlohmann::json::parse(req.body);
nlohmann::json out = {
{"id", 42},
{"name", in["name"]},
{"created_at", "2026-04-10T12:00:00Z"},
};
res.status = 201;
res.set_content(out.dump(), "application/json");
} catch (const std::exception &e) {
res.status = 400;
res.set_content("{\"error\":\"invalid json\"}", "application/json");
}
});
svr.listen("0.0.0.0", 8080);
}
```
`req.body`はそのまま`std::string`なので、JSONライブラリに渡してパースします。レスポンスは`dump()`で文字列にして、Content-Typeに`application/json`を指定して返します。
## Content-Typeをチェックする
```cpp
svr.Post("/api/users", [](const httplib::Request &req, httplib::Response &res) {
auto content_type = req.get_header_value("Content-Type");
if (content_type.find("application/json") == std::string::npos) {
res.status = 415; // Unsupported Media Type
return;
}
// ...
});
```
厳密にJSONだけを受け付けたいときは、Content-Typeを確認してから処理しましょう。
## JSONを返すヘルパーを作る
同じパターンを何度も書くなら、小さなヘルパーを用意すると楽です。
```cpp
auto send_json = [](httplib::Response &res, int status, const nlohmann::json &j) {
res.status = status;
res.set_content(j.dump(), "application/json");
};
svr.Get("/api/health", [&](const auto &req, auto &res) {
send_json(res, 200, {{"status", "ok"}});
});
```
> **Note:** 大きなJSONボディを受け取ると、`req.body`がまるごとメモリに載ります。巨大なペイロードを扱うときは[S07. マルチパートデータをストリーミングで受け取る](s07-multipart-reader)のように、ストリーミング受信も検討しましょう。
> クライアント側の書き方は[C02. JSONを送受信する](c02-json)を参照してください。

View File

@@ -0,0 +1,53 @@
---
title: "S03. パスパラメーターを使う"
order: 22
status: "draft"
---
REST APIでよく使う`/users/:id`のような動的なパスは、パスパターンに`:name`を書くだけで使えます。マッチした値は`req.path_params`に入ります。
## 基本の使い方
```cpp
svr.Get("/users/:id", [](const httplib::Request &req, httplib::Response &res) {
auto id = req.path_params.at("id");
res.set_content("user id: " + id, "text/plain");
});
```
`/users/42`にアクセスすると、`req.path_params["id"]``"42"`が入ります。`path_params``std::unordered_map<std::string, std::string>`なので、`at()`で取り出します。
## 複数のパラメーター
パラメーターはいくつでも書けます。
```cpp
svr.Get("/orgs/:org/repos/:repo", [](const httplib::Request &req, httplib::Response &res) {
auto org = req.path_params.at("org");
auto repo = req.path_params.at("repo");
res.set_content(org + "/" + repo, "text/plain");
});
```
`/orgs/anthropic/repos/cpp-httplib`のようなパスがマッチします。
## 正規表現パターン
もっと柔軟にマッチさせたいときは、`std::regex`ベースのパターンも使えます。
```cpp
svr.Get(R"(/users/(\d+))", [](const httplib::Request &req, httplib::Response &res) {
auto id = req.matches[1];
res.set_content("user id: " + std::string(id), "text/plain");
});
```
パターンに括弧を使うと、マッチした部分が`req.matches`に入ります。`req.matches[0]`はパス全体、`req.matches[1]`以降がキャプチャです。
## どちらを使うか
- 単純なIDやスラッグなら`:name`でじゅうぶん。読みやすく、型が自明です
- 数値のみ、UUIDのみといった形式をURLで絞りたいなら正規表現が便利
- 両方を混ぜると混乱するので、プロジェクト内ではどちらかに統一するのがおすすめです
> **Note:** パスパラメーターは文字列として入ってくるので、整数として使いたい場合は`std::stoi()`などで変換してください。変換失敗のハンドリングも忘れずに。

View File

@@ -0,0 +1,55 @@
---
title: "S04. 静的ファイルサーバーを設定する"
order: 23
status: "draft"
---
HTML、CSS、画像などの静的ファイルを配信したいときは、`set_mount_point()`を使います。URLパスとローカルディレクトリを結びつけるだけで、そのディレクトリの中身がまるごと配信されます。
## 基本の使い方
```cpp
httplib::Server svr;
svr.set_mount_point("/", "./public");
svr.listen("0.0.0.0", 8080);
```
`./public/index.html``http://localhost:8080/index.html`で、`./public/css/style.css``http://localhost:8080/css/style.css`でアクセスできます。ディレクトリ構造がそのままURLに反映されます。
## 複数のマウントポイント
マウントポイントは複数登録できます。
```cpp
svr.set_mount_point("/", "./public");
svr.set_mount_point("/assets", "./dist/assets");
svr.set_mount_point("/uploads", "./var/uploads");
```
同じパスに複数のマウントを登録することもできます。その場合は登録順に探されて、見つかった最初のものが返ります。
## APIハンドラと組み合わせる
静的ファイルとAPIハンドラは共存できます。`Get()`などで登録したハンドラが優先され、マッチしなかったときにマウントポイントが探されます。
```cpp
svr.Get("/api/users", [](const auto &req, auto &res) {
res.set_content("[]", "application/json");
});
svr.set_mount_point("/", "./public");
```
これでSPAのように、`/api/*`はハンドラで、それ以外は`./public/`から配信、という構成が作れます。
## MIMEタイプを追加する
拡張子からContent-Typeを決めるマッピングは組み込みですが、カスタムの拡張子を追加できます。
```cpp
svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
```
> **Warning:** 静的ファイル配信系のメソッドは**スレッドセーフではありません**。起動後(`listen()`以降)には呼ばないでください。起動前にまとめて設定しましょう。
> ダウンロード用のレスポンスを返したい場合は[S06. ファイルダウンロードレスポンスを返す](s06-download-response)も参考になります。

View File

@@ -0,0 +1,63 @@
---
title: "S05. 大きなファイルをストリーミングで返す"
order: 24
status: "draft"
---
巨大なファイルやリアルタイムに生成されるデータをレスポンスとして返したいとき、全体をメモリに載せるのは現実的ではありません。`Response::set_content_provider()`を使うと、データをチャンクごとに生成しながら送れます。
## サイズがわかっている場合
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
size_t total_size = get_file_size("large.bin");
res.set_content_provider(
total_size, "application/octet-stream",
[](size_t offset, size_t length, httplib::DataSink &sink) {
auto data = read_range_from_file("large.bin", offset, length);
sink.write(data.data(), data.size());
return true;
});
});
```
ラムダが呼ばれるたびに`offset``length`が渡されるので、その範囲だけ読み込んで`sink.write()`で送ります。メモリには常に少量のチャンクしか載りません。
## ファイルをそのまま返す
ただファイルを返すだけなら、`set_file_content()`のほうがずっと簡単です。
```cpp
svr.Get("/download", [](const httplib::Request &req, httplib::Response &res) {
res.set_file_content("large.bin", "application/octet-stream");
});
```
内部でストリーミング送信をしてくれるので、大きなファイルでも安心です。Content-Typeを省略すれば、拡張子から自動で判定されます。
## サイズが不明な場合はチャンク転送
リアルタイムに生成されるデータなど、サイズが事前にわからないときは`set_chunked_content_provider()`を使います。HTTP chunked transfer-encodingとして送信されます。
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, httplib::DataSink &sink) {
auto chunk = produce_next_chunk();
if (chunk.empty()) {
sink.done(); // 送信終了
return true;
}
sink.write(chunk.data(), chunk.size());
return true;
});
});
```
データがもう無くなったら`sink.done()`を呼んで終了します。
> **Note:** プロバイダラムダは複数回呼ばれます。キャプチャする変数のライフタイムに気をつけてください。必要なら`std::shared_ptr`などで包みましょう。
> ファイルダウンロードとして扱いたい場合は[S06. ファイルダウンロードレスポンスを返す](s06-download-response)を参照してください。

View File

@@ -0,0 +1,50 @@
---
title: "S06. ファイルダウンロードレスポンスを返す"
order: 25
status: "draft"
---
ブラウザで開いたときにインラインで表示するのではなく、**ダウンロードダイアログ**を出したいときは、`Content-Disposition`ヘッダーを付けます。cpp-httplib側の特別なAPIではなく、普通のヘッダー設定で実現します。
## 基本の使い方
```cpp
svr.Get("/download/report", [](const httplib::Request &req, httplib::Response &res) {
res.set_header("Content-Disposition", "attachment; filename=\"report.pdf\"");
res.set_file_content("reports/2026-04.pdf", "application/pdf");
});
```
`Content-Disposition: attachment`を付けると、ブラウザが「保存しますか?」のダイアログを出します。`filename=`で保存時のデフォルト名を指定できます。
## 日本語など非ASCIIのファイル名
ファイル名に日本語やスペースが含まれる場合は、RFC 5987形式の`filename*`を使います。
```cpp
svr.Get("/download/report", [](const httplib::Request &req, httplib::Response &res) {
res.set_header(
"Content-Disposition",
"attachment; filename=\"report.pdf\"; "
"filename*=UTF-8''%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88.pdf");
res.set_file_content("reports/2026-04.pdf", "application/pdf");
});
```
`filename*=UTF-8''`の後ろはURLエンコード済みのUTF-8バイト列です。古いブラウザ向けにASCIIの`filename=`も併記しておくと安全です。
## 動的に生成したデータをダウンロードさせる
ファイルがなくても、生成した文字列をそのままダウンロードさせることもできます。
```cpp
svr.Get("/export.csv", [](const httplib::Request &req, httplib::Response &res) {
std::string csv = build_csv();
res.set_header("Content-Disposition", "attachment; filename=\"export.csv\"");
res.set_content(csv, "text/csv");
});
```
CSVエクスポートなどでよく使うパターンです。
> **Note:** ブラウザによっては`Content-Disposition`がなくても、Content-Typeを見て自動でダウンロード扱いにすることがあります。逆に、`inline`を付けるとできるだけブラウザ内で表示しようとします。

View File

@@ -0,0 +1,71 @@
---
title: "S07. マルチパートデータをストリーミングで受け取る"
order: 26
status: "draft"
---
大きなファイルをアップロードするハンドラを普通に書くと、`req.body`にリクエスト全体が載ってしまいメモリを圧迫します。`HandlerWithContentReader`を使うと、ボディをチャンクごとに受け取れます。
## 基本の使い方
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
if (req.is_multipart_form_data()) {
content_reader(
// 各パートのヘッダー
[&](const httplib::FormData &file) {
std::cout << "name: " << file.name
<< ", filename: " << file.filename << std::endl;
return true;
},
// 各パートのボディ(複数回呼ばれる)
[&](const char *data, size_t len) {
// ここでファイルに書き出すなど
return true;
});
} else {
// 普通のリクエストボディ
content_reader([&](const char *data, size_t len) {
return true;
});
}
res.set_content("ok", "text/plain");
});
```
`content_reader`は2通りの呼び方ができます。マルチパートのときは2つのコールバックヘッダー用とデータ用を渡し、そうでないときは1つのコールバックだけを渡します。
## ファイルに直接書き出す
大きなファイルをそのままディスクに書き出す例です。
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
std::ofstream ofs;
content_reader(
[&](const httplib::FormData &file) {
if (!file.filename.empty()) {
ofs.open("uploads/" + file.filename, std::ios::binary);
}
return static_cast<bool>(ofs);
},
[&](const char *data, size_t len) {
ofs.write(data, len);
return static_cast<bool>(ofs);
});
res.set_content("uploaded", "text/plain");
});
```
メモリには常に小さなチャンクしか載らないので、ギガバイト級のファイルでも扱えます。
> **Warning:** `HandlerWithContentReader`を使うと、`req.body`は**空のまま**です。ボディはコールバック内で自分で処理してください。
> クライアント側でマルチパートを送る方法は[C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)を参照してください。

View File

@@ -0,0 +1,53 @@
---
title: "S08. レスポンスを圧縮して返す"
order: 27
status: "draft"
---
cpp-httplibは、クライアントが`Accept-Encoding`で対応を表明していれば、レスポンスボディを自動で圧縮してくれます。ハンドラ側で特別なことをする必要はありません。対応しているのはgzip、Brotli、Zstdです。
## ビルド時の準備
圧縮機能を使うには、`httplib.h`をインクルードする前に対応するマクロを定義しておきます。
```cpp
#define CPPHTTPLIB_ZLIB_SUPPORT // gzip
#define CPPHTTPLIB_BROTLI_SUPPORT // brotli
#define CPPHTTPLIB_ZSTD_SUPPORT // zstd
#include <httplib.h>
```
それぞれ`zlib``brotli``zstd`をリンクする必要があります。必要な圧縮方式だけ有効にすればOKです。
## 使い方
```cpp
svr.Get("/api/data", [](const httplib::Request &req, httplib::Response &res) {
std::string body = build_large_response();
res.set_content(body, "application/json");
});
```
これだけです。クライアントが`Accept-Encoding: gzip`を送ってきていれば、cpp-httplibが自動でgzip圧縮して返します。レスポンスには`Content-Encoding: gzip``Vary: Accept-Encoding`が自動で付きます。
## 圧縮の優先順位
クライアントが複数の方式を受け入れる場合、Brotli → Zstd → gzipの順に選ばれますビルドで有効になっている中から。クライアント側では気にせず、一番効率の良い方式で圧縮されます。
## ストリーミングレスポンスも圧縮される
`set_chunked_content_provider()`で返すストリーミングレスポンスも、同じように自動で圧縮されます。
```cpp
svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, httplib::DataSink &sink) {
// ...
});
});
```
> **Note:** 小さなレスポンスは圧縮しても効果が薄く、むしろCPU時間を無駄にすることがあります。cpp-httplibは小さすぎるボディは圧縮をスキップします。
> クライアント側の挙動は[C15. 圧縮を有効にする](c15-compression)を参照してください。

View File

@@ -0,0 +1,54 @@
---
title: "S09. 全ルートに共通の前処理をする"
order: 28
status: "draft"
---
すべてのリクエストに対して共通の処理を走らせたいことがあります。認証チェック、ロギング、レート制限などです。こうした処理は`set_pre_routing_handler()`で登録します。
## 基本の使い方
```cpp
svr.set_pre_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
std::cout << req.method << " " << req.path << std::endl;
return httplib::Server::HandlerResponse::Unhandled;
});
```
Pre-routingハンドラは、**ルーティングよりも前**に呼ばれます。どのハンドラにもマッチしないリクエストも含めて、すべてのリクエストを捕まえられます。
戻り値の`HandlerResponse`がポイントです。
- `Unhandled`を返す: 通常の処理を続行(ルーティングとハンドラ呼び出し)
- `Handled`を返す: ここでレスポンスが完了したとみなし、以降の処理をスキップ
## 認証チェックに使う
全ルート共通の認証を一箇所でかけられます。
```cpp
svr.set_pre_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
if (req.path.rfind("/public", 0) == 0) {
return httplib::Server::HandlerResponse::Unhandled; // 認証不要
}
auto auth = req.get_header_value("Authorization");
if (auth.empty()) {
res.status = 401;
res.set_content("unauthorized", "text/plain");
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
認証が通らなければ`Handled`を返してその場で401を返し、通れば`Unhandled`を返して通常のルーティングに進ませます。
## 特定ルートだけに認証をかけたい場合
全ルート共通ではなく、ルート単位で認証を分けたいときは、[S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)のほうが適しています。
> **Note:** レスポンスを加工したいだけなら、`set_post_routing_handler()`のほうが適切です。[S10. Post-routing handlerでレスポンスヘッダーを追加する](s10-post-routing)を参照してください。

View File

@@ -0,0 +1,56 @@
---
title: "S10. Post-routing handlerでレスポンスヘッダーを追加する"
order: 29
status: "draft"
---
ハンドラが返したレスポンスに、あとから共通のヘッダーを追加したいことがあります。CORSヘッダー、セキュリティヘッダー、独自のリクエストIDなどです。こういうときは`set_post_routing_handler()`を使います。
## 基本の使い方
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Request-ID", generate_request_id());
});
```
Post-routingハンドラは、**ルートハンドラが実行された後、レスポンスが送信される前**に呼ばれます。ここで`res.set_header()``res.headers.erase()`を使えば、全レスポンスに対して一括でヘッダーの追加・削除ができます。
## CORSヘッダーを付ける
よくある用途がCORSです。
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
});
```
プリフライトリクエスト(`OPTIONS`には別途ハンドラを登録するか、pre-routingハンドラで処理します。
```cpp
svr.Options("/.*", [](const auto &req, auto &res) {
res.status = 204;
});
```
## セキュリティヘッダーをまとめて付ける
ブラウザ向けのセキュリティヘッダーを一箇所で管理できます。
```cpp
svr.set_post_routing_handler(
[](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Content-Type-Options", "nosniff");
res.set_header("X-Frame-Options", "DENY");
res.set_header("Referrer-Policy", "strict-origin-when-cross-origin");
});
```
どのハンドラがレスポンスを作っても、同じヘッダーが付くようになります。
> **Note:** Post-routingハンドラは、ルートにマッチしなかったリクエストや、エラーハンドラが返したレスポンスに対しても呼ばれます。ヘッダーをすべてのレスポンスに確実に付けたいときに便利です。

View File

@@ -0,0 +1,49 @@
---
title: "S11. Pre-request handlerでルート単位の認証を行う"
order: 30
status: "draft"
---
[S09. 全ルートに共通の前処理をする](s09-pre-routing)で紹介した`set_pre_routing_handler()`はルーティングの**前**に呼ばれるので、「どのルートにマッチしたか」を知れません。ルートによって認証の有無を変えたい場合は、`set_pre_request_handler()`のほうが便利です。
## Pre-routingとの違い
| フック | 呼ばれるタイミング | ルート情報 | リクエストボディ |
| --- | --- | --- | --- |
| `set_pre_routing_handler` | ルーティングの前 | 取得できない | まだ読まれていない |
| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 | まだ読まれていない |
Pre-requestハンドラなら、`req.matched_route`に「マッチしたパターン文字列」が入っているので、ルートに応じて処理を変えられます。
Pre-requestハンドラが呼ばれる時点ではボディがまだ読まれていないので、認証に失敗したリクエストなどを、巨大かもしれないボディを読み込む前に拒否できます。その代わり、`req.body`やボディから解析されるフォームフィールドはこの時点では参照できません。ヘッダ・パス・クエリパラメータ・`req.matched_route`を使って判断してください。
## ルートごとに認証を切り替える
```cpp
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
// /adminで始まるルートだけ認証を要求
if (req.matched_route.rfind("/admin", 0) == 0) {
auto token = req.get_header_value("Authorization");
if (!is_admin_token(token)) {
res.status = 403;
res.set_content("forbidden", "text/plain");
return httplib::Server::HandlerResponse::Handled;
}
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
## 戻り値の意味
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
- `Unhandled`: 通常の処理を続行(ルートハンドラが呼ばれる)
- `Handled`: ここで完了、ルートハンドラはスキップされる
## 認証情報を後続のハンドラに渡す
認証で取り出したユーザー情報などをルートハンドラに渡したいときは、`res.user_data`を使います。詳しくは[S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)を参照してください。

View File

@@ -0,0 +1,56 @@
---
title: "S12. res.user_dataでハンドラ間データを渡す"
order: 31
status: "draft"
---
Pre-requestハンドラで認証トークンをデコードして、その結果をルートハンドラで使いたい。こういう「ハンドラ間のデータ受け渡し」は、`res.user_data`に任意の型を入れて解決します。
## 基本の使い方
```cpp
struct AuthUser {
std::string id;
std::string name;
bool is_admin;
};
svr.set_pre_request_handler(
[](const httplib::Request &req, httplib::Response &res) {
auto token = req.get_header_value("Authorization");
auto user = decode_token(token); // 認証トークンをデコード
res.user_data.set("user", user);
return httplib::Server::HandlerResponse::Unhandled;
});
svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
auto *user = res.user_data.get<AuthUser>("user");
if (!user) {
res.status = 401;
return;
}
res.set_content("Hello, " + user->name, "text/plain");
});
```
`user_data.set()`で任意の型の値を保存し、`user_data.get<T>()`で取り出します。型を正しく指定しないと`nullptr`が返るので注意してください。
## よくある型
`std::string`、数値、構造体、`std::shared_ptr`など、コピーかムーブできる値なら何でも入れられます。
```cpp
res.user_data.set("user_id", std::string{"42"});
res.user_data.set("is_admin", true);
res.user_data.set("started_at", std::chrono::steady_clock::now());
```
## どこで設定し、どこで読むか
設定する側は`set_pre_routing_handler()``set_pre_request_handler()`、読む側は通常のルートハンドラ、という流れが一般的です。Pre-requestのほうがルーティング後に呼ばれるので、`req.matched_route`と組み合わせて「このルートにマッチしたときだけセット」という書き方ができます。
## 注意点
`user_data``Response`に乗っています(`req.user_data`ではありません)。これは、ハンドラには`Response&`として可変参照が渡されるためです。一見不思議ですが、「ハンドラ間で共有する可変コンテキスト」として覚えておくと素直です。
> **Warning:** `user_data.get<T>()`は型が一致しないと`nullptr`を返します。保存時と取得時で同じ型を指定してください。`AuthUser`で入れて`const AuthUser`で取ろうとすると失敗します。

View File

@@ -0,0 +1,51 @@
---
title: "S13. カスタムエラーページを返す"
order: 32
status: "draft"
---
404や500のような**ハンドラが返したエラーレスポンス**を加工したいときは、`set_error_handler()`を使います。デフォルトの味気ないエラーページを、独自のHTMLやJSONに差し替えられます。
## 基本の使い方
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
auto body = "<h1>Error " + std::to_string(res.status) + "</h1>";
res.set_content(body, "text/html");
});
```
エラーハンドラは、`res.status`が4xxまたは5xxでレスポンスが返る直前に呼ばれます。`res.set_content()`で差し替えれば、すべてのエラーレスポンスで同じテンプレートが使えます。
## ステータスコード別の処理
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
if (res.status == 404) {
res.set_content("<h1>Not Found</h1><p>" + req.path + "</p>", "text/html");
} else if (res.status >= 500) {
res.set_content("<h1>Server Error</h1>", "text/html");
}
});
```
`res.status`を見て分岐すれば、404には専用のメッセージを、5xxにはサポート窓口のリンクを、といった使い分けができます。
## JSON APIのエラーレスポンス
APIサーバーなら、エラーもJSONで返したいことが多いはずです。
```cpp
svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
nlohmann::json j = {
{"error", true},
{"status", res.status},
{"path", req.path},
};
res.set_content(j.dump(), "application/json");
});
```
これで全エラーが統一されたJSONで返ります。
> **Note:** `set_error_handler()`は、ルートハンドラが例外を投げた場合の500エラーにも呼ばれます。例外そのものの情報を取り出したい場合は`set_exception_handler()`を組み合わせましょう。[S14. 例外をキャッチする](s14-exception-handler)を参照してください。

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