Files
cpp-httplib/README.md
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

48 KiB

cpp-httplib

A C++11 single-file header-only cross platform HTTP/HTTPS library.
It's extremely easy to set up. Just include the httplib.h file in your code!

Learn more in the official documentation (built with 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. 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.

Main Features

Simple examples

Server

#define CPPHTTPLIB_OPENSSL_SUPPORT
#include "path/to/httplib.h"

// HTTP
httplib::Server svr;

// HTTPS
httplib::SSLServer svr;

svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
  res.set_content("Hello World!", "text/plain");
});

svr.listen("0.0.0.0", 8080);

Client

#define CPPHTTPLIB_OPENSSL_SUPPORT
#include "path/to/httplib.h"

// HTTP
httplib::Client cli("http://yhirose.github.io");

// HTTPS
httplib::Client cli("https://yhirose.github.io");

if (auto res = cli.Get("/hi")) {
  res->status;
  res->body;
}

SSL/TLS Support

cpp-httplib supports multiple TLS backends through an abstraction layer:

Backend Define Libraries Notes
OpenSSL CPPHTTPLIB_OPENSSL_SUPPORT libssl, libcrypto 3.0 or later required
Mbed TLS CPPHTTPLIB_MBEDTLS_SUPPORT libmbedtls, libmbedx509, libmbedcrypto 2.x and 3.x supported (auto-detected)
wolfSSL CPPHTTPLIB_WOLFSSL_SUPPORT libwolfssl 5.x supported; must build with --enable-opensslall

Note

Mbed TLS / wolfSSL limitation: get_ca_certs() and get_ca_names() only reflect CA certificates loaded via load_ca_cert_store(). Certificates loaded through set_ca_cert_path() or system certificates (load_system_certs) are not enumerable.

Note

BoringSSL (best-effort): BoringSSL builds under CPPHTTPLIB_OPENSSL_SUPPORT and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).

// Use either OpenSSL, Mbed TLS, or wolfSSL
#define CPPHTTPLIB_OPENSSL_SUPPORT   // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
#include "path/to/httplib.h"

// Server
httplib::SSLServer svr("./cert.pem", "./key.pem");

// Client
httplib::Client cli("https://localhost:1234"); // scheme + host
httplib::SSLClient cli("localhost:1234"); // host
httplib::SSLClient cli("localhost", 1234); // host, port

// Use your CA bundle
cli.set_ca_cert_path("./ca-bundle.crt");

// Disable cert verification
cli.enable_server_certificate_verification(false);

// Disable host verification
cli.enable_server_hostname_verification(false);

SSL Error Handling

When SSL operations fail, cpp-httplib provides detailed error information through ssl_error() and ssl_backend_error():

  • ssl_error() - Returns the TLS-level error code (e.g., SSL_ERROR_SSL for OpenSSL)
  • ssl_backend_error() - Returns the backend-specific error code (e.g., ERR_get_error() for OpenSSL/wolfSSL, return value for Mbed TLS)
#define CPPHTTPLIB_OPENSSL_SUPPORT  // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
#include "path/to/httplib.h"

httplib::Client cli("https://example.com");

auto res = cli.Get("/");
if (!res) {
  // Check the error type
  const auto err = res.error();

  switch (err) {
    case httplib::Error::SSLConnection:
      std::cout << "SSL connection failed, SSL error: "
                << res.ssl_error() << std::endl;
      break;

    case httplib::Error::SSLLoadingCerts:
      std::cout << "SSL cert loading failed, backend error: "
                << std::hex << res.ssl_backend_error() << std::endl;
      break;

    case httplib::Error::SSLServerVerification:
      std::cout << "SSL verification failed, verify error: "
                << res.ssl_backend_error() << std::endl;
      break;

    case httplib::Error::SSLServerHostnameVerification:
      std::cout << "SSL hostname verification failed, verify error: "
                << res.ssl_backend_error() << std::endl;
      break;

    default:
      std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
  }
}

Custom Certificate Verification

You can set a custom verification callback using tls::VerifyCallback:

httplib::Client cli("https://example.com");

cli.set_server_certificate_verifier(
    [](const httplib::tls::VerifyContext &ctx) -> bool {
      std::cout << "Subject CN: " << ctx.subject_cn() << std::endl;
      std::cout << "Issuer: " << ctx.issuer_name() << std::endl;
      std::cout << "Depth: " << ctx.depth << std::endl;
      std::cout << "Pre-verified: " << ctx.preverify_ok << std::endl;

      // Inspect SANs (Subject Alternative Names)
      for (const auto &san : ctx.sans()) {
        std::cout << "SAN: " << san.value << std::endl;
      }

      // Return true to accept, false to reject
      return ctx.preverify_ok;
    });

Peer Certificate Inspection

On the server side, you can inspect the client's peer certificate from a request handler:

httplib::SSLServer svr("./cert.pem", "./key.pem",
                       "./client-ca-cert.pem");

svr.Get("/", [](const httplib::Request &req, httplib::Response &res) {
  auto cert = req.peer_cert();
  if (cert) {
    std::cout << "Client CN: " << cert.subject_cn() << std::endl;
    std::cout << "Serial: " << cert.serial() << std::endl;
  }

  auto sni = req.sni();
  std::cout << "SNI: " << sni << std::endl;
});

Platform-specific Certificate Handling

cpp-httplib automatically integrates with the OS certificate store on macOS and Windows. This works with all TLS backends.

Platform Behavior Disable (compile time)
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:

cli.enable_windows_certificate_verification(false);

Note

When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.

Server

#include <httplib.h>

int main(void)
{
  using namespace httplib;

  Server svr;

  svr.Get("/hi", [](const Request& req, Response& res) {
    res.set_content("Hello World!", "text/plain");
  });

  // Match the request path against a regular expression
  // and extract its captures
  svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
    auto numbers = req.matches[1];
    res.set_content(numbers, "text/plain");
  });

  // Capture the second segment of the request path as "id" path param
  svr.Get("/users/:id", [&](const Request& req, Response& res) {
    auto user_id = req.path_params.at("id");
    res.set_content(user_id, "text/plain");
  });

  // Extract values from HTTP headers and URL query params
  svr.Get("/body-header-param", [](const Request& req, Response& res) {
    if (req.has_header("Content-Length")) {
      auto val = req.get_header_value("Content-Length");
    }
    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");
  });

  // If the handler takes time to finish, you can also poll the connection state
  svr.Get("/task", [&](const Request& req, Response& res) {
    const char * result = nullptr;
    process.run(); // for example, starting an external process
    while (result == nullptr) {
      sleep(1);
      if (req.is_connection_closed()) {
        process.kill(); // kill the process
        return;
      }
      result = process.stdout(); // != nullptr if the process finishes
    }
    res.set_content(result, "text/plain");
  });

  svr.Get("/stop", [&](const Request& req, Response& res) {
    svr.stop();
  });

  svr.listen("localhost", 1234);
}

Post, Put, Patch, Delete and Options methods are also supported.

Bind a socket to multiple interfaces and any available port

int port = svr.bind_to_any_port("0.0.0.0");
svr.listen_after_bind();

Static File Server

// Mount / to ./www directory
auto ret = svr.set_mount_point("/", "./www");
if (!ret) {
  // The specified base directory doesn't exist...
}

// Mount /public to ./www directory
ret = svr.set_mount_point("/public", "./www");

// Mount /public to ./www1 and ./www2 directories
ret = svr.set_mount_point("/public", "./www1"); // 1st order to search
ret = svr.set_mount_point("/public", "./www2"); // 2nd order to search

// Remove mount /
ret = svr.remove_mount_point("/");

// Remove mount /public
ret = svr.remove_mount_point("/public");
// User defined file extension and MIME type mappings
svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c");
svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c");
svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h");

The following are built-in mappings:

Extension MIME Type Extension MIME Type
css text/css mpga audio/mpeg
csv text/csv weba audio/webm
txt text/plain wav audio/wave
vtt text/vtt otf font/otf
html, htm text/html ttf font/ttf
apng image/apng woff font/woff
avif image/avif woff2 font/woff2
bmp image/bmp 7z application/x-7z-compressed
gif image/gif atom application/atom+xml
png image/png pdf application/pdf
svg image/svg+xml mjs, js text/javascript
webp image/webp json application/json
ico image/x-icon rss application/rss+xml
tif image/tiff tar application/x-tar
tiff image/tiff xhtml, xht application/xhtml+xml
jpeg, jpg image/jpeg xslt application/xslt+xml
mp4 video/mp4 xml application/xml
mpeg video/mpeg gz application/gzip
webm video/webm zip application/zip
mp3 audio/mp3 wasm application/wasm

Warning

These static file server methods are not thread-safe.

Note

On POSIX systems, the static file server rejects requests that resolve (via symlinks) to a path outside the mounted base directory. Ensure that the served directory has appropriate permissions, as managing access to the served directory is the application developer's responsibility.

File request handler

// The handler is called right before the response is sent to a client
svr.set_file_request_handler([](const Request &req, Response &res) {
  ...
});

Logging

cpp-httplib provides separate logging capabilities for access logs and error logs, similar to web servers like Nginx and Apache.

Access Logging

Access loggers capture successful HTTP requests and responses:

svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
  std::cout << req.method << " " << req.path << " -> " << res.status << std::endl;
});

Pre-compression Logging

You can also set a pre-compression logger to capture request/response data before compression is applied:

svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Response& res) {
  // Log before compression - res.body contains uncompressed content
  // Content-Encoding header is not yet set
  your_pre_compression_logger(req, res);
});

The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called.

Error Logging

Error loggers capture failed requests and connection issues. Unlike access loggers, error loggers only receive the Error and Request information, as errors typically occur before a meaningful Response can be generated.

svr.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
  std::cerr << httplib::to_string(err) << " while processing request";
  if (req) {
    std::cerr << ", client: " << req->get_header_value("X-Forwarded-For")
              << ", request: '" << req->method << " " << req->path << " " << req->version << "'"
              << ", host: " << req->get_header_value("Host");
  }
  std::cerr << std::endl;
});

Error handler

svr.set_error_handler([](const auto& req, auto& res) {
  auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
  char buf[BUFSIZ];
  snprintf(buf, sizeof(buf), fmt, res.status);
  res.set_content(buf, "text/html");
});

Exception handler

The exception handler gets called if a user routing handler throws an error.

svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) {
  auto fmt = "<h1>Error 500</h1><p>%s</p>";
  char buf[BUFSIZ];
  try {
    std::rethrow_exception(ep);
  } catch (std::exception &e) {
    snprintf(buf, sizeof(buf), fmt, e.what());
  } catch (...) { // See the following NOTE
    snprintf(buf, sizeof(buf), fmt, "Unknown Exception");
  }
  res.set_content(buf, "text/html");
  res.status = StatusCode::InternalServerError_500;
});

Caution

if you don't provide the catch (...) block for a rethrown exception pointer, an uncaught exception will end up causing the server crash. Be careful!

Pre routing handler

svr.set_pre_routing_handler([](const auto& req, auto& res) {
  if (req.path == "/hello") {
    res.set_content("world", "text/html");
    return Server::HandlerResponse::Handled;
  }
  return Server::HandlerResponse::Unhandled;
});

Post routing handler

svr.set_post_routing_handler([](const auto& req, auto& res) {
  res.set_header("ADDITIONAL_HEADER", "value");
});

Pre request handler

svr.set_pre_request_handler([](const auto& req, auto& res) {
  if (req.matched_route == "/user/:user") {
    auto user = req.path_params.at("user");
    if (user != "john") {
      res.status = StatusCode::Forbidden_403;
      res.set_content("error", "text/html");
      return Server::HandlerResponse::Handled;
    }
  }
  return Server::HandlerResponse::Unhandled;
});

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.

struct AuthContext {
  std::string user_id;
  std::string role;
};

svr.set_pre_routing_handler([](const auto& req, auto& res) {
  auto token = req.get_header_value("Authorization");
  res.user_data.set("auth", AuthContext{decode_token(token)});
  return Server::HandlerResponse::Unhandled;
});

svr.Get("/me", [](const auto& /*req*/, auto& res) {
  auto* ctx = res.user_data.get<AuthContext>("auth");
  if (!ctx) {
    res.status = StatusCode::Unauthorized_401;
    return;
  }
  res.set_content("Hello " + ctx->user_id, "text/plain");
});

Form data handling

URL-encoded form data ('application/x-www-form-urlencoded')

svr.Post("/form", [&](const auto& req, auto& res) {
  // URL query parameters and form-encoded data are accessible via req.params
  std::string username = req.get_param_value("username");
  std::string password = req.get_param_value("password");

  // Handle multiple values with same name
  auto interests = req.get_param_values("interests");

  // Check existence
  if (req.has_param("newsletter")) {
    // Handle newsletter subscription
  }
});

'multipart/form-data' POST data

svr.Post("/multipart", [&](const Request& req, Response& res) {
  // Access text fields (from form inputs without files)
  std::string username = req.form.get_field("username");
  std::string bio = req.form.get_field("bio");

  // Access uploaded files
  if (req.form.has_file("avatar")) {
    const auto& file = req.form.get_file("avatar");
    std::cout << "Uploaded file: " << file.filename
              << " (" << file.content_type << ") - "
              << file.content.size() << " bytes" << std::endl;

    // Access additional headers if needed
    for (const auto& header : file.headers) {
      std::cout << "Header: " << header.first << " = " << header.second << std::endl;
    }

    // IMPORTANT: file.filename is an untrusted value from the client.
    // Always sanitize to prevent path traversal attacks.
    auto safe_name = httplib::sanitize_filename(file.filename);
    if (safe_name.empty()) {
      res.status = StatusCode::BadRequest_400;
      res.set_content("Invalid filename", "text/plain");
      return;
    }

    // Save to disk
    std::ofstream ofs(upload_dir + "/" + safe_name, std::ios::binary);
    ofs << file.content;
  }

  // Handle multiple values with same name
  auto tags = req.form.get_fields("tags");  // e.g., multiple checkboxes
  for (const auto& tag : tags) {
    std::cout << "Tag: " << tag << std::endl;
  }

  auto documents = req.form.get_files("documents");  // multiple file upload
  for (const auto& doc : documents) {
    std::cout << "Document: " << doc.filename
              << " (" << doc.content.size() << " bytes)" << std::endl;
  }

  // Check existence before accessing
  if (req.form.has_field("newsletter")) {
    std::cout << "Newsletter subscription: " << req.form.get_field("newsletter") << std::endl;
  }

  // Get counts for validation
  if (req.form.get_field_count("tags") > 5) {
    res.status = StatusCode::BadRequest_400;
    res.set_content("Too many tags", "text/plain");
    return;
  }

  // Summary
  std::cout << "Received " << req.form.fields.size() << " text fields and "
            << req.form.files.size() << " files" << std::endl;

  res.set_content("Upload successful", "text/plain");
});

Filename Sanitization

file.filename in multipart uploads is an untrusted value from the client. Always sanitize before using it in file paths:

auto safe = httplib::sanitize_filename(file.filename);

This function strips path separators (/, \), null bytes, leading/trailing whitespace, and rejects . and ... Returns an empty string if the filename is unsafe.

Receive content with a content receiver

svr.Post("/content_receiver",
  [&](const Request &req, Response &res, const ContentReader &content_reader) {
    if (req.is_multipart_form_data()) {
      // NOTE: `content_reader` is blocking until every form data field is read
      // This approach allows streaming processing of large files
      std::vector<FormData> items;
      content_reader(
        [&](const FormData &item) {
          items.push_back(item);
          return true;
        },
        [&](const char *data, size_t data_length) {
          items.back().content.append(data, data_length);
          return true;
        });

      // Process the received items
      for (const auto& item : items) {
        if (item.filename.empty()) {
          // Text field
          std::cout << "Field: " << item.name << " = " << item.content << std::endl;
        } else {
          // File
          std::cout << "File: " << item.name << " (" << item.filename << ") - "
                    << item.content.size() << " bytes" << std::endl;
        }
      }
    } else {
      std::string body;
      content_reader([&](const char *data, size_t data_length) {
        body.append(data, data_length);
        return true;
      });
    }
  });

Send content with the content provider

const size_t DATA_CHUNK_SIZE = 4;

svr.Get("/stream", [&](const Request &req, Response &res) {
  auto data = new std::string("abcdefg");

  res.set_content_provider(
    data->size(), // Content length
    "text/plain", // Content type
    [&, data](size_t offset, size_t length, DataSink &sink) {
      const auto &d = *data;
      sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE));
      return true; // return 'false' if you want to cancel the process.
    },
    [data](bool success) { delete data; });
});

Without content length:

svr.Get("/stream", [&](const Request &req, Response &res) {
  res.set_content_provider(
    "text/plain", // Content type
    [&](size_t offset, DataSink &sink) {
      if (/* there is still data */) {
        std::vector<char> data;
        // prepare data...
        sink.write(data.data(), data.size());
      } else {
        sink.done(); // No more data
      }
      return true; // return 'false' if you want to cancel the process.
    });
});

Chunked transfer encoding

svr.Get("/chunked", [&](const Request& req, Response& res) {
  res.set_chunked_content_provider(
    "text/plain",
    [](size_t offset, DataSink &sink) {
      sink.write("123", 3);
      sink.write("345", 3);
      sink.write("789", 3);
      sink.done(); // No more data
      return true; // return 'false' if you want to cancel the process.
    }
  );
});

With trailer:

svr.Get("/chunked", [&](const Request& req, Response& res) {
  res.set_header("Trailer", "Dummy1, Dummy2");
  res.set_chunked_content_provider(
    "text/plain",
    [](size_t offset, DataSink &sink) {
      sink.write("123", 3);
      sink.write("345", 3);
      sink.write("789", 3);
      sink.done_with_trailer({
        {"Dummy1", "DummyVal1"},
        {"Dummy2", "DummyVal2"}
      });
      return true;
    }
  );
});

Send file content

svr.Get("/content", [&](const Request &req, Response &res) {
  res.set_file_content("./path/to/content.html");
});

svr.Get("/content", [&](const Request &req, Response &res) {
  res.set_file_content("./path/to/content", "text/html");
});

'Expect: 100-continue' handler

By default, the server sends a 100 Continue response for an Expect: 100-continue header.

// Send a '417 Expectation Failed' response.
svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
  return StatusCode::ExpectationFailed_417;
});
// Send a final status without reading the message body.
svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
  return res.status = StatusCode::Unauthorized_401;
});

Keep-Alive connection

svr.set_keep_alive_max_count(2); // Default is 100
svr.set_keep_alive_timeout(10);  // Default is 5

Timeout

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

svr.set_payload_max_length(1024 * 1024 * 512); // 512MB

Note

When the request body content type is 'www-form-urlencoded', the actual payload length shouldn't exceed CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH.

Server-Sent Events

Please see Server example and Client example.

Default thread pool support

ThreadPool is used as the default task queue, with dynamic scaling support. By default, it maintains a base thread count of 8 or std::thread::hardware_concurrency() - 1 (whichever is greater), and can scale up to 4x that count under load. You can change these with CPPHTTPLIB_THREAD_POOL_COUNT and CPPHTTPLIB_THREAD_POOL_MAX_COUNT.

When all threads are busy and a new task arrives, a temporary thread is spawned (up to the maximum). When a dynamic thread finishes its task and the queue is empty, or after an idle timeout, it exits automatically. The idle timeout defaults to 3 seconds, configurable via CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT.

If you want to set the thread counts at runtime:

svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/8, /*max_threads=*/64); };

Max queued requests

You can also provide an optional parameter to limit the maximum number of pending requests, i.e. requests accept()ed by the listener but still waiting to be serviced by worker threads.

svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/12, /*max_threads=*/0, /*max_queued_requests=*/18); };

Default limit is 0 (unlimited). Once the limit is reached, the listener will shutdown the client connection.

Override the default thread pool with yours

You can supply your own thread pool implementation according to your need.

class YourThreadPoolTaskQueue : public TaskQueue {
public:
  YourThreadPoolTaskQueue(size_t n) {
    pool_.start_with_thread_count(n);
  }

  virtual bool enqueue(std::function<void()> fn) override {
    /* Return true if the task was actually enqueued, or false
     * if the caller must drop the corresponding connection. */
    return pool_.enqueue(fn);
  }

  virtual void shutdown() override {
    pool_.shutdown_gracefully();
  }

private:
  YourThreadPool pool_;
};

svr.new_task_queue = [] {
  return new YourThreadPoolTaskQueue(12);
};

Client

#include <httplib.h>
#include <iostream>

int main(void)
{
  httplib::Client cli("localhost", 1234);

  if (auto res = cli.Get("/hi")) {
    if (res->status == StatusCode::OK_200) {
      std::cout << res->body << std::endl;
    }
  } else {
    auto err = res.error();
    std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
  }
}

Tip

Constructor with scheme-host-port string is now supported!

httplib::Client cli("localhost");
httplib::Client cli("localhost:8080");
httplib::Client cli("http://localhost");
httplib::Client cli("http://localhost:8080");
httplib::Client cli("https://localhost");
httplib::SSLClient cli("localhost");

Error code

Here is the list of errors from Result::error().

enum class Error {
  Success = 0,
  Unknown,
  Connection,
  BindIPAddress,
  Read,
  Write,
  ExceedRedirectCount,
  Canceled,
  SSLConnection,
  SSLLoadingCerts,
  SSLServerVerification,
  SSLServerHostnameVerification,
  UnsupportedMultipartBoundaryChars,
  Compression,
  ConnectionTimeout,
  ProxyConnection,
  ConnectionClosed,
  Timeout,
  ResourceExhaustion,
  TooManyFormDataFiles,
  ExceedMaxPayloadSize,
  ExceedUriMaxLength,
  ExceedMaxSocketDescriptorCount,
  InvalidRequestLine,
  InvalidHTTPMethod,
  InvalidHTTPVersion,
  InvalidHeaders,
  MultipartParsing,
  OpenFile,
  Listen,
  GetSockName,
  UnsupportedAddressFamily,
  HTTPParsing,
  InvalidRangeHeader,
};

Client Logging

Access Logging

cli.set_logger([](const httplib::Request& req, const httplib::Response& res) {
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
    std::chrono::steady_clock::now() - start_time).count();
  std::cout << "✓ " << req.method << " " << req.path
            << " -> " << res.status << " (" << res.body.size() << " bytes, "
            << duration << "ms)" << std::endl;
});

Error Logging

cli.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
  std::cerr << "✗ ";
  if (req) {
    std::cerr << req->method << " " << req->path << " ";
  }
  std::cerr << "failed: " << httplib::to_string(err);

  // Add specific guidance based on error type
  switch (err) {
    case httplib::Error::Connection:
      std::cerr << " (verify server is running and reachable)";
      break;
    case httplib::Error::SSLConnection:
      std::cerr << " (check SSL certificate and TLS configuration)";
      break;
    case httplib::Error::ConnectionTimeout:
      std::cerr << " (increase timeout or check network latency)";
      break;
    case httplib::Error::Read:
      std::cerr << " (server may have closed connection prematurely)";
      break;
    default:
      break;
  }
  std::cerr << std::endl;
});

GET with HTTP headers

httplib::Headers headers = {
  { "Hello", "World!" }
};
auto res = cli.Get("/hi", headers);

or

auto res = cli.Get("/hi", {{"Hello", "World!"}});

or

cli.set_default_headers({
  { "Hello", "World!" }
});
auto res = cli.Get("/hi");

POST

res = cli.Post("/post", "text", "text/plain");
res = cli.Post("/person", "name=john1&note=coder", "application/x-www-form-urlencoded");

POST with parameters

httplib::Params params;
params.emplace("name", "john");
params.emplace("note", "coder");

auto res = cli.Post("/post", params);

or

httplib::Params params{
  { "name", "john" },
  { "note", "coder" }
};

auto res = cli.Post("/post", params);

POST with Multipart Form Data

httplib::UploadFormDataItems items = {
  { "text1", "text default", "", "" },
  { "text2", "aωb", "", "" },
  { "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
  { "file2", "{\n  \"world\", true\n}\n", "world.json", "application/json" },
  { "file3", "", "", "application/octet-stream" },
};

auto res = cli.Post("/multipart", items);

To upload files from disk without loading them entirely into memory, use make_file_provider. The file is sent with chunked transfer encoding.

httplib::FormDataProviderItems providers = {
  httplib::make_file_provider("file1", "/path/to/large.bin", "large.bin", "application/octet-stream"),
  httplib::make_file_provider("avatar", "/path/to/photo.jpg", "photo.jpg", "image/jpeg"),
};

auto res = cli.Post("/upload", {}, {}, providers);

POST with a file body

To POST a file as a raw binary body with Content-Length, use make_file_body.

auto [size, provider] = httplib::make_file_body("/path/to/data.bin");
auto res = cli.Post("/upload", size, provider, "application/octet-stream");

PUT

res = cli.Put("/resource/foo", "text", "text/plain");

PATCH

res = cli.Patch("/resource/foo", "text", "text/plain");

DELETE

res = cli.Delete("/resource/foo");

OPTIONS

res = cli.Options("*");
res = cli.Options("/resource/foo");

Timeout

cli.set_connection_timeout(0, 300000); // 300 milliseconds
cli.set_read_timeout(5, 0); // 5 seconds
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

cli.set_payload_max_length(1024 * 1024 * 512); // 512MB

Receive content with a content receiver

std::string body;

auto res = cli.Get("/large-data",
  [&](const char *data, size_t data_length) {
    body.append(data, data_length);
    return true;
  });
std::string body;

auto res = cli.Get(
  "/stream", Headers(),
  [&](const Response &response) {
    EXPECT_EQ(StatusCode::OK_200, response.status);
    return true; // return 'false' if you want to cancel the request.
  },
  [&](const char *data, size_t data_length) {
    body.append(data, data_length);
    return true; // return 'false' if you want to cancel the request.
  });

Send content with a content provider

std::string body = ...;

auto res = cli.Post(
  "/stream", body.size(),
  [](size_t offset, size_t length, DataSink &sink) {
    sink.write(body.data() + offset, length);
    return true; // return 'false' if you want to cancel the request.
  },
  "text/plain");

Chunked transfer encoding

auto res = cli.Post(
  "/stream",
  [](size_t offset, DataSink &sink) {
    sink.os << "chunked data 1";
    sink.os << "chunked data 2";
    sink.os << "chunked data 3";
    sink.done();
    return true; // return 'false' if you want to cancel the request.
  },
  "text/plain");

With Progress Callback

httplib::Client cli(url, port);

// prints: 0 / 000 bytes => 50% complete
auto res = cli.Get("/", [](size_t len, size_t total) {
  printf("%lld / %lld bytes => %d%% complete\n",
    len, total,
    (int)(len*100/total));
  return true; // return 'false' if you want to cancel the request.
}
);

progress

Authentication

// Basic Authentication
cli.set_basic_auth("user", "pass");

// Digest Authentication
cli.set_digest_auth("user", "pass");

// Bearer Token Authentication
cli.set_bearer_token_auth("token");

Note

OpenSSL is required for Digest Authentication.

Proxy server support

cli.set_proxy("host", port);

// Basic Authentication
cli.set_proxy_basic_auth("user", "pass");

// Digest Authentication
cli.set_proxy_digest_auth("user", "pass");

// Bearer Token Authentication
cli.set_proxy_bearer_token_auth("pass");

Note

OpenSSL is required for Digest Authentication.

Bypass the proxy for specific hosts (NO_PROXY)

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 for details and for reading the variable from the environment.

Range

httplib::Client cli("httpcan.org");

auto res = cli.Get("/range/32", {
  httplib::make_range_header({{1, 10}}) // 'Range: bytes=1-10'
});
// res->status should be 206.
// res->body should be "bcdefghijk".
httplib::make_range_header({{1, 10}, {20, -1}})      // 'Range: bytes=1-10, 20-'
httplib::make_range_header({{100, 199}, {500, 599}}) // 'Range: bytes=100-199, 500-599'
httplib::make_range_header({{0, 0}, {-1, 1}})        // 'Range: bytes=0-0, -1'

Keep-Alive connection

httplib::Client cli("localhost", 1234);

cli.Get("/hello");         // with "Connection: close"

cli.set_keep_alive(true);
cli.Get("/world");

cli.set_keep_alive(false);
cli.Get("/last-request");  // with "Connection: close"

Redirect

httplib::Client cli("yahoo.com");

auto res = cli.Get("/");
res->status; // 301

cli.set_follow_location(true);
res = cli.Get("/");
res->status; // 200

Use a specific network interface

Note

This feature is not available on Windows, yet.

cli.set_interface("eth0"); // Interface name, IP address or host name

Automatic Path Encoding

The client automatically encodes special characters in URL paths by default:

httplib::Client cli("https://example.com");

// Automatic path encoding (default behavior)
cli.set_path_encode(true);
auto res = cli.Get("/path with spaces/file.txt"); // Automatically encodes spaces

// Disable automatic path encoding
cli.set_path_encode(false);
auto res = cli.Get("/already%20encoded/path"); // Use pre-encoded paths
  • set_path_encode(bool on) - Controls automatic encoding of special characters in URL paths
    • true (default): Automatically encodes spaces, plus signs, newlines, and other special characters
    • false: Sends paths as-is without encoding (useful for pre-encoded URLs)

Performance Note for Local Connections

Warning

On Windows systems with improperly configured IPv6 settings, using "localhost" as the hostname may cause significant connection delays (up to 2 seconds per request) due to DNS resolution issues. This affects both client and server operations. For better performance when connecting to local services, use "127.0.0.1" instead of "localhost".

See: https://github.com/yhirose/cpp-httplib/issues/366#issuecomment-593004264

// May be slower on Windows due to DNS resolution delays
httplib::Client cli("localhost", 8080);
httplib::Server svr;
svr.listen("localhost", 8080);

// Faster alternative for local connections
httplib::Client cli("127.0.0.1", 8080);
httplib::Server svr;
svr.listen("127.0.0.1", 8080);

Payload Limit

The maximum payload body size is limited to 100MB by default for both server and client. You can change it with set_payload_max_length() or by defining CPPHTTPLIB_PAYLOAD_MAX_LENGTH at compile time. Setting it to 0 disables the limit entirely.

Compression

The server can apply compression to the following MIME type contents:

  • all text types except text/event-stream
  • image/svg+xml
  • application/javascript
  • application/json
  • application/xml
  • application/protobuf
  • application/xhtml+xml

Zlib Support

'gzip' compression is available with CPPHTTPLIB_ZLIB_SUPPORT. libz should be linked.

Brotli Support

Brotli compression is available with CPPHTTPLIB_BROTLI_SUPPORT. Necessary libraries should be linked. Please see https://github.com/google/brotli for more detail.

Zstd Support

Zstd compression is available with CPPHTTPLIB_ZSTD_SUPPORT. Necessary libraries should be linked. Please see https://github.com/facebook/zstd for more detail.

Default Accept-Encoding value

The default Accept-Encoding value contains all possible compression types. So, the following two examples are same.

res = cli.Get("/resource/foo");
res = cli.Get("/resource/foo", {{"Accept-Encoding", "br, gzip, deflate, zstd"}});

If we don't want a response without compression, we have to set Accept-Encoding to an empty string. This behavior is similar to curl.

res = cli.Get("/resource/foo", {{"Accept-Encoding", ""}});

Compress request body on client

cli.set_compress(true);
res = cli.Post("/resource/foo", "...", "text/plain");

Compress response body on client

cli.set_decompress(false);
res = cli.Get("/resource/foo");
res->body; // Compressed data

Unix Domain Socket Support

Unix Domain Socket support is available on Linux and macOS.

// Server
httplib::Server svr;
svr.set_address_family(AF_UNIX).listen("./my-socket.sock", 80);

// Client
httplib::Client cli("./my-socket.sock");
cli.set_address_family(AF_UNIX);

"my-socket.sock" can be a relative path or an absolute path. Your application must have the appropriate permissions for the path. You can also use an abstract socket address on Linux. To use an abstract socket address, prepend a null byte ('\x00') to the path.

This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior:

URI Encoding/Decoding Utilities

cpp-httplib provides utility functions for URI encoding and decoding:

#include <httplib.h>

std::string url = "https://example.com/search?q=hello world";
std::string encoded = httplib::encode_uri(url);
std::string decoded = httplib::decode_uri(encoded);

std::string param = "hello world";
std::string encoded_component = httplib::encode_uri_component(param);
std::string decoded_component = httplib::decode_uri_component(encoded_component);

Functions

  • encode_uri(const std::string &value) - Encodes a full URI, preserving reserved characters like ://, ?, &, =
  • decode_uri(const std::string &value) - Decodes a URI-encoded string
  • encode_uri_component(const std::string &value) - Encodes a URI component (query parameter, path segment), encoding all reserved characters
  • decode_uri_component(const std::string &value) - Decodes a URI component

Use encode_uri() for full URLs and encode_uri_component() for individual query parameters or path segments.

Stream API

Process large responses without loading everything into memory.

httplib::Client cli("localhost", 8080);
cli.set_follow_location(true);
...

auto result = httplib::stream::Get(cli, "/large-file");
if (result) {
  while (result.next()) {
    process(result.data(), result.size());  // Process each chunk as it arrives
  }
}

// Or read the entire body at once
auto result2 = httplib::stream::Get(cli, "/file");
if (result2) {
  std::string body = result2.read_all();
}

All HTTP methods are supported: stream::Get, Post, Put, Patch, Delete, Head, Options.

See README-stream.md for more details.

SSE Client

#include <httplib.h>

int main() {
    httplib::Client cli("http://localhost:8080");
    httplib::sse::SSEClient sse(cli, "/events");

    sse.on_message([](const httplib::sse::SSEMessage &msg) {
        std::cout << "Event: " << msg.event << std::endl;
        std::cout << "Data: " << msg.data << std::endl;
    });

    sse.start();  // Blocking, with auto-reconnect
    return 0;
}

See README-sse.md for more details.

WebSocket

// Server
httplib::Server svr;

svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
    httplib::ws::Message msg;
    while (ws.read(msg)) {
        if (msg.is_text()) {
            ws.send("Echo: " + msg.data);
        }
    }
});

svr.listen("localhost", 8080);
// Client
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");

if (ws.connect()) {
    ws.send("Hello, WebSocket!");

    std::string msg;
    if (ws.read(msg)) {
        std::cout << "Received: " << msg << std::endl;
    }

    ws.close();
}

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 (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 for more details.

Socket Option Utility

set_socket_opt is a convenience wrapper around setsockopt for setting integer socket options:

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

$ ./split.py -h
usage: split.py [-h] [-e EXTENSION] [-o OUT]

This script splits httplib.h into .h and .cc parts.

optional arguments:
  -h, --help            show this help message and exit
  -e EXTENSION, --extension EXTENSION
                        extension of the implementation file (default: cc)
  -o OUT, --out OUT     where to write the files (default: out)

$ ./split.py
Wrote out/httplib.h and out/httplib.cc

Dockerfile for Static HTTP Server

Dockerfile for static HTTP server is available. Port number of this HTTP server is 80, and it serves static files from /html directory in the container.

> docker build -t cpp-httplib-server .
...

> docker run --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server
Serving HTTP on 0.0.0.0 port 80 ...
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."

From Docker Hub

> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server
Serving HTTP on 0.0.0.0 port 80 ...
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."

NOTE

Regular Expression Stack Overflow

Caution

When using complex regex patterns in route handlers, be aware that certain patterns may cause stack overflow during pattern matching. This is a known issue with std::regex implementations and affects the dispatch_request() method.

// This pattern can cause stack overflow with large input
svr.Get(".*", handler);

Consider using simpler patterns or path parameters to avoid this issue:

// Safer alternatives
svr.Get("/users/:id", handler);           // Path parameters
svr.Get(R"(/api/v\d+/.*)", handler);     // More specific patterns

g++

g++ 4.8 and below cannot build this library since <regex> in the versions are broken.

Windows

Include httplib.h before Windows.h or include Windows.h by defining WIN32_LEAN_AND_MEAN beforehand.

#include <httplib.h>
#include <Windows.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <httplib.h>

Note

cpp-httplib officially supports only the latest Visual Studio. It might work with former versions of Visual Studio, but I can no longer verify it. Pull requests are always welcome for the older versions of Visual Studio unless they break the C++11 conformance.

Note

Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.

License

MIT license (© 2026 Yuji Hirose)

Special Thanks To

These folks made great contributions to polish this library to totally another level from a simple toy!