Compare commits

..
1581 Commits
Author SHA1 Message Date
yhirose 00afaaed6a Release v0.60.1 2026-10-07 23:40:36 -04:00
yhirose e803f5e413 Let the SSL chain policy alone judge the Windows chain (#2618)
verify_cert_with_windows_schannel() rejected a chain whenever
TrustStatus.dwErrorStatus was non-zero, before
CertVerifyCertificateChainPolicy() ran. The
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS flag passed to that
policy check was therefore dead code: a certificate without revocation
information, or one whose CRL could not be fetched, failed with
CERT_TRUST_REVOCATION_STATUS_UNKNOWN.

Drop the pre-check so the SSL chain policy is the only judge.
Revocation checking becomes best-effort: a revoked certificate and
every other chain error are still rejected, while an undetermined
revocation status is accepted.

On a rejected chain, ssl_backend_error() now holds the policy status,
such as CERT_E_UNTRUSTEDROOT, instead of the trust status bit mask.
2026-10-07 23:19:36 -04:00
Michael Weitzel 57b8aca6da Avoid name clash with class WebSocketClient by explicit qualification (#2616)
Adds an explicit namespace qualification and a forward declaration for
class WebSocketClient. This change avoids a name clash on code bases
containing unrelated WebSocketClient classes.
2026-10-07 22:38:50 -04:00
KBSandyoudie006 213029685a Serve the whole body for a suffix range longer than it (#2615)
RFC 9110 14.1.2: if the representation is shorter than the suffix-length,
the entire representation is used. range_error computed a negative first
byte position for bytes=-8 on a 7-byte body and answered 416. Clamp it
at 0, as #711 did before the range handling was reworked.

Co-authored-by: youdie006 <youdie006@users.noreply.github.com>
2026-10-07 22:20:53 -04:00
yhirose 17eeb18feb Release v0.60.0 2026-10-06 22:57:42 -04:00
metsw24-maxandyhirose edc9760005 use the SAN type tag in Mbed TLS verify_hostname and get_cert_sans (#2614)
* use the SAN type tag in Mbed TLS verify_hostname and get_cert_sans

Mbed TLS keeps a subjectAltName entry's GeneralName tag in buf.tag and the bare value in buf.p / buf.len. verify_hostname ignored the tag, so a dNSName whose bytes equal an address authenticated that IP host, and an iPAddress or rfc822Name was matched as a DNS pattern. get_cert_sans looked for the tag inside the value, so it reported no entries for an ordinary certificate, or part of a dNSName as an entry of its own.

* Shorten the SAN type comments

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-10-05 21:13:50 -04:00
yhirose 6d1049462d Ignore a subprotocol the WebSocket client did not offer
A SubProtocolSelector could return a value outside the client's
Sec-WebSocket-Protocol list, and the server sent it back as is. The
server now treats such a value as no selection.
2026-10-04 18:34:21 -04:00
metsw24-maxandyhirose 4fcbc08f2d reject an unoffered subprotocol in read_websocket_upgrade_response (#2595)
* reject an unoffered subprotocol in the ws client handshake

* test the subprotocol check through WebSocketClient so the split build compiles

* Trim comments in the subprotocol check

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-10-04 18:21:39 -04:00
Jordan Woyak d59f3e438c Remove semantically redundant closed_ checks in ping thread. (#2613) 2026-10-04 09:39:24 -04:00
yhirose 09fa6820fe Fix 303 redirect request bodies and Location path decoding (Fix #2606, #2607)
A 303 response turns the follow-up request into a GET, but only the buffered body and headers were cleared. A content provider (sized or chunked) stayed on the request, so the original payload was sent again, and for a chunked provider the unframed chunks also broke the keep-alive connection.

The redirect also percent-decoded the Location path before sending it. That turned %23, %3F and %25 into a fragment, a query delimiter and a different octet, so the client requested a different resource than the one named, and made set_path_encode(false) fail on any Location containing %20. The path is now sent as given.
2026-10-03 19:06:45 -04:00
yhirose 438319cfcb Fix WebSocket pings being sent early on spurious wakeups
The heartbeat thread waited on ping_cv_ without a predicate, so a
spurious wakeup ended the wait early and sent a ping before
ping_interval_sec_ had elapsed. With max_missed_pongs enabled, the
early ping also counted toward the pong timeout.

Pass a predicate to wait_for so that the wait only ends when the
interval elapses or the connection is closed.

Reported in #2612.
2026-10-03 17:47:30 -04:00
yhirose eda9a10bfe Fix SSE client not clearing Last-Event-ID on an empty id field (Fix #2611)
An event with an empty id field must reset the last event ID, so that
no Last-Event-ID header is sent on reconnect. run_event_loop only
updated last_event_id_ when the id was non-empty, so it could not tell
an empty id field from an event with no id field, and kept sending the
stale ID.

Track whether an id field was seen with a has_id flag, as has_data
does for the data field, and add a regression test.
2026-10-03 17:29:33 -04:00
yhirose cf3693cb5c Release v0.59.0 2026-10-02 22:06:39 -04:00
yhirose 4fd9ae8f42 Serve pipelined requests without waiting for the keep-alive timeout
A client may pipeline its requests (RFC 9112 9.3.2). The server read the
following request(s) into a per-request SocketStream buffer, discarded them
with the stream after the first response, and then waited in keep_alive()
for socket data that never came, closing the connection after the
keep-alive timeout. Over TLS the bytes stayed decrypted in the TLS library,
where keep_alive() could not see them either.

Create one stream per connection and serve a request that is already
buffered (Stream::is_readable()) without waiting in keep_alive().

Keeping the buffer means an extra CRLF that some clients send after a
request body is now parsed as the next request line, which answered 400
and closed the connection. Ignore one empty line before the request-line,
as RFC 9112 2.2 recommends.

Fixes #2599
2026-10-02 21:47:18 -04:00
yhirose 3d40dfc727 Fix SSE parsing of CRLF field names and data-less events
A field line without a colon kept the \r of a CRLF line ending in its
name, so "data\r\n" was not recognized. Strip the \r once per line
before parsing instead of from each value.

An event without a data field was neither dispatched nor cleared, so
its event type leaked into the next event and its id only reached
last_event_id after a later event was dispatched. Reset the message on
every blank line and record the id even when nothing is dispatched.

The parsing tests exercised a copy of parse_sse_line that had drifted
from the real one. Run them through SSEClient against a local server
instead, and add regression tests for the cases above.
2026-10-02 21:38:39 -04:00
metsw24-max dd71728110 escape quoted-string auth-params in make_digest_authentication_header (#2597) 2026-10-02 21:29:35 -04:00
DosX 7255a7e979 Preserve empty SSE data fields (#2594)
SSE events may contain an empty data field, and a data field without a colon also has an empty value. Track whether a data field was seen separately from the accumulated payload so empty events are dispatched and leading empty lines are preserved. Add an integration regression test for both forms.
2026-10-02 21:17:47 -04:00
yhirose 8a3abfb597 Extract parse_int_in_range from parse_port
parse_port and the NO_PROXY CIDR prefix parsing each repeated the same
from_chars call, full-consumption check and range check. Move that into
a parse_int_in_range helper and use it in both places.
2026-10-02 21:15:14 -04:00
KBS 10aadd57f7 Reject trailing characters in URL port numbers (#2593)
parse_port checked only the error code of from_chars, which stops at
the first non-digit, so http://host:80abc was accepted as port 80, and
a redirect Location with such a port was followed. RFC 3986 defines
port as *DIGIT. Require the whole string to be consumed, as #2590 does
for quality values.
2026-10-02 21:07:34 -04:00
yhirose 43863e1f67 Make CryptoAPI the only chain verifier when Windows verification is on (#2604)
Every backend loads the Windows ROOT and CA stores, but Windows adds a
root to them only when CryptoAPI needs it to build a chain. On a machine
that had not needed a root yet, the backend rejected the chain before
the CryptoAPI check ran, so Windows never fetched the root (for example
OpenSSL error 20 for accounts.spotify.com under Starfield Root G2).

With Windows verification enabled, the backend's chain verdict is no
longer used. Mbed TLS and wolfSSL skip chain verification during the
handshake, and the post-handshake verify result is ignored. The
CryptoAPI check becomes mandatory: a leaf that cannot be encoded now
fails the connection instead of skipping the check, and the chain must
allow server authentication, which the backends used to check.

A server certificate verifier works on the backend's chain verification,
so when one is set the backend still decides and CryptoAPI only adds its
own check, as before.

ClientCertMissing now disables hostname verification: cert.pem does not
match HOST, and on Windows the hostname check fails before the chain
check.

Refs #2596
2026-10-02 20:48:47 -04:00
yhirose 0db1df7cf2 Pass the server's intermediates to Windows certificate verification (#2602)
CryptoAPI got only the leaf, so it fetched an issuer from the leaf's AIA
URL instead of using the intermediates the server sent. For
accounts.spotify.com that issuer chains to Certainly Root R1, which
Windows does not trust, while the server's own chain ends at Starfield
Root G2.

Add tls::get_peer_certs(), which returns the certificates the peer sent
in the same way get_ca_certs() returns the CA certificates, for every
backend. The CryptoAPI check puts them into a memory store that it
passes to CertGetCertificateChain(), skipping any certificate that
cannot be added. wolfSSL keeps the received chain only when built with
SESSION_CERTS; without it, CryptoAPI still gets the leaf alone.

Refs #2596
2026-10-02 20:48:10 -04:00
yhirose 639391ad7f Reject an invalid Content-Length and honor Connection: close
A request whose Content-Length was present but not a valid decimal length
(e.g. "42, 42", "+42", "0x2e" or empty) was treated as having no body
unless a handler read it: no 400 was returned, the body was not drained,
and the bytes after the header block were parsed as the next request on
the keep-alive connection. Reject such a request with 400 and close the
connection before routing, as RFC 9112 Section 6.3 requires.

The server also kept reading after a response that announced
Connection: close. A rejected request line or header block left the rest
of the message to be parsed as a new request, and an error response to a
bodyless request did the same with whatever followed. Close the
connection whenever the final response carries Connection: close
(RFC 9112 Section 9.6), and mark the two request-head rejection paths
closed explicitly as the other rejection paths already do.
2026-10-02 00:22:24 -04:00
yhirose 0715c2739e Enforce a minimum SSE reconnect wait to avoid a busy loop (#2592)
SSEClient::wait_for_reconnect() sleeps in 100ms steps until the
reconnect interval has elapsed. With an interval of 0 (for example
"retry: 0" from the server, or set_reconnect_interval(0)) it never
slept at all, so a server that sends "retry: 0" and closes the stream
made the client reconnect in a tight loop. set_max_reconnect_attempts()
does not stop this either, because each successful connection resets
the attempt counter.

Always wait at least one step (100ms). Intervals of 1-99ms already
waited 100ms because of the step size, so only 0 and negative values
change behavior.
2026-09-28 17:25:37 -04:00
KBS 3330d0eb06 Ignore an SSE retry field that is not all digits (#2591)
parse_sse_line checked only the error code of from_chars, which accepts
a leading '-' and stops at the first non-digit, so retry: -1 made the
client reconnect without waiting and retry: 10s set 10 ms. The SSE spec
ignores a retry value that is not all ASCII digits.
2026-09-28 15:31:20 -04:00
yhirose c1c2b1f4b4 Apply the request-target check to the client and encode control chars
Share the server's request-target check as fields::is_request_target()
and use it in write_request_line too. The client previously used
is_field_value(), which let an embedded SP or HTAB through.

encode_path() only escaped CR/LF among the control characters, so with
path encoding enabled a path like "/a\tb" would now be rejected instead
of sent. Percent-encode every control character (0x00-0x1F, 0x7F).
2026-09-28 03:37:11 -04:00
yhirose e11dbec7b3 Reject control characters in the request-target
RFC 9112 §3.2 does not allow control characters in the request-target,
and §2.2 requires a bare CR to be treated as invalid. parse_request_line
accepted them, so e.g. "GET /a\rb HTTP/1.1" was routed normally. Reject
any byte that is not VCHAR or obs-text with 400 Bad Request. obs-text is
still allowed since some clients send raw UTF-8 in the target.
2026-09-27 23:35:09 -04:00
yhirose 174bce5ccf Escape request data in the docker server's access and error logs
req.path is percent-decoded, so a request like GET /%0D%0A... put a
literal CR/LF into the NGINX-style log lines and let a client forge
extra entries. Log the raw req.target (matching NGINX's $request) and
escape '"', '\', control and non-ASCII bytes as \xHH the way NGINX
does. Also note in the README logging section that req.path may
contain control characters and should be escaped before logging.
2026-09-27 22:50:26 -04:00
DosX 57c4f7f385 Reject trailing characters in HTTP quality values (#2590)
parse_quality accepted values such as q=0.5junk because it checked only the conversion error and ignored the returned end pointer. Require the numeric parser to consume the complete q parameter so malformed Accept values are rejected and invalid Accept-Encoding weights are ignored. Add regression cases for both headers.
2026-09-27 19:20:14 -04:00
yhirose 2fb2dbbe1e Send small static files with the headers and large bodies without a copy (#2589)
A file served from a mount point or through set_file_content() left in two
writes, one for the status line and headers and one for the body, because the
body came from a content provider. A small file is now read into the header
buffer so the whole response leaves in a single write. Only file-backed
providers are coalesced this way: a user-supplied provider may produce its data
over time, and holding the headers back until it finishes would stall the
client.

A large set_content() body was copied into the header buffer before being
sent. A body of CPPHTTPLIB_SEND_BUFSIZ or more is now written directly after
the headers, which saves the copy at the cost of one extra write.
2026-09-26 22:19:15 -04:00
VecSzn 8b6ab24159 Resolve relative Location references on redirect (#2586) 2026-09-26 19:49:21 -04:00
Tobias WallnerandTobias Wallner 5a202d3d5f Added a feature test to auto enable/disable CPPHTTPLIB_USE_NON_BLOCKI… (#2578)
* Added a feature test to auto enable/disable CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO

* turned status: WARNING 'GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.' into a warning

* added ws2_32 for the GetAddrInfoExCancel. this catches previous false negatives

---------

Co-authored-by: Tobias Wallner <tobias.wallner@qtlabs.at>
2026-09-25 15:20:00 -04:00
yhirose 4f3f9ef19b Release v0.58.0 2026-09-21 20:01:43 -04:00
yhirose 6d59d1e2df Build open_stream's request head in memory before sending
open_stream wrote the request line and each header straight to the
socket, so a header rejected by check_and_write_headers left the request
line and the headers before it on the wire. Build them in a BufferStream
first and flush once, as write_request and the WebSocket handshake do.
2026-09-21 19:15:39 -04:00
yhirose 9386b25dd7 Don't wait for a response to a request rejected before sending
process_request reads the response even after write_request fails, so
that an early response (e.g. 413/414) sent while the body is still being
uploaded is not lost. A request line or header rejected while the
request is being built in memory never reaches the socket, though, so
no response will come and the client blocked until the read timeout (or
until the server closed the idle connection).

write_request now reports such a local rejection, and process_request
returns immediately in that case. Socket write failures still read the
response as before.
2026-09-21 19:15:30 -04:00
yhirose 91c55a4385 Release v0.57.1 2026-09-21 12:47:22 -04:00
yhirose ad88645a83 Reject non-token methods in write_request_line
write_request_line checked the request target for CR/LF but concatenated
the method verbatim. A method carrying CR/LF could smuggle a whole
request ahead of the real one, and the client would take the smuggled
request's response as its own. A method with a space or an empty method
put a malformed request line on the wire.

Require the method to be a token (RFC 9110 Section 9.1) before anything
is written. All three callers (the buffered client path, open_stream and
the WebSocket handshake) go through this function and fail with
Error::Write, as they already do for a rejected target.

Claude-Session: https://claude.ai/code/session_01NTDesJQTQPEuu4o4XCu69g
2026-09-21 12:25:12 -04:00
yhirose 82722fcb13 Release v0.57.0 2026-09-20 20:38:01 -04:00
metsw24-maxandyhirose 8b872605e0 reject control characters in chunk extensions in read_payload (#2585)
* reject control characters in chunk extensions in read_payload

* Bound every chunk-size line scan by the line terminator

read_payload() ended its scans of one line buffer two different ways: the
hex-size parse and the space skip that follows stopped on the NUL that
stream_line_reader::append() writes, while the new chunk-ext check walked
to an explicit end pointer. Compute that end pointer first and bound all
of them by it, so no scan depends on the buffer's NUL and the terminator
can never be read as line content.

The bare-LF branch is reachable only under
CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR, where getline() ends the line on
an LF that is the terminator rather than extension text. Say so: the
comment below it explains why a bare LF inside the line is rejected, and
without that note the two read as contradictory. Its guard no longer
depends on the scan cursor either, since all it ever needed was a check
that there is a byte to look at.

* Reuse the chunked-body helper in the chunk-ext acceptance test

AcceptsChunkExtension repeated expect_chunked_body_rejected()'s body
verbatim apart from the expected status, so parameterise the helper on
the status and keep the rejection wrapper for the existing callers. The
decoded body is already checked by the /chunked handler, so asserting
the status is all the new test needs.

Also record why the control-character literal stays split: a hex escape
consumes every hex digit that follows it, so "\x01b" would be the single
byte \x1b rather than \x01 followed by 'b', and joining the halves would
quietly change what the test sends.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-09-20 20:22:52 -04:00
yhirose 52f214bf2e Don't wait for the peer's close_notify on OpenSSL shutdown
tls::shutdown() on OpenSSL called SSL_shutdown() a second time to wait
for the peer's close_notify. An idle keep-alive client never sends one,
so closing its connection held the worker thread until the read timeout,
and Server::stop() waited for it. Send close_notify and return, as the
Mbed TLS and wolfSSL backends already do.
2026-09-19 17:28:09 -04:00
yhirose 09c02f1335 Send 100 Continue only when the request body is read
The server wrote 100 Continue as soon as it saw the expectation, before
pre_routing_handler, pre_request_handler, or routing ran. A request
those handlers rejected, or one that matched no route, still invited
the client to send a body the server would never read.

Defer the interim response until the body is about to be read. If the
request is answered without reading the body, 100 Continue is never
sent and the connection is closed, since whether and when the client
sends the body is unknown.

Also treat a 417 returned by expect_100_continue_handler as the final
response. It used to be written as a bare status line, after which the
request was processed and a second response was written.
2026-09-19 17:28:09 -04:00
yhirose 2e5480ad65 Document that pre_request_handler runs for WebSocket routes 2026-09-19 17:28:09 -04:00
yhirose 4cb363e3f2 Run pre_request_handler for WebSocket routes
The WebSocket upgrade path matched the route and switched protocols
without setting req.matched_route or calling pre_request_handler, so a
check placed there (e.g. authentication) never ran for WebSocket
routes. Set matched_route and run the handler before the upgrade; if it
handles the request, reply with a regular HTTP response instead of 101.

Also write rejected upgrade responses (from pre_routing_handler too)
with write_response_with_content, so they carry Content-Length. Without
it, a client reading the body waited until the keep-alive timeout.
2026-09-19 17:28:09 -04:00
yhirose deb520e26b Update version files with sed -i.bak, which GNU sed accepts too
`sed -i ''` is BSD-only: GNU sed takes the '' as the script and the expression as a file name, so `just release --run` failed on Linux before touching anything.
2026-09-19 13:56:31 -04:00
yhirose f37a5b1407 Fix #2583 2026-09-14 17:31:07 -04:00
yhirose f15992c7ed Update documentation 2026-09-14 12:27:52 -04:00
yhirose 278c2979e8 Release v0.56.0 2026-09-11 17:35:54 -04:00
yhirose 6b8c3f5387 Report only a caller-set WebSocket read timeout as Timeout
199d7ee made read() return the new ReadResult::Timeout for every read
timeout and leave the connection open. The compile-time server default
(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 300s) is always in
effect, so a handler written as `while (ws.read(msg))`, the form the
README's Quick Start uses, no longer ended when a peer went quiet:
Timeout is non-zero, so the loop ran its body again with the previous
message still in `msg`, and the worker the backstop is meant to reclaim
was never released. Nothing caught it because every test of the new
result set a timeout explicitly and checked the result by value, and the
heartbeat tests keep the connection alive with pings.

The two timeouts mean different things. One the caller sets through
set_read_timeout() is a request for control back, and is reported as
Timeout on a still-open connection. The compile-time default is a
backstop against a peer that has gone quiet, and elapsing it is now a
failure again: read() returns Fail and closes the connection, as it did
before 199d7ee. WebSocket tracks whether set_read_timeout() was called,
and WebSocketClient carries the same flag over to the WebSocket it
creates on connect().

Tests use the heartbeat binary, which compiles both defaults down to 3s:
a `while (ws.read(msg))` server handler runs its body once and exits
when the client falls silent, and a client that never set a timeout gets
Fail with the connection closed. The README and cookbook now say which
timeout produces Timeout.

Claude-Session: https://claude.ai/code/session_01EF5uZ1X2kaHhqJ8VgfjVaQ
2026-09-11 17:29:20 -04:00
yhirose e640f8376b Release v0.55.0 2026-09-11 16:54:08 -04:00
yhirose 73a4092f8a Point the remaining httpbingo.org proxy tests at the self-hosted httpbin
ProxyTest, RedirectTest.HTTPBin*, KeepAliveTest and ProxyTest.SSLOpenStream
still sent their requests through the squid proxies to the external
httpbingo.org, so an upstream hiccup there failed CI with no code change
involved (KeepAliveTest.SSLWithDigest got a 502 on its first /get).

Switch them to the "httpbin" container (nginx + go-httpbin) that
BaseAuthTest/DigestAuthTest already use. go-httpbin serves /get,
/redirect/n and /digest-auth the same way, so the test logic is
unchanged; the SSL variants disable certificate verification for the
self-signed test cert, as BaseAuthTest.SSL does.

RedirectTest.YouTube* is left pointing at youtube.com since it exercises
a real cross-host, http -> https redirect chain.

Claude-Session: https://claude.ai/code/session_0148ZAzsuYRYXkwcA7UFeh95
2026-09-11 16:35:11 -04:00
yhirose 3517f92e2f ci: remove the temporary windows-without-SSL flaky failure reporter
The intermittent failures it was tracking stopped after the graceful
drain before close in 8e702d3: no windows-without-SSL test failure on
master since 2026-08-09. Drop the reporting step, the issues: write
permission it needed, and the run_tests step id that only it used.

Claude-Session: https://claude.ai/code/session_0148ZAzsuYRYXkwcA7UFeh95
2026-09-11 16:16:25 -04:00
metsw24-maxandyhirose 0480ff77b8 reject ambiguously framed responses in client read paths (#2581)
* reject ambiguously framed responses in client read paths

* Accept non-chunked Transfer-Encoding responses in the client framing guard

RFC 9112 §6.3 treats requests and responses differently when the final
transfer coding is not chunked: a request's body length cannot be
determined and the server must answer 400, but a response's body simply
runs until the server closes the connection. read_content() and the
open_stream() body reader already do that, so such a response is not
ambiguous and rejecting it broke valid responses such as
"Transfer-Encoding: gzip" followed by a close.

Keep rejecting a Transfer-Encoding paired with a non-zero Content-Length,
which is the actual ambiguity, and drop the non-chunked clause from both
client read paths.

Tests: check that rejection surfaces as Error::Read, that a non-chunked
Transfer-Encoding response is read until close on both paths, and that
HEAD, 204 and 304 responses with both framing headers are not rejected.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

* Share the framing check and reuse existing test helpers

Factor "Transfer-Encoding with a non-zero Content-Length" into
detail::has_conflicting_content_length() next to
is_chunked_transfer_encoding(), and call it from the server request
guard and both client read paths so the rule and its RFC 9112 §6.3
rationale live in one place.

In the tests, drop the POSIX-only raw socket helper in favour of the
existing serve_single_response() and read_all(), which also lets the
tests run on Windows. Fold the stream-only test into the buffered one so
each case checks both Get() and open_stream(), and cover the HEAD/204/304
exclusion on the open_stream() path too.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-09-11 15:55:28 -04:00
KBSandyhirose 8d25b6a3ac Reject a Range first-byte-pos that overflows ssize_t (#2580)
* Reject a Range first-byte-pos that overflows ssize_t

parse_range_header initializes first to the -1 sentinel that means "no
first-byte-pos" and only overwrites it when detail::from_chars succeeds.
On std::errc::result_out_of_range the assignment is skipped and -1
survives, so "bytes=9223372036854775808-100" is parsed as the suffix
range "bytes=-100" and range_error serves the last 100 bytes instead of
returning 416.

Before the parser was rewritten onto detail::from_chars, std::stoll threw
std::out_of_range on the same input, the catch arm added in 8f8761e for
issue #705 returned false, and the request was answered with 416. The
catch arm is still there but from_chars reports through an error code, so
nothing reaches it any more.

get_header_value_u64 and parse_port already reject an out-of-range value
at their from_chars call sites; this was the remaining one that dropped
the error.

The last-byte-pos side is deliberately unchanged: -1 there is the
documented RFC 9110 14.1.2 "remainder of the representation" value, so an
oversized last-byte-pos stays accepted.

* Simplify the Range first-byte-pos overflow check

Parse the first-byte-pos straight into first, since a failed parse now
returns before first is read, and fold the overflow test into the
existing batch of rejected ranges. Also note on the last-byte-pos side
why an overflow there deliberately keeps -1.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-09-11 13:48:04 -04:00
metsw24-maxandyhirose 515b8f84af send each credential only to its own hop in write_request (#2579)
* send each credential only to its own hop in write_request

An SSLClient behind a proxy sent Proxy-Authorization inside the TLS tunnel, where the origin reads it, and sent the origin's Authorization on the CONNECT request the proxy reads. Attach each only on the message its hop actually reads.

* Keep default headers off the CONNECT request

set_default_headers() is typically used for origin credentials such as
Authorization, Cookie or API keys, but they were also attached to the
CONNECT request an SSLClient sends to its proxy, in plaintext before the
TLS tunnel exists. Default headers now go only on requests the origin
reads, the same split the previous commit makes for set_basic_auth and
set_bearer_token_auth.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

* Simplify per-hop credential handling and its tests

Flatten the Authorization insertion in write_request into one guard with
an else-if (Basic already took precedence over Bearer), and shorten the
comments around it. Fold DefaultHeadersStayOffConnect into the
CredentialsStayWithTheirHop helper, which now takes the list of headers
that must reach only the origin.

Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-09-11 13:25:48 -04:00
yhirose 88956ccad8 Self-host the httpbin auth-testing backend for BaseAuthTest/DigestAuthTest
These tests exercise the squid proxies by hitting /basic-auth and
/digest-auth on an external httpbin-style site. That site's identity has
already moved twice (httpbin.org -> httpcan.org, per #2300) chasing
uptime, and httpcan.org itself is now down (Cloudflare 502 from its
origin), failing CI with no code change involved.

Adds two containers to the existing squid docker-compose stack instead:
go-httpbin (mccutchen/go-httpbin) as the backend, and an nginx sidecar in
front of it under the single "httpbin" hostname so both the NoSSL tests
(port 80) and the SSL tests, which CONNECT-tunnel through the proxy to
port 443, resolve the same name -- go-httpbin only listens on one port at
a time, so it can't serve both protocols itself. nginx uses the repo's
existing self-signed test cert; the SSL client tests already disable
verification for it like other self-signed-cert tests in this suite.

go-httpbin was picked over the more feature-complete kennethreitz/httpbin
after finding the latter accepts a wrong digest-auth username as long as
the password matches -- confirmed with a direct curl against the
container, unrelated to anything in this repo. go-httpbin correctly
rejects both. The trade-off is losing SHA-512 digest-auth coverage here,
since go-httpbin only implements MD5 and SHA-256; nothing else in the
suite exercises SHA-512 digest auth against a live server. Response body
assertions are adjusted to go-httpbin's actual JSON shape (an added
"authorized" field, no "algorithm" field), and the domain changes from
httpcan.org to the self-hosted "httpbin".

This only affects 'make proxy'/'make proxy_mbedtls'/'make proxy_wolfssl'
and the Proxy Test CI workflow -- the default 'make' target is untouched.
2026-09-07 21:49:00 -04:00
yhirose f83d06538b Track Homebrew's clang-format version instead of a fixed pin
The previous commit pinned CI and the pre-commit hook to a fixed
clang-format 23.1.0, but the maintainer develops on macOS against
whatever version `brew` currently installs, which changes over time
as Homebrew updates the formula.

style-check now runs on macos-latest and installs clang-format via
`brew install`, so it tracks the same moving target the maintainer's
Mac does. The pre-commit hook switches from pre-commit's own pinned
mirror to a local hook that shells out to the system clang-format,
so a local commit and CI both go through the same Homebrew-installed
binary rather than two independently versioned copies.

Trade-off: this reintroduces the non-determinism a fixed pin avoids
-- a commit's style-check result can now change over time as Homebrew
updates the formula -- but that mirrors how the maintainer already
develops, which is the point.

Also install coreutils in CI: the style_check Makefile target needs
grealpath's --relative-to, which the macOS-native realpath lacks.
2026-09-07 20:15:31 -04:00
yhirose dc41dbd954 Pin style-check and pre-commit to clang-format 23.1.0
CI relied on ubuntu-latest's default apt clang-format (18.1.3), while the
pre-commit hook was pinned to a different 18.x build. Neither tracked a
specific, deliberately-chosen version, and the two could drift from each
other and from whatever a contributor has installed locally.

Both now pin the same clang-format 23.1.0 (latest stable), installed via
pipx in CI. Also scope the pre-commit hook's file matcher to the same set
of files test/Makefile's style_check target checks, since the previous
\.(cpp|cc|h)$ pattern reached into vendored code (test/gtest,
benchmark/crow) that must stay untouched.

Reformat httplib.h's brace-init spacing to match 23.1.0's output.
2026-09-07 17:18:45 -04:00
yhirose 6303a99ce1 Run clang-format on httplib.h and test.cc
Fixes formatting introduced in 199d7ee and d5f8858 that clang-format
18.1.3 (the version used by CI) disagrees with.
2026-09-07 16:29:08 -04:00
yhirose d5f8858731 Move ws::WebSocket's chrono set_read_timeout into the header half
Defined next to its non-template overload, it landed in the part of the header
that test_split compiles into httplib.cc, leaving a user TU's instantiation
with nothing to link against. The WebSocketClient overload of the same name has
always sat up with the class definitions; this one belongs there too.

Only test_split sees it -- a header-only build instantiates the template
wherever it is written, which is why the regular test target stayed green.
2026-09-03 15:00:02 -04:00
yhirose 199d7ee248 Tell a WebSocket read timeout apart from a closed connection
read() collapsed every failure into Fail and marked the connection closed with
it, so a read timeout could not be used to get control back and send on the
same connection -- it killed the connection instead. The information was
already there and thrown away: SocketStream::read records Error::Timeout, and
read_websocket_frame flattened it into a bool.

ReadResult gains Timeout, reported only when the timeout elapsed on a frame
boundary with nothing consumed, which is the only case where the stream can be
read again. Every multi-byte field now loops until it has its bytes, which also
fixes a frame header straddling the read buffer's boundary failing the frame:
Stream::read is allowed to return less than asked for, and only the payload
was reading in a loop.

ws::WebSocket::set_read_timeout() lets a server handler bound its own reads,
and WebSocketClient::set_read_timeout() now reaches an already-open connection
instead of only seeding the next connect().

The read timeout macro splits in two. A client waits forever by default -- a
read timeout is the caller's tool for taking back control, not a liveness
check, which is ping/pong's job -- while a server keeps the 300s that reclaims
a worker from a peer gone quiet. Defining the old name still sets both.

Also record a reason on the two WebSocketSSLStream::read failure paths that
returned -1 without one, so get_error() cannot report a previous call's
timeout, and make SocketStream's read timeout atomic now that it can be
changed while a read is in flight.
2026-09-03 14:32:13 -04:00
Avionic Harshit 9e2e33da56 let EXTRA_CXXFLAGS override -fsanitize=address (#2577) 2026-09-03 10:22:00 -04:00
Jhen-Jie Hongandyhirose 7d53a31d23 Don't compress a response whose handler already set Content-Encoding (#2575)
* Don't compress a response whose handler already set Content-Encoding

* Don't compress a pre-encoded response served from a file

The guard that stands down when a response already names a content coding
covered the responses that settle their coding in `apply_ranges()`, but a
file-backed one settles it in `static_file_encoding()`, which asked the
content-type overload and so never saw the field. With static file
compression enabled, a mount point naming the coding for a tree of
build-time compressed assets, and a handler setting the field on a
`set_file_content()` response, both had their stored bytes compressed a
second time and a second `Content-Encoding` field line appended.

A file-backed response has not been given a content type by the time its
coding is decided, which is the only reason it could not go through
`encoding_type()`. It takes the type as an argument now, so both paths share
the one guard instead of carrying a copy each.

`Response::content_encoding_` becomes `content_coding_`, after what it
holds. It names the coding chosen for the body, which is what its own
comment already called it, while the old name read as the value of the
`Content-Encoding` field whose presence is exactly what forces the coding to
`None`.

README gains the behaviour, including the part that stays with the handler:
`Vary` is added only to a coding the server chose, so a handler that picks a
representation from `Accept-Encoding` has to add the field itself.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-08-31 20:44:12 -04:00
yhirose 9d6a7ee2c1 Release v0.54.1 2026-08-29 21:20:55 -04:00
yhirose 9ce15a14e5 Reject Digest challenges missing realm or nonce (RFC 7616 §3.3)
parse_www_authenticate() accepted any WWW-Authenticate: Digest
challenge that carried at least one auth-param, so a server sending
e.g. Digest qop="auth" with no realm/nonce would make it through.
make_digest_authentication_header() then dereferences auth.at("realm")
and auth.at("nonce") unconditionally, throwing std::out_of_range with
no try/catch on the retry path, which terminates the client process.

Now require both realm and nonce before treating a Digest challenge as
usable, same as if no Digest challenge were present at all.
2026-08-28 23:36:40 -04:00
yhirose 9185fdb6ae Release v0.54.0 2026-08-28 19:16:24 -04:00
yhirose 644cda7032 Update .gitignore 2026-08-28 18:40:55 -04:00
yhirose 2adfc45838 Report a flaky failure only when the test step is what failed
The reporting step for issue #2533 was gated on failure(), which is true
when any step in the job failed. Two build failures on feature branches
were posted to the issue as flaky test recurrences, with the body falling
back to "Could not extract failed test name" because no test had run.

Gate it on the test step's own conclusion instead, and skip posting when
no [  FAILED  ] line turns up in the shard logs. The explicit failure()
stays because an if expression with no status check function gets an
implicit success().
2026-08-28 18:39:53 -04:00
yhirose c7db3da982 Document that the multipart part-count cap only applies to the buffered form path
CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT is enforced only in
Server::read_content(), where parts are accumulated into req.form. The
streaming ContentReader path keeps nothing and was never in scope, but
this was undocumented (GHSA-923p-8q8g-xcqj). Note the split in the README
and show how to bound the part count from inside a ContentReader handler.
2026-08-28 08:26:08 -04:00
yhirose 139f30e0f1 Compress static file responses behind an opt-in (Fix #2545) (#2572)
* Drop the claim that small bodies skip compression

There is no size threshold anywhere in the compression path.
encoding_type() gates on the content type and Accept-Encoding only, and
apply_ranges() compresses whatever body it is given, so a two-byte
text/plain response comes back gzipped at 22 bytes.

Say what actually happens and leave the decision to the handler.

* Compress static file responses behind an opt-in (Fix #2545)

apply_ranges() runs the compressor inside the branch it takes when
res.body is non-empty. A response served from a file leaves res.body
empty and sets content_length_, so it took the other branch, which
writes Content-Length and returns; encoding_type() was computed before
the split and never consulted on that side. The same bytes handed to
set_content() came back gzipped, which left set_mount_point() and
Response::set_file_content() as the one path that missed out.

Add Server::set_static_file_compression(), off by default so nothing
about an existing server changes. When it is on, the file-backed
provider is run through the compressor into res.body ahead of the rest
of apply_ranges(), so the response is framed the way set_content()
already frames one: it keeps its Content-Length, and HEAD still reports
the size a GET would return.

Ranges are answered from the identity representation, since RFC 9110
applies Range after content coding and slicing a compressed body would
mean compressing the whole file first. The ETag carries the coding it
belongs to, so a client that cached the compressed form revalidates
against its own validator rather than the identity one. Both the ETag
and the body take their coding from static_file_encoding(), so the two
cannot disagree.

Providers registered with set_content_provider() are left alone. zlib
buffers until its window fills, so running one through a compressor
would hold back writes that a caller expects to reach the peer as they
are produced.

The compressed bytes stay in memory until the response has been
written, so the peak cost scales with requests in flight.
set_static_file_compression_max_length() bounds it, defaulting to 4MB.

* Add a minimum size for static file compression

Compressing a file that already fits in a single 1500-byte MTU does not
get it to the client any sooner, and a file of a few bytes comes back
larger than it went in once gzip's header and trailer are added. Every
other server draws this line: nginx's gzip_min_length, Caddy's
minimum_length, IIS's minFileSizeForComp, CloudFront's 1000-byte floor.

The note this replaces told callers to decide in the handler. A response
served through set_mount_point() has no handler to decide in, so the
floor has to live in the server. It defaults to 1400 bytes, the size
that fits inside one MTU with room for headers.

set_static_file_compression_min_length() moves it, and
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH sets the default at
compile time. The empty-file case keeps its own early-out so that a zero
floor still cannot turn an empty body into a 20-byte gzip stream.

The two bounds now read as a pair, so the documentation says what each
one is for: the lower bound is about what is worth compressing, the
upper bound about what one request is allowed to cost.

Every file under test/www except 1MB.txt is below the default floor, so
the tests that need a small file compressed lower it explicitly.
2026-08-27 17:19:43 -04:00
yhirose b4ec1bb1de Respect quoted-strings when splitting header parameters (Fix #2568) (#2573)
parse_disposition_params() and extract_media_type() both split on every
';' and then on every '=', with no idea that a parameter value can be a
quoted-string. RFC 9110 5.6.6 allows ';' and '=' inside one, so
filename="report=v2.pdf" came out as v2.pdf", and filename="a;b.txt" was
truncated at the semicolon and left a bogus parameter behind.

The same defect reached the boundary. RFC 2046 5.1.1 allows '=' in a
boundary, which forces a sender to quote it, so the common MIME form
boundary="----=_NextPart_000_0000_01D9" parsed as
_NextPart_000_0000_01D9".

Add split_unquoted(), which is split() with the one extra rule that a
delimiter inside a quoted-string is not a delimiter, and route both
parameter parsers through it. The key/value split, duplicated verbatim
in the two of them, moves into divide_param_pair(). That one divides at
the first '=' without tracking quotes: 5.6.6 makes the key a token, so
no quote can precede the separator, and reusing divide() keeps this off
the per-byte scan.

A backslash stays an ordinary character here. Both browsers and
httplib's own sender percent-encode '"' rather than escaping it, and
recognizing a quoted-pair without also unescaping it would just trade
one wrong value for another.
2026-08-27 17:18:29 -04:00
yhirose c58061ea81 Fail a content provider that makes no progress
write_content_with_progress() advances its offset only by what the provider
writes, so a provider that reported success without writing anything and
without calling done() was handed the same offset and length again on the next
pass. With the peer still connected it spun there, re-entering the provider as
fast as the loop could run.

make_file_body()'s provider was one way to reach this and was fixed in #2566,
but any user-supplied provider can do the same. Treat a pass that makes no
progress as a short body, which is how done() called early is already handled.
2026-08-26 23:05:19 -04:00
yhirose f2b338ae2e Drop the empty Accept entry from the example's invalid list
e96a52e made a leading comma legal, so the example printed
"Unexpectedly succeeded!" for ",application/json". Reported in #2570.
2026-08-26 22:54:09 -04:00
yhirose efe709c45f Format example/accept_header.cc
The file predates the clang-format hook, so any edit to it drags the
whole file into the diff. Reformat it on its own first.
2026-08-26 22:54:09 -04:00
Robert Miller 794a997d8c Fail make_file_body()'s provider when the file is short (#2566)
make_file_body() measures the file once and that length is already the
response's Content-Length. The provider re-opens the file by path on each
call, so if the file has been truncated since, the read comes up empty and
the provider returned true without writing. write_content_with_progress()
advances its offset only by what was written, so it called the provider
again, got nothing again, and kept spinning until the peer gave up.

Return false instead, as every other failure in this provider does.
2026-08-26 22:44:04 -04:00
yhirose e96a52e9dd Ignore empty list elements in the Accept header (Fix #2567)
parse_accept_header() rejected any Accept value with a leading, trailing
or doubled comma, and Server::process_request() validates Accept before
routing, so "Accept: text/html," was answered 400 Bad Request on every
route.

RFC 9110 Section 5.6.1.2 requires a recipient to parse and ignore empty
list elements in a #rule list, so those values are legal. split() already
trims each element and skips the empty ones, which made the guard inside
the callback unreachable as well; drop both and let the empty elements
fall away. The header length limit bounds how many a sender can send, so
ignoring all of them cannot be used as a denial-of-service vector.

get_combined_header_value() keeps skipping empty field lines, but that
skip is no longer observable through a request now that a stray comma
parses cleanly, so it gets its own test.
2026-08-26 22:37:22 -04:00
yhirose 2addb41089 Stop a throwing user callback from terminating the server (#2564)
Server::process_request() wraps only routing() in a try/catch.
Everything else the user supplies runs outside it:

- the content provider, from write_response_core()
- post_routing_handler_, error_handler_, logger_
- expect_100_continue_handler_
- a WebSocket handler, and pre_routing_handler_ on the upgrade path

An exception from any of those unwinds out of process_and_close_socket()
into the task queue, which calls the job without a catch, so it reaches
the top of a pool thread and terminates the process. One handler that
throws takes down every other connection the server is holding.

Add Server::serve_guarded() and run the serving loop through it in both
process_and_close_socket() overloads. The exception is not turned into a
500: by the time a content provider runs, the status line and headers
are already on the wire, so there is nothing left to replace. Report it
through the error logger as Error::UserCallbackException and drop the
connection, which is what the peer observes regardless. Requests on
other connections are unaffected, and the socket is still drained and
closed - which unwinding used to skip on the non-SSL path, since
drain_and_close_socket() sits after the call rather than in a scope
guard.

The error logger is a user callback too, so the report inside the guard
is itself wrapped: a throwing logger must not be able to open the guard
back up.

Adds ServerExceptionTest: a throwing content provider, post-routing
handler, WebSocket handler and error logger, plus the content provider
case against SSLServer, each checking that a later request on a new
connection still succeeds. Every test runs the server on a single worker
thread, so a guard that catches the exception but still loses the thread
shows up as the follow-up request never being served. Note that all of
them abort the test binary without this change - which is the bug, but
it means a regression here fails the run rather than one test.
2026-08-26 01:16:48 -04:00
yhirose ae417b405a Do not let a zero-length write end a chunked body (#2563)
write_content_chunked()'s sink treated "the provider wrote nothing" as
"the provider has finished":

    data_available = l > 0;

so sink.write(p, 0) ended the loop. Only done()/done_with_trailer()
emit the terminating zero-length chunk, so the body was left
unterminated - and the function still returned Success, because the
post-loop check only reports the is_shutting_down() case. The peer waits
for a last chunk that never arrives, and on a keep-alive connection
anything written next is parsed as a chunk-size line.

A provider reaching a pass with nothing to hand over is ordinary:
popping an empty buffer off a queue, or a compressor that has consumed
its input without producing output yet. It is not the end of the
message.

Ignore zero-length writes instead. A zero-length chunk is the terminator
in chunked coding, so it must never be emitted mid-body either way, and
data_available is now controlled only by done()/done_with_trailer().
This matches write_content_without_length(), where the sink's write
never ends the body.

The old behaviour cannot have been relied on: it produced an
unterminated response, so a provider using it never worked in the first
place.
2026-08-26 01:16:38 -04:00
yhirose bc7e51dbb9 Give DataSink's optional callbacks safe defaults (#2562)
DataSink has four callbacks, but only write is assigned by every writer
that hands a sink to a content provider:

  write_content_with_progress()    write, is_writable
  write_content_without_length()   write, is_writable, done
  write_content_chunked()          all four
  send_with_content_provider...()  write
  get_multipart_content_provider() write, done  (cur_sink)

A provider that calls one of the unassigned ones invokes an empty
std::function and throws std::bad_function_call. Nothing on that path
catches it, so it unwinds out of the thread running the provider and
terminates the process. The README's own idiom is enough to hit it:
sink.done() is documented for the without-length overload, but a
provider registered through set_content_provider() with a length gets a
sink where done is empty.

Default the three optional callbacks instead. A sink is writable unless
a writer says otherwise, and a sink that cannot carry trailers still has
to finish, so done_with_trailer() falls back to done(). Capturing this
for that is safe because DataSink is neither copyable nor movable.

A no-op done() alone would only trade the crash for a hang on the two
length-framed paths: both loop until offset reaches the promised length,
so a provider that reports itself done without writing would be called
again immediately, forever. Both now record that the provider finished
and stop, and the short body is reported as a write error. The client
path gains that check for the compressor-failure exit as well, which
used to send a truncated request body without reporting anything.

cur_sink in get_multipart_content_provider() now forwards is_writable
from the outer sink, so a provider item asking whether it may keep going
gets the stream's answer rather than the default.
2026-08-26 01:16:17 -04:00
yhirose f9c205632d Fix accept() error handling on Windows (#2561)
The accept loop in Server::listen_internal() classified accept() failures
by reading errno, but Winsock reports them through WSAGetLastError() and
never touches the CRT errno. Both retry branches were therefore dead code
on Windows, and every accept() failure fell through to the fatal path,
which closes the listening socket and ends listen().

That is reachable in normal operation: a peer resetting a pending
connection before it is accepted is enough, and descriptor or buffer
exhaustion shows up under load. One such event stopped the server from
accepting anything again.

Add is_accept_resource_error() and is_accept_transient_error() next to
is_connection_error(), which already abstracts the same errno vs
WSAGetLastError() difference, and use them in the accept loop.

The POSIX sets are widened to match the Windows ones rather than being
left as they were: ECONNABORTED is the POSIX spelling of the aborted
pending connection that motivates this, and ENFILE, ENOBUFS and ENOMEM
are resource exhaustion in the same sense as EMFILE.
2026-08-26 01:16:01 -04:00
yhirose 84f75185fe Clear svr_sock_ before closing it on the accept loop's fatal path (#2560)
When accept() failed for a reason the retry branches do not cover, the
loop closed svr_sock_ but left the descriptor in the atomic. Two things
go wrong from there:

- A later stop() reads the stale value and calls shutdown()/close() on
  it. By then the OS may have reused the descriptor for an unrelated
  socket (a worker's keep-alive connection, or one the application
  opened), and that connection is torn down instead.
- keep_alive() in the worker threads watches svr_sock_ to notice that
  the server is going away, so the workers keep waiting on a listening
  socket that no longer exists.

Take the descriptor with exchange(INVALID_SOCKET) before closing it,
which is what stop() already does. That also settles the race with a
concurrent stop(): whichever side takes the descriptor closes it exactly
once, and the other sees INVALID_SOCKET and does nothing.
2026-08-26 01:15:47 -04:00
yhirose 19352ae929 Cap the received multipart boundary at RFC 2046's 70 characters (#2565)
parse_multipart_boundary only rejected an empty boundary, so a request could
declare one as long as a header line is allowed to be. A stock server accepts
up to 8146 bytes there, which is what CPPHTTPLIB_HEADER_MAX_LENGTH leaves after
"Content-Type: multipart/form-data; boundary=".

FormDataParser searches the body for "--" + boundary + CRLF with a plain
substring scan. buf_find scans for that delimiter's first byte, always '-', and
at every position that matches calls start_with, which compares until the first
mismatch. A body of '-' makes every position a candidate, and a boundary of '-'
makes each candidate compare the whole delimiter before failing at the CRLF. The
worst case is the product of the body length and the boundary length, and only
the first factor was bounded.

Measured by driving the parser directly in 16 KB reads, Apple clang 17 at
-O2 -DNDEBUG, best of three runs on an otherwise idle machine. 100 MB of '-',
the default payload limit, costs 2.59 s of CPU with a 70 byte boundary and
281.83 s with an 8147 byte one, a factor of 109. The same shape shows at 8 MB:
0.211 s, 3.081 s, 11.359 s and 22.091 s for boundaries of 70, 1024, 4096 and
8147 bytes.

RFC 2046 5.1.1 caps a boundary at 70 characters, so honoring that limit bounds
the multiplier too. The limit applies to the value after unquoting, so a quoted
70 character boundary stays valid. Only the server receive path parses a
boundary out of a Content-Type, so what clients may send is unaffected, and the
boundaries the library generates itself are 45 characters.
2026-08-26 01:15:22 -04:00
yhirose 2afe933103 Send the Connection: close header the multipart test comment describes
expect_split_multipart_ok() carries a comment saying the request sends
"Connection: close" so the response drain ends as soon as the server has
answered, but the header itself never made it into the request, so both
callers kept idling until the read timeout instead.

Add the header. EpilogueSplitAcrossReadsIsIgnored and
InitialBoundarySplitAfterLongPreamble each drop from about 3.1s to about
0.11s.
2026-08-26 01:06:21 -04:00
yhirose bc58e6e9ac Bound the multipart parser's buffer while it waits for a boundary (#2557)
* Bound the multipart parser's buffer while it waits for a boundary

FormDataParser accumulated the entire request body whenever the declared
boundary never appeared in it. State 0 returned without erasing anything, so
the buffer grew to the full payload (100 MB by default) and buf_find rescanned
all of it on every 16 KB read. The cost grew with the square of the body size:
50 MB of '-' took 198 s of CPU on one core, and the buffer pinned the body in
memory for the whole request. One unauthenticated request was enough, and the
parser runs for any multipart request even when the handler never looks at the
parsed result.

State 0 now keeps only the last dash_boundary_crlf_.size() - 1 bytes while it
waits, which bounds both the memory and the rescan without capping how long a
preamble may be. The same 50 MB body now takes 0.14 s and the buffer stays at
one read plus the boundary. A boundary split across reads still parses, which
is what de5a255 (#2159) gave up this erase for.

State 4 buffered without bound in the same way when a boundary was followed by
neither CRLF nor "--". No further data can make such a body valid, so it now
fails right away. That is only safe because the close-delimiter branch moves to
a new state 5 that discards the epilogue: it used to stay in state 4, so an
epilogue arriving in a later read fell into this same branch. An epilogue
beginning with CRLF was then parsed as a new part and the request was rejected
with 400, which state 5 fixes as well.

Affected since v0.23.0, where de5a255 replaced the erase that had kept the
buffer in check.

* Skip buffering the multipart epilogue

Once the close delimiter has been parsed the parser is in state 5 and discards
whatever follows, but it still copied each epilogue read into the buffer before
erasing it. Return before buffering so a large epilogue spread across several
reads is dropped without being copied in at all.

* Clean up the multipart parser tests and the state 4 branch

Review follow-ups on top of the previous two commits, no behavior change.

- Move the four new tests next to the rest of MultipartFormDataTest. They
  had landed in the middle of the RedirectTest block.
- Use bind_to_any_port instead of the fixed PORT, as AGENTS.md requires for
  newly added servers. NoInitialBoundaryParsingIsNotQuadratic holds its port
  for a couple of seconds, which matters when the suite is run sharded.
- Send "Connection: close" from expect_split_multipart_ok. The server kept
  the connection alive after answering, so the response drain idled until the
  client read timeout; both tests drop from about 3s to about 0.11s.
- Drop the dead `dash_.size() > buf_size()` guard in state 4 and flatten the
  nested else. The check above it already guarantees two buffered bytes, and
  both CRLF and "--" are two bytes, so it can never fire. Removing it is what
  makes the new comment's claim readable straight off the code.

* Rename the timing test's locals to avoid a Windows macro

MSVC's <rpcndr.h>, pulled in by <windows.h>, defines `small` as `char`, so
`auto small = ...` failed to compile on the Windows jobs. Same class of
problem as the std::min / std::max collision.
2026-08-25 22:55:52 -04:00
yhirose 254e576b50 Add Server::CustomRoute() for HTTP methods outside the built-in set (#2553)
* Add Server::CustomRoute() for HTTP methods outside the built-in set

parse_request_line validates the request method against a fixed whitelist and
rejects anything else with 400 before routing runs. That blocks WebDAV, where
PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK and UNLOCK are ordinary methods
defined by RFC 4918, and it blocks extension methods such as UPnP's SUBSCRIBE.
The need has been open since #847.

Registering a handler is now what makes the server accept a method:

    svr.CustomRoute("PROPFIND", "/dav/:id", handler);

Because custom methods go through the normal dispatch path, patterns work the
way they do for Get() and friends, and the request body is available in
req.body. Serving these methods through set_pre_routing_handler was never
enough: the body has not been read at that point, so PROPPATCH and LOCK, which
require one, could not be implemented at all.

A HandlerWithContentReader overload is available too. The content reader gate
in routing() also fires when a custom method carries no body, matching what
expect_content() does unconditionally for POST/PUT/PATCH/DELETE, so a body-less
PROPFIND (RFC 4918 treats one as allprop) reaches its handler instead of
falling through to 404.

Method names are validated as RFC 9110 tokens, and the ten built-in methods are
refused. Seven of them are dispatched by the if/else chain in routing() before
the custom tables are consulted, so a route registered for one could never
fire; CONNECT, TRACE and PRI carry protocol-level meaning this library does not
route. A refused registration makes is_valid() return false, so listen() fails
rather than starting a server holding a handler that would never run. This is
also why SSLServer::is_valid() now chains to Server::is_valid() instead of only
checking ctx_.

Servers that never call CustomRoute() keep the previous per-request cost: the
built-in method set is checked first and short-circuits, and the custom lookup
returns early on an empty map.

* Add cookbook recipe for custom HTTP methods

The CustomRoute() docs were a section inside S01, which pushed that page to 90
lines, the longest in the cookbook, and mixed a separate feature into a page
about registering GET/POST/PUT/DELETE handlers. Move the section into its own
recipe and give it room for the part that was missing: the OPTIONS handler
returning DAV: and Allow, which WebDAV clients probe for before anything else.
S01 goes back to 68 lines and keeps a pointer to the new page.

The recipe is titled after the API rather than after WebDAV, and says outright
that generating the 207 Multi-Status XML, interpreting Depth and managing locks
are the reader's job. Routing the method is all the library does.

S23 takes order 42, so the TLS, SSE and WebSocket recipes shift to 43-57. That
only moves the sort key. Filenames, the T01/E01/W01 labels, the published URLs
and every cross-reference are untouched.
2026-08-25 19:31:42 -04:00
yhirose af75a4160f CI: quote the OpenSSL installer's /DIR argument
Start-Process joins ArgumentList entries with spaces, so /DIR=C:\Program
Files\OpenSSL reached Inno Setup as /DIR=C:\Program and the install landed
there. Linking still succeeded, because the import libraries were present
under that path, and the failure surfaced only when gtest_discover_tests ran
the test binary: exit code 0xc0000135, DLL not found, since PATH pointed at
C:\Program Files\OpenSSL\bin.

Quote the value, and assert that the import libraries and runtime DLLs are
where we expect before exporting PATH, so a misplaced install fails loudly at
the install step instead of quietly at load time.
2026-08-25 19:09:56 -04:00
yhirose f3e5a93a5b CI: install Windows OpenSSL from slproweb's manifest instead of Chocolatey
The Chocolatey openssl package hardcodes a versioned slproweb URL in its
install script, and slproweb keeps only the newest build of each OpenSSL
branch. Every OpenSSL release therefore deletes the file the current package
points at, and "windows with SSL" fails at the install step with a 404 until
someone respins the package. That is what broke the job today: the package is
still at 4.0.1 while slproweb has moved to 4.0.2.

slproweb publishes a JSON manifest of its current downloads, linked from the
download page and updated at the same time as the files themselves. Read that
and take the newest 64-bit 4.x installer from it, so the URL is always live.
The SHA512 in the manifest is verified before the installer runs.

The silent flags are the ones the Chocolatey package used. /DIR pins the
install location that the CMake step already finds, instead of relying on a
registry lookup. PATH and OPENSSL_CONF are exported the same way the package
set them.

Staying on 4.x is deliberate: it keeps this job on the OpenSSL 4.0 series
rather than dropping to the 3.6 that vcpkg would provide.
2026-08-25 19:04:26 -04:00
yhirose 00d1f54267 Fix WebSocket::close() racing a concurrent read() on the same stream
close() drained the peer's Close reply with its own frame read. If an
application reader thread was inside read() at that moment, two threads
parsed frames off one stream: read_websocket_frame()'s payload loop keeps
reading until it has the declared length, so bytes stolen by the drain
were silently replaced with bytes from further along the stream. The
in-flight message kept its correct length but got the wrong content.

Add a read_mutex_ that marks which thread owns the stream's read side.
read() holds it for the whole call. close() sends the Close frame, then
drains the peer's reply (RFC 6455 7.1.1) only if it can try_lock the
mutex; otherwise it returns immediately, leaving the stream entirely to
the thread already reading it. This also fixes close() blocking for the
full close timeout when a reader thread was parked waiting on a peer
that never replies.

Add WebSocketTest.CloseDoesNotStealBytesFromConcurrentRead, which drives
a raw TCP peer that stalls mid-payload to force the race; it fails
reliably against the old code and passes against the fix.

Update README-websocket.md: close() during a concurrent read() is now
supported.
2026-08-24 17:30:04 -04:00
yhirose f82d2d90b6 Update README-websocket.md 2026-08-24 07:09:13 -04:00
yhirose 228af9033b Fix TLS session data race on wss:// WebSocket connections (#2551)
A wss:// WebSocket enters a single TLS session from several threads: the
read path, the application's send()/close(), and the heartbeat ping thread.
The existing write_mutex_ only serializes writers, so a reader's SSL_read and
a writer's SSL_write (plus the SSL_peek in is_peer_closed() on the write path)
run concurrently on the same session. OpenSSL and the other backends forbid
concurrent access to one session, so this corrupts the record layer: messages
are silently dropped, and under ASan it shows up as a heap-buffer-overflow.
It affects wss:// only; plain ws:// is unaffected because the kernel allows
concurrent recv()/send() on a socket.

Route wss:// through a new WebSocketSSLStream that serializes every TLS call
with one per-stream mutex. The socket is kept non-blocking for the stream's
lifetime and each read()/write() performs a single non-blocking TLS call under
the lock, then waits for readiness with select() outside the lock. The lock is
therefore held only for CPU-bound work, so a reader blocked waiting for data
never stalls a concurrent sender.

Because the socket is non-blocking, a TLS call can stop needing either
direction, so read() also waits for writability on WantWrite and write() waits
for readability on WantRead. A read that shares its session with the send path
has to flush pending output before it can decrypt more input, and Mbed TLS
surfaces this on every mbedtls_ssl_read(). The read timeouts are atomic since
WebSocket::close() shortens them from the closing thread while the receive
thread is inside wait_readable().

SSLSocketStream is left untouched, so ordinary HTTP/HTTPS keeps its exact code
path and performance. The heartbeat ping thread also stays, so timer-driven
pings keep working as before.

Add test_websocket_thread_safety.cc, which drives send/close/heartbeat against
a concurrent reader over wss://. Built with ASan in CI, a regression surfaces
as a heap-buffer-overflow.
2026-08-24 07:04:43 -04:00
yhirose 6494edd8c0 Add static-file, large-body and TLS workloads to the A/B benchmark
The harness had a single endpoint returning a 12-byte set_content() body.
That is the one case where the response line, the headers and the body
already share a single write(), so any change to the write path measured
as noise. Comparing a gather-write branch against its merge base reported
0.993x at p = 1.000 while the same branch moved static-file throughput by
a quarter and TLS throughput by nearly half in both directions.

The server now also serves a large set_content() body and small and large
files from a mount point, over HTTPS when a certificate is given, with
--path, --large-mib and --tls selecting the combination.

ab.sh now compiles the harness from the invoking worktree instead of each
ref's own copy, so both refs run an identical workload and a ref that
predates a harness change stays measurable. Only httplib.h varies, through
-I. --timeout is exposed because bombardier's 2s default aborts large TLS
responses, which then fails the non-2xx check.
2026-08-19 21:13:30 -04:00
yhirose 70b49d50bd Simplify get_bearer_token_auth and table-drive its test
Drop the now-redundant has_header guard (get_header_value already
returns "" for a missing header, which the length check rejects),
name the "Bearer " prefix once, and cite RFC 9110 to match the
file's convention. Convert the regression test to the table-driven
form used elsewhere in test.cc and move it out of the middle of
GetHeaderValueTest so that suite stays contiguous.
2026-08-19 19:16:43 -04:00
metsw24-max 2a068def54 validate bearer scheme in get_bearer_token_auth (#2544) 2026-08-19 19:04:46 -04:00
yhirose abf525d78c Parse WWW-Authenticate/Proxy-Authenticate as an RFC 9110 challenge list
detail::parse_www_authenticate() assumed a single challenge starting at
the first space in the field value and read only its first occurrence,
so a Basic challenge listed before Digest (or split across two field
lines, as some servers do) hid the Digest challenge entirely, and a
second Digest challenge with different parameters (RFC 7616 offering
both SHA-256 and MD5) could mix params from both. Combine repeated
field lines the same way the other list-valued headers do, then split
on commas that aren't inside a quoted-string so a quoted realm can
contain a comma, and track which challenge each auth-param belongs to
by the auth-scheme token that starts it. Also require at least one
auth-param before reporting a Digest challenge as found, since an
empty challenge can't produce a usable Authorization header.
2026-08-19 06:54:21 -04:00
yhirose 0151b3e23e Match the Upgrade websocket token rather than the whole field value
RFC 9110 7.8 defines Upgrade as a comma-separated list of protocols and asks
recipients to match each protocol-name case-insensitively; RFC 6455 4.2.1 asks
for a header field containing the value "websocket". Both handshake checks
instead read occurrence zero and required the whole field value to be exactly
"websocket", so a client offering "websocket, HTTP/3.0" -- or naming websocket
on a second Upgrade field line -- was answered 404 rather than 101.

This is the defect ffe2a1c fixed for Connection two lines below, and
has_header_token() is already called in both of these functions.

The client-side check loosens what we accept back from a server, which is the
same reading: a server answering 101 may name websocket alongside another
protocol, and rejecting that handshake was ours to get wrong.
2026-08-18 22:29:26 -04:00
yhirose 8b19c288e8 Tidy up the new list-field tests
The four ExpectTokenTest cases landed inside the #ifndef _WIN32 that guards the
10 GiB content-provider test below them, so Windows never compiled them and the
green Windows jobs said nothing about the fix. Nothing in them is POSIX-only --
they use the same helpers as ConnectionTokenTest, which sits outside any guard
-- so move them above the guard.

probe_expect() re-implemented send_request(), down to the create_client_socket
argument list. Call send_request() instead, with Connection: close so its read
loop ends at the response rather than idling to the read timeout; the Connection
check runs before the Expect block, so it does not disturb what is under test.

Move the new Content-Encoding case below its siblings. Appending it to the tail
of the comment block left the "whole token" paragraph reading as documentation
for a test about repeated field lines. The paragraph above it had been detached
from KnownEncodingWithoutSupportIsReported the same way one commit earlier; put
that one back too.
2026-08-18 22:21:51 -04:00
yhirose f442226581 Match the Expect 100-continue expectation as a token
RFC 9110 Section 10.1.1 defines Expect as a comma-separated list, states that
its value is case-insensitive, and requires a server that receives a
100-continue expectation in an HTTP/1.0 request to ignore it. Comparing the
whole field value against "100-continue" met none of those.

An HTTP/1.0 request asking for 100-continue was answered with a 100 (Continue)
interim response, which that section forbids. "100-Continue" and
"100-continue, foo" were both read as no expectation at all, so a client that
waits for the interim response before sending its content waited for a response
that was never coming.

Route the check through has_header_token(), which walks every field line and
compares complete tokens case-insensitively, and skip it for HTTP/1.0. An
expectation cpp-httplib does not recognize is still ignored rather than
refused; the 417 the section offers for one is a MAY, not a requirement.
2026-08-18 22:01:40 -04:00
yhirose 3e3e4863b0 Read Content-Encoding as the combined field value
RFC 9110 Section 5.3 makes a Content-Encoding spread over several field lines
the same message as the comma-joined one, so the two have to be read the same
way. Reading occurrence zero did not: a response carrying "gzip" on two field
lines was decoded as a single gzip coding, so a body the sender says was
encoded twice came back after one pass -- still compressed, but presented to
the caller as decoded. The same value written as "gzip, gzip" on one line took
the pass-through path instead.

Read the combined value at both sites. A value naming several codings matches
none of the ones cpp-httplib implements, so both representations now take the
pass-through path that prepare_content_receiver() already documents for an
unrecognized coding.

This does mean a sender that repeats "Content-Encoding: gzip" on two lines for
a body it gzipped once no longer has that body decoded. There is no way to tell
that sender apart from one that really did encode twice, and the conservative
reading is the one the field value states.
2026-08-18 22:01:14 -04:00
yhirose e8887d98e9 Match Brotli and Zstandard content codings as whole tokens
is_brotli_encoding() and is_zstd_encoding() searched the Content-Encoding
value for "br" and "zstd" as substrings, while is_zlib_encoding() beside them
compared the whole value. So "fibre" and "librarian" were read as Brotli and
"x-zstd-ish" as Zstandard, and "gzip, br" -- a value naming two codings, which
cpp-httplib does not support -- was labeled Brotli and run through a Brotli
decompressor over gzip data.

RFC 9110 8.4.1 defines a content coding as a token, so compare the whole value
case-insensitively as the zlib check already does. A value naming several
codings no longer matches any of them and takes the pass-through path
prepare_content_receiver() already documents for an unrecognized coding.

contains_case_ignore() has no callers left.
2026-08-18 21:05:22 -04:00
yhirose 881842cd72 Match Connection options as tokens rather than whole field values
RFC 9110 Section 7.6.1 defines Connection as a comma-separated list of
case-insensitive connection options, and Section 5.3 lets that list be split
across several field lines. Comparing the whole field value against a single
option gets both wrong.

A client sending "Connection: keep-alive, close" was answered without a
Connection header and its socket was kept open, so the close it asked for was
never performed and never announced. An HTTP/1.0 client asking for keep-alive
only got it by spelling the option exactly "Keep-Alive"; the lowercase form
everyone actually sends closed the connection instead.

Route the five Connection checks through has_header_token(), which already
walks every field line and compares complete tokens. Expect is left alone:
matching "100-continue" as a token would make an unrecognized expectation
alongside it look acceptable, where Section 10.1.1 asks for 417.
2026-08-18 20:58:34 -04:00
yhirose 2731b728f5 Move has_header_token() next to the other header field helpers
It was defined as a static inline above the border line so that the split
build would not turn it into an exported symbol of the shared library, which
kept abidiff from reporting an added function. That put an internal helper's
location at the mercy of a CI check rather than of where it belongs: it reads
a header field the same way get_header_value() and get_combined_header_value()
do, and it is the closest sibling of the latter, both being about a list-valued
field spread over several field lines.

Define it as a plain inline beside them and forward-declare it with the
split() family it calls. Adding a symbol is a source and binary compatible
change, so let abidiff report it.
2026-08-18 20:12:45 -04:00
yhirose 161f787fee Combine repeated field lines before parsing list-valued headers
RFC 9110 Section 5.2 and 5.3 define the combined value of repeated field
lines as their values joined by commas in the order they were received.
Several call sites read only the first occurrence and then split that on
commas, so whatever the later field lines carried was silently dropped: an
acceptable media type or content coding, an ETag, a WebSocket subprotocol, a
declared trailer name, or an address a proxy appended as its own line rather
than by extending the one it received.

Add detail::get_combined_header_value() and use it for Accept,
Accept-Encoding, If-None-Match, Sec-WebSocket-Protocol, Trailer and
X-Forwarded-For. Empty field lines are skipped so the combined value never
starts with a bare comma, which parse_accept_header() rejects outright.

Also drop the now-dead manual trimming in parse_trailers() and replace the
istringstream-based subprotocol tokenizer with detail::split(); split()
already trims each token and skips empty ones.
2026-08-18 19:46:31 -04:00
BioticR 7ae9ffad3e Update README.md (#2543)
AF_UNIX support on windows have already been added in Pr #2115 .
2026-08-18 06:49:10 -04:00
yhirose ffe2a1c1e9 Match the Connection "Upgrade" token exactly in WebSocket handshakes (#2542)
* Match the Connection "Upgrade" token exactly in WebSocket handshakes

The server and the client both looked for "upgrade" as a substring of the
Connection field value, so "notupgrade", "upgrade-not" and "xupgrade" all
passed as the standalone token the handshake requires. RFC 6455 4.2.1 asks
for an ASCII case-insensitive token match, and a value split across several
Connection lines was missed entirely because only the first line was read.

Parse the field as the comma-separated token list it is, across every line,
and reuse the same helper for the server request check and the client
response check.

Reported by gb1dev.

* Tidy up the Connection token helper

Move has_header_token() out of the WebSocket-only detail block and next to
the other header field helpers, forward-declaring it beside split(). Use the
existing split_find(), which drops the manual found flag and stops at the
first matching token.

Drive the client-side test from an ordinary Server route answering 101 with
a bad Connection value, rather than the hand-rolled listening socket copied
from the test above it.

* Keep the Connection token helper out of the split build's ABI

The split build strips inline from everything below the border line, so a
helper defined there becomes an exported symbol of the shared library and
abidiff reports it as an added function. The tests also could not see
is_websocket_upgrade() or websocket_accept_key(), since neither is declared
in the part of the header that survives the split.

Define has_header_token() as a static inline above the border, next to the
split() declarations its two call sites already sit below, and declare the
two WebSocket helpers the way ws::impl::read_websocket_frame() already is.
The shared library's exported symbols are now identical to master's.
2026-08-18 06:48:12 -04:00
Denis Gregor 2004668509 Make decode_uri the inverse of encode_uri (#2540)
decode_uri was a byte-for-byte copy of decode_uri_component: it decoded every
%XX, including escapes of the reserved characters that encode_uri leaves
literal. So decode_uri was not the inverse of encode_uri and promoted an
escaped delimiter into a real one -- decode_uri("http://h/a%2Fb") returned
"http://h/a/b". Keep escapes of the reserved set encode_uri preserves, matching
JS decodeURI; non-reserved escapes still decode.
2026-08-17 20:27:57 -04:00
Jean-Francois Simoneau d3ff68d28d Always use meson option non_blocking_getaddrinfo (#2537)
* Always use meson option non_blocking_getaddrinfo

* Replace not .disabled() with .allowed() for clarity
2026-08-15 08:48:39 -04:00
yhirose b8fe69e4e8 Release v0.53.1 2026-08-14 21:51:42 -04:00
yhirose acd0640870 Code improvement 2026-08-14 21:29:14 -04:00
metsw24-max 2d8e49dd9b reject trailing bytes after IPv6 host literal in parse_url (#2536) 2026-08-14 21:27:40 -04:00
yhirose 1e9d6f0b0b Match literal route patterns without building a std::regex (#2538)
Server::make_matcher() built a std::regex for every pattern that did not
contain "/:", even though most route patterns are plain literals with no
regular expression syntax in them. Matching those went through
std::regex_match on every request, for every registered route the
dispatcher scanned before reaching the one that matches.

PathParamsMatcher already performs an exact literal comparison when it
captures no parameter, so no new matcher class is needed: a pattern with
no regex metacharacter can simply use it. Add an early return for the
zero parameter case in PathParamsMatcher::match(), and select the matcher
by also looking for the 14 ECMAScript metacharacters instead of only for
"/:". Path params keep taking precedence, so a pattern that mixes both,
such as "/users/:id/(.*)", is unaffected.

Measured with clang -O2 on macOS, scanning routes that all miss until the
last one: at 100 routes a scan drops from 10.3us to 0.46us, and end to end
throughput rises by about 24%. At 1000 routes throughput is roughly 3
times higher. Registering 5000 routes drops from about 3.0ms to about
0.9ms, since no std::regex is built for literal patterns.

This also keeps CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH confined to the
routes it is meant for. That limit rejects overlong paths before calling
std::regex_match, but until now every literal route was a RegexMatcher
too, so a literal route longer than the limit stopped matching even though
no regular expression was involved. Literal routes no longer go through
RegexMatcher, so only real regex routes are capped.

Patterns containing a metacharacter keep their current behavior, so
"/index.html" still matches "/indexXhtml" the way it always has. One
visible change: a literal route no longer populates Request::matches,
which is now a default constructed std::smatch. Path parameter routes
have always behaved that way, and Request::matches only carries useful
information for regex routes.
2026-08-14 12:58:01 -04:00
yhirose 88240172ea Guard regex routes against stack overflow from long paths
RegexMatcher::match() called std::regex_match() directly on the
attacker-controlled request path. For quantified patterns such as "(.*)",
std::regex_match's recursive backtracking implementation (most acute on
libstdc++) recurses roughly once per matched character, so a long enough
path can exhaust the calling thread's stack and crash the process. Verified
against real GNU libstdc++: under the default thread stack size, a path of
a couple thousand characters against a simple quantified route pattern
reliably crashed the process, well within the existing 8192-byte request
URI limit.

Add CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH (default 256) and reject paths
longer than it before ever calling std::regex_match, treating them as a
non-match instead. Confirmed the fix eliminates the crash under the same
libstdc++ build and default stack size that reproduced it.
2026-08-13 23:57:28 -04:00
yhirose 89e0c5c238 Enforce payload_max_length on decompressed size for unframed requests
For a non-SSL request with neither Content-Length nor Transfer-Encoding,
Server::read_content_core() fell back to reading raw wire bytes with
detail::read_content_without_length() directly, bypassing the decompressor
wrapper that the length-framed and chunked paths already use. As a result,
payload_max_length only bounded the compressed bytes read off the socket,
not the decompressed size a handler could produce from them.

Route this fallback through detail::read_content(..., decompress=true)
instead, the same helper already used below for the length-framed and
chunked cases, so the decompressed-size guard applies uniformly.
2026-08-13 23:35:30 -04:00
yhirose f00e476f1b Release v0.53.0 2026-08-09 19:54:29 -04:00
yhirose 8e702d3837 Gracefully drain socket before close in Server::process_and_close_socket (#2534)
* Gracefully drain socket before close in Server::process_and_close_socket

Closing a connection while the receive queue still has unread data,
or while bytes are still in flight, can make the OS send an abortive
RST instead of a graceful FIN. On Windows this surfaces as
WSAECONNABORTED/WSAECONNRESET on the peer's read, which can make an
otherwise fully-written response look like a failed request -- a
likely contributor to the ServerTest.HTTP2Magic flakiness tracked in
#2533.

Add detail::close_socket_gracefully(), which half-closes the write
side, drains any queued/in-flight bytes (bounded to 100ms / 1MB),
then performs the final shutdown+close. Use it in
Server::process_and_close_socket.

Root cause and fix mechanism identified by @Hyukya in #2533.

* Rename close_socket_gracefully to drain_and_close_socket

'gracefully' already means something specific in this codebase: whether
to send a TLS close_notify before closing (shutdown_ssl's
shutdown_gracefully param, ClientImpl::disconnect(gracefully),
tls::shutdown(session, graceful)). Reusing the word for an unrelated
TCP-level drain-before-close made the new function read as part of that
TLS machinery when it isn't. Rename it to describe what it does instead,
matching the existing close_socket/shutdown_socket and
WebSocketClient::shutdown_and_close naming.
2026-08-09 19:43:19 -04:00
yhirose 19333f80d4 Fix Mbed TLS/wolfSSL hostname verification bugs in set_sni()
The stricter ws::Result error checks added in 6018c7f and 86d0210
exposed two backend-parity bugs in setup_client_tls_session(), shared
by SSLClient and WebSocketClient since their TLS setup was merged:

- enable_server_hostname_verification(false) had no effect on Mbed TLS
  or wolfSSL for DNS hosts: mbedtls_ssl_set_hostname() and
  wolfSSL_check_domain_name() bind SNI and handshake-time identity
  checking together, so the identity check ran regardless of the
  option, failing the handshake before the post-handshake
  server_hostname_verification check was ever reached.

- On a genuine wrong-hostname failure, Mbed TLS reported the generic
  Error::SSLServerVerification instead of
  Error::SSLServerHostnameVerification, because
  MBEDTLS_ERR_X509_CERT_VERIFY_FAILED was mapped without looking at
  which verify flag actually caused it.

Fixes:
- set_sni() now takes a verify_hostname flag. wolfSSL skips
  wolfSSL_check_domain_name() when it's false. Mbed TLS can't request
  SNI without also arming the CN/SAN check, so it installs a verify
  callback that masks the mismatch flag instead - a self-contained one
  when the session has no user verify callback of its own, so it never
  reads the process-wide set_verify_callback() slot another client may
  have populated (this was caught by ASAN as a stack-use-after-scope:
  VerifyCallbackTest.VerifyContextFields leaves a dangling lambda
  there because MbedTlsSession never had a reason to consult it
  before).
- map_mbedtls_error() now takes the handshake's verify flags and
  reports HostnameMismatch when CN/SAN mismatch is the only one set,
  matching the wolfSSL mapping and the post-handshake identity check.
- The duplicated verify-flags/error-mapping/backend_code logic in
  connect() and connect_nonblocking() is factored into
  fill_mbedtls_tls_error(); the duplicated flag-clearing in the two
  verify callbacks is factored into mbedtls_clear_cn_mismatch(); both
  use the existing hostname_mismatch_code() accessor instead of the
  raw Mbed TLS macro.

Also tightens SSLClientTest.ServerHostnameVerificationError_Online to
assert the specific error code now that all three backends agree,
rather than accepting Mbed TLS's old fallback value.

Verified full non-online suite green on OpenSSL (791), Mbed TLS (737),
and wolfSSL (735), plus the split build, plus the Online
hostname-mismatch test against badssl.com on all three backends.
2026-08-07 21:31:21 -04:00
yhirose 86d0210391 Add WebSocketClient::enable_server_hostname_verification
WebSocketClient's TLS setup already threaded
ClientTlsSessionOptions::server_hostname_verification through
setup_client_tls_session(), the same path SSLClient uses, but never
exposed a way to set it: create_stream() called setup_client_tls_session()
without an options argument, so the default (verification on) was the
only reachable value.

Add the public setter, mirroring ClientImpl/SSLClient/Client, and wire
it into create_stream()'s ClientTlsSessionOptions. Last open item from
issue #2531's WebSocketClient/SSLClient API alignment.
2026-08-07 18:48:59 -04:00
yhirose 6018c7feb3 Return ws::Result from WebSocketClient::connect() instead of bool
Issue #2531 asked for connect() to expose error detail the way
ClientImpl/SSLClient do via Result, instead of collapsing every failure
into a bare bool. The groundwork (detail::ClientTlsSessionError) was
already laid during the WebSocketClient/SSLClient dedup but left
unwired.

- Add httplib::ws::Result: explicit operator bool(), error(), and
  flattened upgrade-response accessors (status(), headers(),
  get_header_value(), has_header()); ssl_error()/ssl_backend_error() on
  SSL builds.
- Add Error::WebSocketHandshake for upgrade-validation failures
  (non-101 status, bad Sec-WebSocket-Accept, bad Upgrade/Connection
  headers).
- Extract detail::parse_status_line from ClientImpl::read_response_line
  and reuse it in read_websocket_upgrade_response, replacing the
  previous "HTTP/1.1 101" substring match with a proper parse. Non-101
  responses now surface their status and headers instead of being read
  and discarded.
- Wire WebSocketClient::create_stream() to capture ClientTlsSessionError
  so TLS failures (SSLServerVerification, SSLServerHostnameVerification,
  ...) reach the caller with backend error codes.
- Update tests and README-websocket.md accordingly.

This is a source-breaking change for callers that assign the result to
bool (e.g. bool ok = cli.connect();); if (cli.connect()) and gtest's
ASSERT_TRUE/EXPECT_FALSE(...) macros are unaffected since operator bool
still participates in contextual conversion.
2026-08-07 18:02:29 -04:00
yhirose 8d5085df1b Update README 2026-08-07 17:26:13 -04:00
yhirose 8f0ff32056 Add WebSocket TLS and timeout recipes to the Cookbook
T04 (mTLS) had grown a "WebSocketClient" subsection describing
wss:// client certificates, and c12/t02 were getting similar
WebSocketClient asides for timeouts and CA paths. The Cookbook's
own index already separates WebSocket into its own category
(W01-W04) from TLS/Security (T01-T05) and Client (C01-C19), so
burying WebSocketClient specifics inside those pages fought the
site's structure.

Move that content into two new recipes under the WebSocket
category instead:

- W05: wss:// TLS setup (set_ca_cert_path CA directory parity,
  PemMemory client certificate)
- W06: WebSocketClient's three timeouts, including the recently
  added chrono overloads

T04, T02, C12, and W01 now carry a single reference link to the
new pages instead of duplicated explanations, matching the site's
existing cross-link convention.

While rewriting T04's client-side section, noticed it documented
SSLClient's file-path constructor but not its PemMemory one, even
though the server-side section covered both forms for SSLServer.
Added the missing PemMemory example so both sides are symmetric.
2026-08-07 17:17:07 -04:00
yhirose 2dd44d0f52 Document WebSocketClient/SSLClient TLS parity gaps in the READMEs
WebSocketClient::set_connection_timeout (both the time_t and
chrono overloads) was missing from README-websocket.md's API
reference and the timeout example, even though set_read_timeout
and set_write_timeout were both listed.

README.md never documented the PemMemory in-memory constructor
that SSLServer and SSLClient both have, so mTLS setup only showed
the file-path form. Add a "Mutual TLS (mTLS)" section covering
both forms for server and client, and note that
ws::WebSocketClient's wss:// constructor takes the same PemMemory
struct.

Also note, next to Client::set_interface, that WebSocketClient has
the same method, matching the existing cross-reference for
set_hostname_addr_map right below it.
2026-08-07 17:16:54 -04:00
yhirose 86abc9a0ea Give WebSocketClient the PemMemory client certificate constructor SSLClient has
Adds ws::WebSocketClient::PemMemory and a constructor overload that
installs an in-memory client certificate on the TLS context, enabling
mutual TLS for wss:// connections. The certificate is silently ignored
for ws:// URLs, consistent with the existing TLS-only setters such as
set_ca_cert_path().

Part of the interface alignment discussed in #2531.
2026-08-07 16:34:12 -04:00
yhirose bd02a50cbb ci: auto-comment on issue #2533 when windows-without-SSL job fails
Temporary instrumentation to track the intermittent windows-without-SSL
failures reported in #2533. When the job fails on a push, it posts the
run URL, commit, and per-shard failed-test lines as a comment on the
issue, building up failure-pattern history automatically.

This should be removed once the root cause is found and fixed.
2026-08-07 16:07:40 -04:00
yhirose 1c2607cbb8 Merge the SSLClient and WebSocketClient TLS session setup
SSLClient::initialize_ssl kept its own copy of the session setup that
detail::setup_client_tls_session already implemented for WebSocketClient.
Extend the shared function with the pieces only SSLClient needed - a session
verifier, an independent hostname verification flag, the context mutex,
Windows Schannel verification and error details - and let initialize_ssl
build a ClientTlsSessionOptions and call it. All of them default, so
WebSocketClient's call site is unchanged.

This settles one difference between the two: WebSocketClient used to call
tls::set_hostname for named hosts, which on OpenSSL turns on verification
during the handshake, while SSLClient always set SNI only and verified
post-handshake. The shared function now does the latter for both, so
tls::set_hostname loses its last caller and goes away, as does the
write-only SSLClient::verify_result_.

Certificate verification with a host name rather than an IP literal was the
one combination the WebSocket tests never covered, and it is exactly the
path this normalizes. WebSocketSSLDnsHostTest fills that in; cert2 gains a
DNS:localhost SAN so a name can be verified against it.
2026-08-07 13:41:39 -04:00
yhirose 98be5fd3a6 Share the default Host and User-Agent header logic between the clients
ClientImpl::prepare_default_headers and WebSocketClient::prepare_default_headers
each built the Host value with the same AF_UNIX special case and appended the
same User-Agent. Move both into detail:: so there is one copy.

The Host helper returns only the value, because the two callers disagree on
where it goes: ClientImpl prepends it per RFC 9110 5.3, WebSocketClient appends.
The User-Agent helper takes the Request, since it has to consult and set a
header rather than compute a string, and it stays inside ClientImpl's
content_receiver branch so that path keeps sending no User-Agent.
2026-08-07 13:41:39 -04:00
yhirose d2ef193b9c Let WebSocketClient take a CA directory the way ClientImpl does
WebSocketClient::set_ca_cert_path took a single path and create_stream()
hardcoded an empty directory when calling detail::load_client_ca_config, while
ClientImpl has always accepted (ca_cert_file_path, ca_cert_dir_path = ""). Give
WebSocketClient the same signature and store the directory, so both clients
configure CA loading identically. The one-argument form is unchanged for
callers.

Also note at both call sites why the "load the CA config once" guard differs:
SSLClient needs call_once because one client serves concurrent requests, and
WebSocketClient does not because connect() is not safe to call concurrently
anyway.
2026-08-07 13:41:38 -04:00
yhirose a1aa2ad9cd Give WebSocketClient the chrono::duration timeout setters ClientImpl has
WebSocketClient only accepted timeouts as (time_t sec, time_t usec), while
ClientImpl has taken std::chrono::duration overloads for its read, write and
connection timeouts for a long time. Add the same three overloads, forwarding
through the existing detail::duration_to_sec_and_usec helper so the split
matches ClientImpl exactly.

The template bodies go above the first split.py BORDER, next to the class, so
that the .h/.cc split keeps them in the header where instantiation needs them.
2026-08-07 13:41:38 -04:00
shaozk b2b1d56d6d fix: server example typos (#2532) 2026-08-06 19:34:24 -04:00
metsw24-max 2b8658fa99 match mount points on a segment boundary in handle_file_request (#2529) 2026-08-03 17:51:50 -04:00
yhirose 095a5c1caf Release v0.52.0 2026-08-02 19:41:06 -04:00
yhirose 6e8a7dcd3f Bind the ordering tests to an ephemeral port (#2528)
test.cc says right above the PORT constant that it is only for the
legacy fixtures and that new standalone tests must use
bind_to_any_port() instead. The six tests added with the insertion-order
work all took the shared PORT anyway, which made them one more thing
contending for it.

Move them to bind_to_any_port() plus listen_after_bind(), the pattern
the note asks for and the rest of the standalone tests already use.
HeadersOrderTest.ReceivedFieldsKeepTheirOrder sends a raw request rather
than going through Client, so send_request() gains an optional port that
defaults to PORT and leaves its other 36 callers alone.

Checked by holding PORT open from another process while running the six:
they pass, where before the change listen(HOST, PORT) would have failed.
2026-08-02 19:33:07 -04:00
yhirose 148d61a6a3 Give the raw listener in wait_writable_INET the socket options its neighbours have (#2527)
SocketStream.wait_writable_INET binds PORT + 1 directly, and it was the
only raw listener in the file that did not set SO_REUSEPORT/SO_REUSEADDR
first. PORT + 1 is shared with three SSL redirect tests and with
VulnerabilityTest.CRLFInjectionInHeaders, so once any of those has run,
the TIME_WAIT entries they leave make bind() fail here.

That failure did not surface where it happened. The bind runs on a
worker thread, where a failed ASSERT_EQ only returns from the lambda, so
the test carried on and failed later at ASSERT_NE(disconnected_svr_sock,
-1) with nothing pointing at the port.

Within one run of the suite the test comes before everything that uses
PORT + 1, so a clean run passes; it failed when a previous process had
left TIME_WAIT behind, which made it look intermittent. Running any of
those four tests first and then this one reproduces it every time: 4 of
4 before this change, 4 of 4 passing after, with up to eight TIME_WAIT
entries on the port.

Call default_socket_options(), which is what the library does for its
own listeners and what the other raw listeners in this file already do.
2026-08-02 19:32:03 -04:00
yhirose c48ed1ed9a Share the query pair splitting between its two callers (#2526)
parse_query_text() and normalize_query_string() both walk a query string
and both open with the same eight lines to cut one "key=value" span at
its first '='. Give that a name and call it from both.

divide() puts everything before the first delimiter in the left half and
the rest in the right, so a span with no '=' lands entirely in key and
leaves val empty. Both callers rely on that: parse_query_text() records
a bare "flag" with an empty value, and normalize_query_string() emits it
back without an '='. The helper's comment says so, since that is the
part of divide()'s behaviour a reader has to know to follow either
caller.
2026-08-02 19:31:46 -04:00
yhirose 8d428361fb Count entries with count() rather than equal_range plus distance (#2525)
Seven accessors each spelled the same two lines:

    auto r = x.equal_range(key);
    return static_cast<size_t>(std::distance(r.first, r.second));

The containers behind them all became detail::insertion_ordered_multimap
along the way, so count() is available and says what these functions
mean. The work is the same either way: equal_range() scans to the first
match and distance() then walks the restricted iterator to the end,
comparing keys at each step, which comes to one pass over the entries,
and count() is one pass too. This is a readability change, not a faster
one.

Covers get_header_value_count, Request and Response
get_trailer_value_count, Request::get_param_value_count,
MultipartFormData get_field_count and get_file_count, and
Result::get_request_header_value_count. The distance() call in
get_param_values() stays, since it sizes a reserve() and needs the range
anyway.
2026-08-02 19:31:22 -04:00
yhirose 23f67f25c2 Preserve the order of multipart form parts (#2524)
FormFields and FormFiles were std::multimaps, which sort by field name.
RFC 7578 5.2 says a form processor "SHOULD send back results in order"
and that "Intermediaries MUST NOT reorder the results", so a handler
walking req.form.fields saw the parts alphabetised rather than as they
were sent, and a body received for forwarding could not be reproduced.

Point both at the insertion-ordered container #2523 generalized, with
std::equal_to since field names are case-sensitive. Entries sharing a
name already kept their relative order under std::multimap; what is
recovered here is the order across different names.

Server::read_content() keeps a FormFields::iterator alive across the
content callbacks that fill the part it points at, which is the one
thing this container could have broken: it is vector-backed, so a later
emplace can reallocate and leave an older iterator dangling. The code is
safe because the iterator is reassigned by the same emplace that could
reallocate, and is only read while the flag set alongside it says so.
ContentSurvivesContainerGrowth pins that down with 64 parts, enough to
grow the vector through seven reallocations; reverting the reassignment
makes it abort under ASan rather than fail quietly.

Growth also never copies a part's payload: the parser inserts the entry
with an empty content and appends the body bytes afterwards, and both
mapped types are nothrow-move-constructible, so a reallocation steals
the string buffers rather than deep-copying them.
2026-08-02 18:37:20 -04:00
yhirose 7963c382d6 Preserve the order of query parameters (#2523)
Params was a std::multimap, which sorts by parameter name. Parsing a
query string therefore threw away the order it arrived in, and building
one back out of Params handed the caller an alphabetised query rather
than the one they wrote. ClientImpl::send() takes that path whenever a
request carries Params without a query already in its path, so a caller
signing its query string could not reproduce the order it asked for.
normalize_query_string() exists in part to work around exactly this.

Generalize the container #2520 introduced for Headers into
detail::insertion_ordered_multimap<Mapped, KeyEqual> and alias both
types to it. Params passes std::equal_to, so parameter names stay
case-sensitive, while Headers keeps matching field names
case-insensitively. The name says insertion_ordered because in STL
vocabulary std::multimap is the ordered one, which is the reading this
change exists to correct.

This is a correctness change, not a performance one. Measured on the
real paths against the previous implementation, parse_query_text() over
eight parameters goes 931.5ns -> 935.1ns and params_to_query_str()
374.7ns -> 377.2ns; both are inside the run-to-run noise. The container
is a small share of that work, most of which is the string building in
decode_query_component().

Params picks up the same API changes Headers took in #2520: iterators
follow std::vector rules, value_type is std::pair<std::string,
std::string>, and insert(hint, value) is gone. Headers itself becomes an
alias rather than a class, so the names it appears in mangle differently
again; #2520 has not shipped in a release yet, so this costs nothing on
top of the break already there.
2026-08-02 12:40:00 -04:00
yhirose 486c81b275 Determine the final transfer coding across Transfer-Encoding lines (#2522)
RFC 9110 5.3 lets a coding list be split across several Transfer-Encoding
lines, which combine, in the order the lines were received, into one
comma-separated list. RFC 9112 6.1 then frames the message as chunked
only when chunked is the final coding of that combined list.

is_chunked_transfer_encoding() could not apply that rule while Headers
was an unordered_multimap, since the order of the lines was not
recoverable, so it fell back to reporting any message naming chunked on
any line as chunked. Headers now preserves the order the lines were
received in, so read the final coding directly: the last token of the
last line.

The fallback erred toward reporting chunked because mis-reading a
chunked message as unframed leaves its body in the socket, where a
keep-alive connection parses it as a smuggled request. That direction is
no longer needed. A request whose combined list ends in something other
than chunked is now reported as not chunked, and process_request()
answers 400 and closes rather than letting it reach the "no body" path,
which is what it already did for the single-line "chunked, gzip" form.
So `Transfer-Encoding: chunked` followed by `Transfer-Encoding: gzip` is
rejected instead of being read as chunked, and `gzip` followed by
`chunked` is still accepted.

A trailing line carrying no coding at all now leaves the combined list
ending in nothing rather than inheriting the coding from the line
before it.
2026-08-02 01:05:26 -04:00
yhirose be28cf9435 Run CIFuzz only for pull requests that touch the fuzzed code (#2521)
The fuzzers build httplib.h and the targets under test/fuzzing, so a pull
request that touches neither has nothing for CIFuzz to exercise. Fuzzing
is by far the longest job in CI: 600 seconds of fuzzing on top of
building the OSS-Fuzz image, which came to 12m20s on a recent run while
every other job finished within 5m7s.

Filter the trigger by path rather than shortening fuzz-seconds. OSS-Fuzz
recommends 600 seconds as a minimum, and the budget is divided among all
of the project's fuzz targets, so with five targets a shorter run would
leave each one well under two minutes. Skipping the job for
documentation-only changes cuts the wait without giving up any fuzzing
on the pull requests that do reach the parsers.
2026-08-02 00:33:11 -04:00
yhirose d860c842ea Preserve the order of header fields with the same name (#2520)
RFC 9110 5.3 makes the order of header fields sharing a field name
significant, but Headers was a std::unordered_multimap, which gives no
ordering guarantee for equivalent keys. libstdc++ hands duplicates back
in reverse insertion order while libc++ uses insertion order, so
get_header_value() returned a different field depending on the platform,
and code that picks a value out of an accidentally or maliciously
duplicated field name had no way to say which one it wanted.

Replace Headers with a small container that keeps the fields in the
order they were received or set. Storage is a flat vector and lookup is
a linear scan, which beats hashing for the at most
CPPHTTPLIB_HEADER_MAX_COUNT fields a message carries. begin()/end() walk
every field, while find() and equal_range() hand back the same iterator
type restricted to one field name; equality compares only the position,
so a restricted iterator still compares equal to end(). Erasing an
equal_range() therefore removes only the fields with that name and
leaves interleaved fields alone.

std::multimap was the smaller change but sorts by field name, which
would stop control data such as Host from leading the message. Instead
Host is now prepended via emplace_front() so it keeps its place at the
front of a request.

Two side effects worth noting: incrementing past the last field of a
name now saturates at end(), so an out-of-range id passed to
get_header_value() returns the default instead of running off the
container as it did before; and iterators follow std::vector rules, so
they are invalidated by insertion.

Fixes #2509
2026-08-02 00:22:41 -04:00
yhirose 8e08c22783 Cut a syscall and the byte-at-a-time line reader out of the request path (#2513)
* Skip the redundant readability poll on the first read of a request

process_server_socket_core() calls keep_alive(), which polls the socket
and only invokes the callback once it reports readable. The stream is
then constructed and its first read polls the very same socket again
before calling recv, asking the kernel a question that was answered
microseconds earlier.

Profiling a request shows how little else is going on: of the samples
taken while the worker was off the keep-alive wait, 85% sat in syscalls
(recv, send, and the two polls) and only 15% in header parsing, routing
and response serialization. Removing one of five syscalls per request is
worth more than anything reachable inside that 15%.

Let the caller hand the stream what it already knows. The hint is
consumed by the first read, so body reads and every later request keep
polling as before, and set_read_timeout() during a WebSocket upgrade is
unaffected. Nothing is skipped when a read finds the buffer empty on its
own. The accepted socket also carries SO_RCVTIMEO from listen_internal(),
so even a wrong hint could not block forever.

Measured with wrk -t2 -c8 over three interleaved runs, server CPU per
request drops from 33.4/32.5/29.7us to 23.6/22.9/23.9us, and throughput
rises 10-15%. The TLS path gets the same treatment - it shares
process_server_socket_core(), and SSLSocketStream::read() polls after
tls::pending() comes up empty - for a smaller 44.9->40.2us, crypto being
the larger cost there.

Removing the second poll, in write(), looks worth another 15% but is left
alone: it would rely on SO_SNDTIMEO being set on every socket a
SocketStream is built over, and the TLS write path retries on WantWrite
in a way that turns a send timeout into a much longer stall.

* Scan buffered bytes for line ends instead of reading one at a time

stream_line_reader::getline() pulls the request line and every header
through strm_.read(&byte, 1). A 300-byte header block therefore costs
300 virtual calls, each with its own bounds check and one-byte copy.
None of them are syscalls - SocketStream has already pulled up to 4KB
off the socket - so this is pure CPU spent one character at a time.

On a Linux CI runner read_headers() takes 2.44us for seven headers, of
which 1.53us is this loop. That is 63% of header parsing, and header
parsing is a bigger share of the total on Linux than the profiling on
macOS suggested: 3.8us of HTTP processing against 16us of server CPU
per request, versus roughly 3us against 30us on macOS. Cheap syscalls
leave parsing a larger slice of what remains.

Let a stream offer what it has already buffered, and scan that for the
terminator in one pass. Streams that do no buffering of their own report
none and keep the existing byte loop, so Stream subclasses outside the
library are unaffected, as is the TLS path - SSLSocketStream has no
buffer of its own to expose.

A bare LF still does not end a line in the default configuration, so the
scan looks for CRLF and carries the CR across a chunk boundary. Both
append overloads now share one path: the per-character one used to
decide where to write from fixed_buffer_used_size_ alone, which after a
bulk append had spilled into the growable buffer would send the byte to
a fixed buffer that ptr() and size() no longer read. That dropped a byte
from over-long request lines - caught by ServerTest.TooLongRequest and
AlmostTooLongRequest.
2026-08-01 22:46:10 -04:00
yhirose f51df1473b Merge branch 'metsw24-max-mmap-map-failed-guard' 2026-08-01 22:21:46 -04:00
yhirose 15a23abd7e Clarify the MAP_FAILED guard comment and assert the cleared size
Explain why `addr_` is reset before `close()`: `munmap()` must not be
called with the sentinel. The test also checks `size()`, since the hazard
is a stale size paired with a sentinel `data()`.
2026-08-01 22:21:26 -04:00
yhirose 2e37c51921 Merge branch 'mmap-map-failed-guard' of github.com:metsw24-max/cpp-httplib into metsw24-max-mmap-map-failed-guard 2026-08-01 22:16:28 -04:00
yhirose 571717adb8 Merge branch 'metsw24-max-range-206-only' 2026-08-01 21:54:25 -04:00
yhirose 5539a66c63 Cover the non-206 Range paths and drop the duplicated test route
The added test only exercised a 403 with a single range, leaving three
paths that were equally broken untested: a suffix range, whose first_pos
is still -1 when the bounds asserts are compiled out under NDEBUG; two
ranges, where apply_ranges() makes no boundary for a non-206 response and
the multipart branch wrote an empty one; and a 2xx that is not a 206,
which was validated but still announced the full content length.

Fold the copied route back into /streamed-with-range with a status query
parameter, following the ?error idiom already used there.
2026-08-01 21:47:35 -04:00
yhirose 6b723fbf9d Merge branch 'range-206-only' of github.com:metsw24-max/cpp-httplib into metsw24-max-range-206-only 2026-08-01 21:38:55 -04:00
yhirose 1562f0ec4f Pass unrecognized Content-Encoding values through instead of failing (#2518)
Since 8bba34e, prepare_content_receiver() rejected every non-empty
Content-Encoding that create_decompressor() could not handle, conflating an
unrecognized content coding with a known one whose support was not compiled
in. A response labeled `Content-Encoding: UTF-8` -- a header some servers
misuse to advertise a character set -- therefore failed with 415, which the
client surfaced as the generic Error::Read. Before that commit the raw body
came back untouched.

Restore that behavior: reject only a coding cpp-httplib recognizes but was
not built with, and leave an unrecognized one (including "identity") alone.
Match codings case-insensitively while here, as RFC 9110 8.4.1 requires;
otherwise `Content-Encoding: GZIP` would look unrecognized and hand back a
still-compressed body.

open_stream() had the mirror-image problem. It silently passed the payload
through whenever create_decompressor() returned null, so a gzip response on
a build without zlib reached the caller still compressed, and it never
checked is_valid() -- gzip_decompressor::decompress() only asserts, so a
failed inflateInit2() was undefined behavior in release builds. It now
applies the same policy as the buffered path.

Add Error::UnsupportedContentEncoding so callers can tell this apart from a
read failure, and report an unusable decompressor as Error::Compression. The
status read_content() writes was previously discarded into an uninitialized
dummy_status.
2026-08-01 21:25:22 -04:00
yhirose a691e531c3 Close the listening socket in stop() even when not serving (#2517)
* Close the listening socket in stop() even when not serving

Server::stop() released svr_sock_ only under `if (is_running_)`, and
is_running_ is set inside listen_internal(). A server that bound with
bind_to_port() / bind_to_any_port() and never reached listen_after_bind()
therefore kept its listening descriptor for the life of the process:
~Server() is defaulted and svr_sock_ is a bare atomic<socket_t>, so nothing
else closes it. The port stayed held too.

Close whenever the socket is open instead. The exchange already made this
safe against a concurrent accept loop, so dropping the is_running_ gate also
removes a TOCTOU between the check and the exchange.

* Fail listen_after_bind() when stop() already closed the socket

With stop() now releasing a bound-but-not-serving socket, a stop() that
lands between bind and listen used to slip through listen_internal():
the accept loop saw INVALID_SOCKET, never iterated, and returned success
without ever serving, pulsing is_running_ just long enough that a
wait_until_ready() caller could miss it and spin forever. Return false
instead and mark the server decommissioned the way any failed listen
does, so waiters wake up.

Also drop the assert() in stop(). It read is_running_ and svr_sock_
separately, which is exactly the race the exchange removes: a second
stop() while the accept loop is still unwinding sees is_running_ true
and svr_sock_ already INVALID_SOCKET, aborting debug builds.

The regression test checks the released port with a raw connect()
instead of a Client request: against the old code the connection is
accepted into the backlog and never answered, which would hang the test
rather than fail it.
2026-08-01 19:57:37 -04:00
yhirose ae8356d86e Clean up addr_map hostname support
Follow-up to 49b921b.

Move the duplicated addr_map lookup into detail::apply_addr_map, shared by
ClientImpl::create_client_socket and WebSocketClient::connect.

Add a WebSocketClient test for a hostname mapped value, so that path has
the same coverage as the Client one. Guard its teardown with scope_exit:
a failing ASSERT_TRUE returns from the test body, and destroying a still
joinable std::thread calls std::terminate, taking the whole binary down.

Document set_hostname_addr_map in README. It had no entry at all.
2026-08-01 14:51:35 -04:00
Kim, Hyuk 49b921b52d Allow addr_map_ values to be hostnames, not just IP literals (#2515)
addr_map_ only accepted IP literals as mapped values; a non-IP value was
passed as the `ip` argument and rejected by getaddrinfo's AI_NUMERICHOST
path. The lookup in ClientImpl::create_client_socket and
WebSocketClient::connect now checks the mapped value with
detail::is_ip_address: IP literals keep the existing AI_NUMERICHOST path,
while hostnames are passed as the connect host so they get resolved.

host_ is never touched, so it keeps supplying the Host header and SNI in
both cases.

is_ip_address() moved into the non-SSL detail block so that both client
code paths can use it without CPPHTTPLIB_SSL_ENABLED.

This also fixes the documented Unix domain socket client example, where
the mapped value is a socket path: it previously took the AI_NUMERICHOST
path and never reached the AF_UNIX branch in create_socket.
2026-08-01 14:29:20 -04:00
yhirose 447b9c4a29 Buffer the WebSocket handshake before writing it to the socket
Follow-up to #2514. The rebuilt handshake wrote the request line straight
to the socket, so a header rejected by check_and_write_headers left a
truncated "GET /ws HTTP/1.1" sitting in the peer's buffer before the
connection was torn down, and every header cost its own small write.

Build the request into a BufferStream and flush it in one go, matching
ClientImpl::write_request. The new test drives a raw listener and asserts
the peer sees a clean EOF with zero bytes; without this change it observes
18.

Also fold WebSocketTest.HostHeaderInHandshake into
WebSocketTest.DefaultHeadersInHandshake, which covers the same Host
assertion through the capture helper #2514 introduced.
2026-08-01 14:09:53 -04:00
yhirose f406808497 Merge pull request #2514 from Hyukya/master
websocket: rebuild handshake on Request (support Host override, align with HTTP client behavior)
2026-08-01 08:20:23 -04:00
hyuk.kim 2c28d2fa3e websocket: rebuild handshake on Request/header pipeline
Replace the hand-built upgrade request string in
perform_websocket_handshake with the same Request,
write_request_line, and check_and_write_headers path used by
ClientImpl::open_stream. Add WebSocketClient::prepare_default_headers
to inject Host and User-Agent defaults; protocol-mandatory headers
(Upgrade, Connection, Sec-WebSocket-Key/Version) are always
overwritten.
2026-08-01 17:15:58 +09:00
yhirose f24e79aab9 Fix benchmark-run reporting success without measuring anything
The first run of this workflow exposed three problems.

Crow's amalgamated header includes <asio.hpp>, which no runner provides,
so the build died immediately. It compiled locally only because CPATH
happened to point at Homebrew's include directory. Install asio
explicitly, and on macOS pass its include path through CROW_CXXFLAGS.

The macos runner image has no Go, so bombardier could not be installed.
Add actions/setup-go, which also pins a known toolchain on Linux.

Worst of all, the ubuntu job reported success. `make ... | tee` returns
tee's status, so the failed build was invisible. Enable pipefail. That
alone is not enough: every recipe in benchmark/Makefile ends in `kill`,
so make still exits 0 when bombardier itself fails to run. Assert that
the expected number of "Reqs/sec" lines came out.
2026-07-30 20:59:12 -04:00
yhirose 60f285a301 Add a workflow to run the committed benchmark on CI
benchmark/Makefile has always been local-only, so the numbers it produces
were never recorded anywhere. Wire it up to a manual workflow so a run can
be kicked off and its output kept in the job summary.

This does not gate anything: it reports absolute throughput for the
current ref, with Crow v1.3.1 alongside for reference. Absolute req/s is
only comparable against other runs on the same runner type, which is why
the ref, runner and load parameters are recorded next to the numbers.

Use benchmark-ab instead when the question is whether a specific change
made things faster; comparing absolute numbers across runs cannot answer
that.

Linux and macOS only. Windows needs benchmark/Makefile rewritten first,
since it relies on nc, & and kill.
2026-07-30 20:52:50 -04:00
yhirose 29ceccecd6 Add A/B throughput benchmark for comparing two refs
The existing test_benchmark asserts a single request completes within
5ms, which catches gross connection-setup regressions but cannot see
throughput changes: the effects we care about are tens of microseconds
per request, a hundredth of that resolution. There was no way to answer
"does this patch make the server faster" other than measuring by hand.

Absolute req/s is not usable for that. Running the same binary five
times on an idle 8-core machine gave 53.9k to 74.6k req/s, and shared CI
runners are noisier still, so a number printed per push says nothing.

Build both refs and measure them alternately in one session, flipping
the order each round to cancel ordering bias, then report only the ratio
of the medians. Whether that ratio means anything is decided by an exact
permutation test rather than by comparing the change against the min/max
spread - a single slow round is enough to make a spread check give up,
while the rank test rides it out.

Validated against a patch that removes a redundant poll() per request:
individual measurements ranged 41.7k-93.9k req/s, yet nine rounds
resolved a 1.244x speedup at p = 0.019. The same data truncated to five
rounds was inconclusive, so the workflow defaults to nine.

Manual dispatch only, Linux and non-SSL for now, and it never fails the
build - this is a measurement, not a test.
2026-07-30 19:56:03 -04:00
yhirose 85ec3963bc Increase default listen backlog from 5 to 128
The accept queue was limited to 5 pending connections, which overflows
easily under connection churn or a burst of simultaneous connects (LB
health checks, thundering herd on restart). On overflow the kernel
silently drops the ACK rather than failing fast, so clients stall on
SYN/ACK retransmission backoff.

Benchmark (bombardier -c 10 -d 10s, 3 trials, interleaved with the old
binary) shows max latency dropping from 48-89ms to 5.8-11.2ms, while
p99 is unchanged - the fix affects only the extreme tail, as expected.
2026-07-30 17:19:36 -04:00
Sayed Kaif f4fce42e77 fail mmap::open when ::mmap returns MAP_FAILED 2026-07-30 00:46:08 +05:30
Sayed Kaif 23fef15e07 apply Range only to a 206 response in write_content_with_provider 2026-07-28 13:19:35 +05:30
yhirose 34b7c846d4 Merge pull request #2496 from superm1/superm1/SWSPLAT-23622
Sanitize uploaded filenames in upload example to prevent path traversal
2026-07-27 17:11:45 -04:00
Mario Limonciello 76b54e7de4 Sanitize uploaded filenames in upload example to prevent path traversal
The upload example wrote each uploaded file using the filename supplied
verbatim in the multipart Content-Disposition header. A client could set
that filename to an absolute path or one containing "../" components and
cause the server to create or overwrite files outside the working
directory.

Reduce each client-supplied filename to its base name and reject the
request with 400 Bad Request if the result is empty, ".", "..", or
still contains a path separator (including colon for Windows drive
letters).
2026-07-27 10:56:49 -05:00
yhirose 5b9d1495ff Apply path encoding to open_stream()
ClientImpl::open_stream() passed the caller-supplied path straight to the
request line, so it ignored path_encode_ entirely and always behaved as if
set_path_encode(false) had been called. The same path therefore produced
different bytes on the wire depending on which API was used:

  Get()          "/a b"  ->  GET /a%20b HTTP/1.1
  open_stream()  "/a b"  ->  GET /a b HTTP/1.1

A space is the request-target delimiter, so the streaming form is not merely
inconsistent: an RFC 9112 conformant server reads the target as "/a" and the
version as "b". Non-ASCII bytes and '+' diverged the same way, the latter
changing the value a server that decodes '+' as space sees.

Extract the path/query splitting and encoding out of ClientImpl::write_request
into detail::encode_request_target() and call it from both paths, so the
encoding rule lives in one place. open_stream() appends Params before
encoding, matching ClientImpl::Get(path, params), which builds its target the
same way.

Note a behavior change: with path encoding enabled, CR/LF in the target is now
percent-encoded and sent rather than rejected with Error::Write, matching
Get(). This is not a weakening of the CR/LF guard in write_request_line() --
that check is independent of path_encode_ and still backstops
set_path_encode(false), where encode_path() does nothing.
2026-07-25 13:05:48 -04:00
yhirose d66d9a9599 Release v0.51.0 2026-07-23 22:52:55 -04:00
yhirose 2fa0417754 Fix mbedTLS is_peer_closed() destroying the first response byte
Mbed TLS has no SSL_peek() equivalent, so is_peer_closed() (called after
every SSL request write) probed liveness with a real 1-byte
mbedtls_ssl_read() and discarded whatever it read. If the response had
already arrived by the time the probe ran — plausible under CI load or
plain OS scheduling — the probe silently ate the first byte of the
status line, corrupting the response and surfacing as a fast
"Failed to read connection" failure.

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

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

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

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

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

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

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

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

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

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

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

* Document Mbed TLS 4.x support and libtfpsacrypto rename

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

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

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

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

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

* declare write_request_line in test.cc for split builds

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

* Fail the request when write_request_line rejects the target

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

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

---------

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

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

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

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

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

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

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

* Fix clang-format style errors in test.cc

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

* Update README Get() examples to disambiguate Headers overload

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fix formatting

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

---------

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

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

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

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

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

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

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

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

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

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

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

* add WebSocketTest unit test cases
* SpecifyServerIPAddress_AnotherHostname
* SpecifyServerIPAddress_RealHostname

* Change wrong_ip from 0.0.0.0 to 192.0.2.1

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

* Fix style check

* set short timeout in WebSocketTest.SpecifyServerIPAddress_RealHostname

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

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

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

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

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

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

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

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

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

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

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

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

* Add NO_PROXY parsing and matching helpers in detail namespace

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

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

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

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

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

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

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

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

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

* Add Client::set_proxy_from_env with httpoxy mitigation

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

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

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

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

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

All 608 unit tests pass.

* Add NO_PROXY behavior tests

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

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

Coverage:

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

  Wildcard
    - "*" bypasses everything

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

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

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

  Backward compat
    - default behavior unchanged when set_no_proxy is never called

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

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

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

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

* Document set_no_proxy and set_proxy_from_env in README

Adds two subsections under "Proxy server support":

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

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

Documentation only. Closes the doc gap from #2446.

* Document NO_PROXY and set_proxy_from_env in cookbook c16-proxy

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

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

Adds two sections:

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

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

* Simplify NO_PROXY implementation per review

Apply seven post-implementation cleanups:

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

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

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

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

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

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

  - Drop test comments that paraphrased their own test name.

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

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

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

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

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

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

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

Net effect:

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

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

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

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

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

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

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

* Add NO_PROXY tests covering edge cases found during PR review

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

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

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

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

635 unit tests pass.

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

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

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

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

* Drop set_proxy_from_env per #2446 discussion

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

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

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

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

* Skip digest 407 retry when target is bypassed by NO_PROXY

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

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

Regression test BypassedTargetReturning407DoesNotLeakProxyDigest
Credentials reproduces the leak without this gate.

* Make set_no_proxy safe across redirects and keep-alive

Two correctness bugs that the dynamic NO_PROXY API exposed:

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

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

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

* Tighten NO_PROXY entry parsing

Three small parser fixes surfaced during code review:

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

New regression tests: BracketedIPv6EntryAccepted,
BracketedIPv6CidrEntryAccepted, TrailingSlashCidrIsRejected.

* Refactor: introduce disconnect() and remove invalidate_keep_alive_socket

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

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

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

Net -47 lines in httplib.h.

* Fix MultiHopRedirect test on Windows; trim NoProxyTest comments

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

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

* Consolidate NoProxyTest server boilerplate; drop hardcoded sentinel ports

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

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

Verified pass under 4-shard parallel run.

* Trim README NO_PROXY section to match surrounding granularity

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

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

* Restrict Keychain cert loading to macOS

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

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

Addresses the iOS build break reported in #2454.

* Add iOS header parse check to CI

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

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

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

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

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

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

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

Fix #2444

* Strip ASAN from test_thread_pool to coexist with pthread_create override

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fix split build for #2431 reproducer tests

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

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

* Cap issue-2431 repro job at 5 minutes

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

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

* Enable ASAN detect_stack_use_after_return for #2431 repro

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

* Revert ASAN detect_stack_use_after_return for #2431 repro

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

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

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

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

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

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

---------

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:50:38 -04:00
yhirose 09d00c099c Update README 2026-04-11 22:24:04 -04:00
yhirose 6bdd657713 Enhance WebSocket support with unresponsive-peer detection and documentation updates
- Added `set_websocket_max_missed_pongs` method to configure unresponsive-peer detection.
- Updated README and documentation to clarify WebSocket limitations and features.
- Introduced tests for detecting non-responsive peers and ensuring responsive peers do not trigger timeouts.
2026-04-11 22:17:38 -04:00
yhirose b4eec3ee77 Removed deprecated APIs (#2423) 2026-04-11 20:54:06 -04:00
yhirose c0248ff7fc Add links to other topics in Cookbook documents 2026-04-11 20:40:08 -04:00
yhirose 203e1bf2ac Code cleanup 2026-04-11 20:40:08 -04:00
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 SlabyandJiri 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
yhirose 073b587962 Release v0.41.0 2026-04-03 21:50:24 -04:00
yhirose 96785eea21 Add parse_url 2026-04-03 20:54:17 -04:00
yhirose 3093bdd9ab Fix #2416 2026-04-03 18:27:12 -04:00
crueter 6607a6a592 [cmake] Allow using pre-existing zstd target if it exists (#2390)
adds support for pre-existing `zstd::libzstd` which is useful for
projects that bundle their own zstd in a way that doesn't get caught by
`CONFIG`

Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-30 21:26:20 -04:00
DavidKorczynski 831b64bdeb Add two new fuzzers (#2412)
The goal is to increase code coverage by way of OSS-Fuzz. A recent code
coverage report is available at
https://storage.googleapis.com/oss-fuzz-coverage/cpp-httplib/reports/20260326/linux/report.html

Signed-off-by: David Korczynski <david@adalogics.com>
2026-03-28 15:00:10 -04:00
yhirose 32c82492de Add workflow_dispatch trigger to docs deployment workflow 2026-03-28 11:08:25 -04:00
yhirose b7e02de4a7 Release v0.40.0 2026-03-28 00:57:24 -04:00
yhirose a9359df42e Optimize multipart content provider to coalesce small writes and reduce TCP packet fragmentation (Fix #2410) 2026-03-28 00:23:59 -04:00
yhirose 9a97e948f0 Add set_socket_opt function and corresponding test for TCP_NODELAY option (Resolve #2411) 2026-03-28 00:23:59 -04:00
yhirose 6fd97aeca0 Implement request body consumption and reject invalid Content-Length with Transfer-Encoding to prevent request smuggling 2026-03-27 23:16:08 -04:00
yhirose 05540e4d50 Fixed warnings 2026-03-27 22:35:22 -04:00
yhirose ceefc14e7d Use go-httplibbin 2026-03-27 22:26:14 -04:00
yhirose a77284a634 Release v0.39.0 2026-03-23 23:14:05 -04:00
yhirose 315a87520d Add release script and update .gitignore for work directory 2026-03-23 23:06:37 -04:00
yhirose 703abbb53b Prevent forwarding of authentication credentials during cross-host redirects as per RFC 9110. Add tests for basic auth and bearer token scenarios. 2026-03-23 22:32:53 -04:00
yhirose cb8365349f Fix #2404 (Refactor make_file_body to improve file handling and scope management) 2026-03-22 22:40:01 -04:00
yhirose 3792ce0da7 Add socket configuration options and corresponding test case for WebSocketClient. Fix #2401 2026-03-22 22:31:59 -04:00
yhirose 7178f451a4 "Building a Desktop LLM App with cpp-httplib" (#2403) 2026-03-21 23:31:55 -04:00
yhirose c2bdb1c5c1 SSE Client: Update Authorization Header
Fixes #2402
2026-03-21 13:17:28 -04:00
yhirose 45820de332 Enhance stream handling in LongPollingTest and add new test for client close detection 2026-03-18 18:29:19 -04:00
yhirose dd8071a7d4 Fix #2397 2026-03-17 17:39:57 -04:00
v-nam c59ef98b3b [cmake] Update modules.cmake to fix cmake error (#2393)
* Update modules.cmake

* tst1
2026-03-17 12:09:51 -04:00
yhirose 1c6b3ea5a0 Add status: "draft" to multiple documentation pages and enhance navigation sections 2026-03-14 23:46:14 -04:00
yhirose 4a1e9443ee Update deprecation messages to indicate removal in v1.0.0 2026-03-14 23:32:07 -04:00
yhirose 6f2717e623 Release v0.38.0 2026-03-14 23:17:13 -04:00
yhirose 257b266190 Add runtime configuration for WebSocket ping interval and related tests 2026-03-14 22:44:17 -04:00
yhirose ba0d0b82db Add benchmark tests and related configurations for performance evaluation 2026-03-14 22:16:53 -04:00
yhirose 5ecba74a99 Remove large data tests for GzipDecompressor and SSLClientServerTest due to memory issues 2026-03-14 21:50:55 -04:00
yhirose ec1ffbc27d Add Brotli compression support and corresponding tests 2026-03-14 21:42:48 -04:00
yhirose 4978f26f86 Fix port number in OpenStreamMalformedContentLength test to avoid conflicts 2026-03-14 20:59:15 -04:00
yhirose bb7c7ab075 Add quality parameter parsing for Accept-Encoding header and enhance encoding type selection logic 2026-03-14 20:51:25 -04:00
yhirose 1c3d35f83c Update comment to clarify requirements for safe handling in ClientImpl::handle_request 2026-03-14 19:27:51 -04:00
yhirose b1bb2b7ecc Implement setup_proxy_connection method for SSLClient and refactor proxy handling in open_stream 2026-03-14 19:26:31 -04:00
yhirose f6ed5fc60f Add SSL support for proxy connections in open_stream and corresponding test 2026-03-14 18:38:34 -04:00
yhirose 69d468f4d9 Enable BindDualStack test and remove disabled large content test due to memory issues 2026-03-14 18:22:27 -04:00
yhirose 2e61fd3e6e Update documentation to clarify progress callback usage and user data handling in examples 2026-03-13 22:54:29 -04:00
yhirose 3ad4a4243a Update .gitignore to include AGENTS.md and related documentation files 2026-03-13 22:31:44 -04:00
yhirose 511e3ef9e5 Update README.md to enhance TLS backend documentation and clarify platform-specific certificate handling 2026-03-13 22:30:05 -04:00
yhirose f787f31b87 Implement symlink protection in static file server and add corresponding tests 2026-03-13 16:22:16 -04:00
yhirose 43a54a3e3d Add tests for Unicode path component decoding in decode_path_component function 2026-03-13 00:32:41 -04:00
yhirose 83e98a28dd Add filename sanitization function and tests to prevent path traversal vulnerabilities 2026-03-13 00:29:13 -04:00
yhirose 4d7c9a788d Release v0.37.2 2026-03-13 00:11:00 -04:00
yhirose 1cd0347ace Refactor parse_port function to accept char pointer and length, improving flexibility and validation 2026-03-13 00:05:13 -04:00
yhirose b3a8af80b9 Add port validation and corresponding tests to prevent overflow and out-of-range values 2026-03-13 00:05:13 -04:00
yhirose 1e97c28e36 Implement request smuggling protection for duplicate Content-Length headers and add corresponding tests 2026-03-13 00:05:13 -04:00
yhirose d279eff4db Fix the proxy test error 2026-03-12 23:15:10 -04:00
yhirose 188035fb6d Add a test for the previous change 2026-03-12 22:57:11 -04:00
yhirose 125272f34b Fix TLS cert verification bypass on proxy redirect introduced in #2165 (#2396) 2026-03-12 21:54:51 -04:00
yhirose 9ced2f614d Fix #2395 2026-03-12 19:18:09 -04:00
yhirose 68fa9bce0f Release v0.37.1 2026-03-09 20:51:37 -04:00
yhirose 1f34c541b0 Add more books 2026-03-09 19:30:18 -04:00
yhirose e41ec36274 Fix handling of malformed Content-Length in open_stream and add tests 2026-03-08 22:30:20 -04:00
yhiroseandClaude Opus 4.6 7489fd3a8b Remove 32-bit limitation (#2388)
* Remove 32-bit limitation

* Fix build problems

* Add 32-bit disclaimer and fix MSVC x86 warnings

- Move 32-bit warning to top of README with strong disclaimer
- Add static_cast<size_t> to fix truncation warnings on 32-bit MSVC

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:07:21 -05:00
yhirose ab3098f18b Update README 2026-03-06 18:50:23 -05:00
yhirose 0d3a3d805c Release v0.37.0 2026-03-06 16:57:33 -05:00
yhirose a058e31a3a Use doc.yml to generate document site 2026-03-05 20:50:22 -05:00
yhirose c87d442aac Remove docs-gen 2026-03-03 23:58:03 -05:00
yhirose de296af3eb Fix problem with alternate file name check 2026-03-03 13:37:28 -05:00
yhirose 41899acece Release v0.36.0 2026-03-02 21:56:38 -05:00
yhirose bb22553087 Fix code format 2026-03-02 21:49:10 -05:00
yhirose 8bffbe3ff2 Merge commit from fork 2026-03-02 21:46:31 -05:00
yhirose 0e4f104141 Fix #2383 (#2385) 2026-03-02 21:09:31 -05:00
yhirose 74807d24a7 Update pages-data.json with new content and structure for cpp-httplib documentation 2026-03-02 17:22:52 -05:00
yhirose 2e124cde02 Enhance documentation and configuration for static site generator
- Update README for clarity and quick start instructions
- Refine default config.toml with hostname and base path
- Adjust index.md files for consistent heading levels
- Simplify CSS for code block styling and remove unnecessary theme switching
- Refactor SiteConfig to derive full base URL from hostname and base path
- Update MarkdownRenderer to remove light theme handling
2026-03-02 00:48:14 -05:00
yhirose fdb589d97e Update dependency 2026-03-01 23:55:52 -05:00
yhirose 843ad379c7 Add search functionality across documentation pages
- Implemented a search button in the header of each documentation page.
- Added a search modal that allows users to input search queries.
- Integrated a JavaScript search feature that fetches and displays results from a new `pages-data.json` file.
- Each documentation page now includes a search overlay for improved navigation.
- Updated the main JavaScript file to handle search logic, including result highlighting and navigation.
- Created a `pages-data.json` file containing metadata for all documentation pages to facilitate search functionality.
2026-03-01 23:37:51 -05:00
yhirose cda9680cdc Update document site 2026-03-01 23:12:45 -05:00
yhirose e906c31a79 Add favicon and update navigation icons across documentation
- Added a favicon link to all tour pages in the Japanese documentation.
- Updated navigation links to include SVG icons for Home and GitHub.
- Changed language button to include an SVG icon for better visual representation.
- Improved theme toggle button to use SVG icons for light and dark modes.
- Refactored the documentation build commands in the justfile for clarity and consistency.
2026-03-01 23:04:38 -05:00
yhirose 2d669c3636 Refactor main function to display help message when no command is provided 2026-03-01 23:04:38 -05:00
yhirose b2d76658fc Add default templates, styles, and scripts for documentation site 2026-03-01 23:04:38 -05:00
Charles von Kalm 7444646627 Removed unused local variable (#2382) 2026-03-01 18:02:57 -05:00
Matheus Gabriel Wernyandyhirose 82a61a6b60 [CMake] New compoments MbedTLS and wolfSSL (#2360)
* [CMake] New component MbedTLS

New component MbedTLS.

* Fix case

Fix case: HTTPLIB_REQUIRE_OPENSSL=OFF; HTTPLIB_REQUIRE_MBEDTLS=ON

* [CMake] Test target MbedTLS::tfpsacrypto

[CMake] Test target MbedTLS::tfpsacrypto.

* [CMake] Test MbedTLS::mbedx509

[CMake] Test MbedTLS::mbedx509.

* Revert "[CMake] Test MbedTLS::mbedx509"

This reverts commit 1d0b91f59a.

* Revert "[CMake] Test target MbedTLS::tfpsacrypto"

This reverts commit bf099f6264.

* Fix problem caused by the recent performance improvement

* wolfSSL support

wolfSSL support.
Partly solve https://github.com/yhirose/cpp-httplib/issues/2371. Only
meson is missing.

* Solve https://github.com/yhirose/cpp-httplib/issues/2361

Solve https://github.com/yhirose/cpp-httplib/issues/2361.
Apply `WARNING`.

* Fix variable

Fix variable.

* [CMake] Solve incompatibilities with loop

Solve incompatibilities with loop.

* Fix

Fix.

* Remove debug prints

Remove debug prints.

* [CMake] Fix bug

Prevent a bug aus the required and if available libraries are checked
independently from each other. A could be chosen in required but B could
be chosen in if available and everything would pass.

* Remove debug print

Remove debug print.

* Restore change

Restore change.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-03-01 18:00:36 -05:00
yhirose 63ede29db1 Update README 2026-03-01 17:34:03 -05:00
yhirose ae64a5ee90 Fix #2381 2026-02-28 22:07:44 -05:00
yhirose f441fc33fb Update README with documentation link and formatting
Added a line break and a link to the official documentation.
2026-02-28 21:58:58 -05:00
yhirose a9fc935919 Add a link to GitHub 2026-02-28 21:25:01 -05:00
yhirose b6cd71d4ff Update docs/ 2026-02-28 20:58:42 -05:00
yhirose bda599bfb4 Fix base_dir for GitHub PageS 2026-02-28 20:56:46 -05:00
yhirose 797758a742 Documentation Site on GitHub Pages (#2376)
* Add initial documentations

* Update documentation for Basic Client and add WebSocket section

* feat: add a static site generator with multi-language support

- Introduced a new Rust-based static site generator in the `docs-gen` directory.
- Implemented core functionality for building sites from markdown files, including:
  - Configuration loading from `config.toml`.
  - Markdown rendering with frontmatter support.
  - Navigation generation based on page structure.
  - Static file copying and output directory management.
- Added templates for base layout, pages, and portal.
- Created a CSS file for styling and a JavaScript file for interactive features like language selection and theme toggling.
- Updated documentation source with new configuration and example pages in English and Japanese.
- Added a `justfile` target for building the documentation site.

* Add language/theme toggle functionality

- Created a new Japanese tour index page at docs/ja/tour/index.html
- Implemented navigation links for various sections of the cpp-httplib tutorial
- Added a language selector to switch between English and Japanese
- Introduced theme toggle functionality to switch between light and dark modes
- Added mobile sidebar toggle for better navigation on smaller screens
2026-02-28 14:45:40 -05:00
yhirose 85b18a9c64 Release v0.35.0 2026-02-27 22:14:27 -05:00
yhirose 0853ce7753 Fix #2379 2026-02-27 22:08:27 -05:00
yhirose c99d7472b5 Merge commit from fork 2026-02-27 21:36:04 -05:00
yhirose defd907c74 Merge commit from fork 2026-02-27 21:35:32 -05:00
yhirose c6c75e4c69 Remove tag-latest.yml 2026-02-23 00:01:58 -05:00
yhirose c2002f6e06 Make loading system certificates from the Keychain on macOS an opt-out feature (#2377) 2026-02-22 19:18:40 -05:00
yhirose 7c33fd47bf Release v0.34.0 2026-02-22 17:27:02 -05:00
yhirose 21243b3c9e Fix problem caused by the recent performance improvement 2026-02-22 13:02:40 -05:00
yhirose e068da4f6b Fix race condittion with logging 2026-02-22 08:46:30 -05:00
yhirose f6aec98145 Fix SocketStream.wait_writable_INET test 2026-02-22 08:39:08 -05:00
yhirose f29bb15f9d Performance improvement! 2026-02-22 07:53:23 -05:00
yhirose c53d93d145 Add make_file_body 2026-02-22 07:53:23 -05:00
yhirose b4d16a582d Update latest tag when new release 2026-02-21 23:27:16 -05:00
yhirose e61a8bcec7 Release v0.33.1 2026-02-21 08:55:06 -05:00
yhirose 33dbe00cce Fix compiple problem with C++11 compiler 2026-02-21 08:52:50 -05:00
yhirose f1f8ff53d5 Release v0.33.0 2026-02-21 08:43:49 -05:00
yhirose 9b9bda6b6e Fix CI build errors 2026-02-21 08:14:47 -05:00
yhirose 2280f1d191 Improvement for Multipart Form Data 2026-02-20 23:15:01 -05:00
yhirose 43cf1822c6 Resolve #2369 2026-02-20 16:22:01 -05:00
yhirose 17f301877f Add bench command in justfile 2026-02-20 15:45:04 -05:00
yhirose 4f17fbaa03 Missing change for wolfSSL support 2026-02-20 15:44:31 -05:00
yhirose 0d5bf55c73 Add wolfSSL support (#2370)
* Add wolfSSL support

* Update CI

* Fix build error

* Revert "Fix build error"

This reverts commit d48096277f.

* Fix build errors

* Build errors on ubuntu

* Update README

* Refactoring

* Fix wolfSSL issues
2026-02-20 15:42:45 -05:00
yhirose 718d7d92b9 Fix problems in Unit tests 2026-02-16 06:22:18 -05:00
yhirose c41c5fb8a9 Revise README features section and header
Updated section headers and improved feature list formatting.
2026-02-14 23:22:42 -05:00
yhirose ab96f72b96 Update README 2026-02-14 20:06:01 -05:00
yhirose b2430249d2 Update README-websocket.md and justfile 2026-02-14 17:59:15 -05:00
yhirose 464867a9ce WebSocket and Dynamic Thread Pool support (#2368)
* WebSocket support

* Validate selected subprotocol in WebSocket handshake

* Fix problem with a Unit test

* Dynamic Thread Pool support

* Fix race condition in new Dynamic ThreadPool
2026-02-14 17:44:49 -05:00
Kostia Sokolovskyi d4180e923f Fix comparison of integers of different signs warning when compiling with BoringSSL. (#2367) 2026-02-13 17:25:59 -05:00
Adrien Gallouët 8d225afc8c Remove macOS select() fallback (#2365)
* Remove macOS select() fallback

macOS has supported `poll` for a long time now, so there's no need for
the specific `select` code paths.

With this commit, we can successfully build on visionOS.

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>

* Fix coding style

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>

---------

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>
2026-02-13 13:44:06 -05:00
yhirose ed5c5d325b Parallel test on CI (#2364)
* Parallel test on CI

* Fix problem with Windows

* Use cache for vcpkg

* Parallel 'No Exception' test

* Use one job to run all shards
2026-02-13 01:55:30 -05:00
yhirose c1ee85d89e Use iptables to disable network (#2363)
* Use iptables to disable network

* Fix race condition problem

* Enable network after test finishes
2026-02-12 22:46:26 -05:00
yhirose 14e37bd75b Offline test (Resolve #2356) (#2358)
* Offline test

* Disabled network

* Removed MbedTLS
2026-02-12 16:31:23 -05:00
yhirose c0adbb4b20 Release v0.32.0 2026-02-12 15:24:12 -05:00
yhirose f80864ca03 Resolve #2359 2026-02-11 14:25:27 -10:00
Adrien Gallouët 4e75a84b39 Fix compilation on BoringSSL by replacing ASN1_TIME_to_tm (#2354)
* Fix compilation on BoringSSL by replacing ASN1_TIME_to_tm

BoringSSL doesn't expose `ASN1_TIME_to_tm`.
This patch switches to using `ASN1_TIME_diff` to calculate `time_t`.
This is supported by OpenSSL, LibreSSL, and BoringSSL, and also avoids
the platform-specific `timegm` vs `_mkgmtime` logic.

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>

* Format code

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>

* Use detail::scope_exit

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>

---------

Signed-off-by: Adrien Gallouët <adrien@gallouet.fr>
2026-02-11 07:55:07 -10:00
Justin 77d945d3b6 Correct sign comparison error with sk_X509_OBJECT_num (#2355)
* Correct sign comparison error with sk_X509_OBJECT_num

In some build configurations, sk_X509_OBJECT_num (from BoringSSL) returns a size_t. Comparing this directly against a signed int loop counter triggers -Werror,-Wsign-compare.

Instead, move the count into a local int variable so the compiler uses implicit conversion to ensure type consistency during the loop comparison.

* Update httplib.h

* Update httplib.h

Missed a s/int/decltype(count)
2026-02-11 07:54:35 -10:00
yhirose f69737a838 Fix problem with PayloadMaxLengthTest.NoContentLengthPayloadLimit 2026-02-10 23:38:57 -10:00
yhirose a188913b02 Merge branch 'master' of github.com:yhirose/cpp-httplib 2026-02-10 23:23:13 -10:00
yhirose 02e4c53685 Fix 'no TLS' problem with RequestWithoutContentLengthOrTransferEncoding 2026-02-10 23:21:43 -10:00
Alexey Sokolov 8c4370247a Add support for mbedtls to meson (#2350)
Related: #2345
2026-02-10 09:51:37 -10:00
yhirose 1f1a799d13 Fix #2351 2026-02-09 16:43:02 -10:00
yhirose a875292153 Move stream and sse implementations from the decl area to the impl area. (#2352) 2026-02-09 16:41:49 -10:00
yhirose f0b7d4161d Update justfile 2026-02-09 15:12:12 -10:00
yhirose 2867b74f13 Release v0.31.0 2026-02-08 16:04:29 -10:00
yhirose 4e14bc8948 Fix memory leak (#2348)
* Fix memory leak

* Fix flaky errors
2026-02-08 15:49:30 -10:00
yhirose 5d717e6d91 Resolve #2347 2026-02-08 09:40:26 -10:00
yhirose 8b4146324f Fix #2116 (#2346)
* Fix #2116

* Fix problem
2026-02-07 19:26:11 -10:00
yhirose 94b5038eb3 Fix build error on Windows 2026-02-06 22:08:28 -10:00
yhirose 9248ce3bfe Fix problem with PayloadMaxLengthZeroMeansNoLimit 2026-02-06 22:02:22 -10:00
yhirose 4639b696ab Fix #2339 (#2344)
* Fix #2339

* Fix CI errors

* Fix Windows build error

* Fix CI errors on Windows

* Fix payload_max_length initialization in BodyReader

* Initialize payload_max_length with CPPHTTPLIB_PAYLOAD_MAX_LENGTH in BodyReader

* Update README and tests to clarify payload_max_length behavior and add no limit case

* Fix server thread lambda capture in ClientVulnerabilityTest
2026-02-06 19:30:33 -10:00
yhirose 5ead179b8e Update README 2026-02-02 11:32:23 -05:00
MikoandAndrea Pappacoda 1942e0ef01 Add C++ modules support (#2291)
* Add C++ modules support

* Add module examples

* Missing semicolon

* Update GitHub Actions script and create a modules updating script

* Name the unused param

* Use the guarded/direct export of header approach

* Update CMakeLists.txt

Co-authored-by: Andrea Pappacoda <andrea@pappacoda.it>

* Update CMakeLists.txt

Co-authored-by: Andrea Pappacoda <andrea@pappacoda.it>

* Split scripts into split.py and generate_module.py

---------

Co-authored-by: Andrea Pappacoda <andrea@pappacoda.it>
2026-02-02 11:27:09 -05:00
yhirose 6be32a540d Abstract TLS API support (Resolve #2309) (#2342)
Abstract TLS API support (OpenSSL and MbedTLS backends)
2026-02-01 23:48:03 -05:00
yhirose dc6faf5c17 Release v0.30.2 2026-02-01 19:11:44 -05:00
yhirose 71bb17fb0e Fix problems with CPPHTTPLIB_NO_EXCEPTIONS 2026-02-01 08:43:13 -05:00
yhirose 094bf112bb Fix #2340 2026-01-30 22:01:43 -05:00
ctabor-itracsandTabor, Chris fbec2a3466 case insensitive hostname validation (fix #2333) (#2337)
* Add Tests for case sensitive hostname verfication

* Modify check_host_name to perform case insensitive comparisons during ssl hostname validation

---------

Co-authored-by: Tabor, Chris <chris.tabor@commscope.com>
2026-01-23 14:58:47 -05:00
Sung Po-HanandYour c3fa06112b Fix set_ca_cert_store() to skip system certs like set_ca_cert_path() (#2335)
Both APIs conceptually do the same thing: "use these CA certs for
verification." However, set_ca_cert_store() falls through to the else
branch in load_certs() where system certs are added to the user's
custom store, defeating the purpose of certificate pinning.

This change makes set_ca_cert_store() behave consistently with
set_ca_cert_path() by checking ca_cert_store_ before loading system
certificates.

Added test to verify system certs are not loaded when custom store is set.

Co-authored-by: Your <you@example.com>
2026-01-22 18:58:25 -05:00
Prajwal B Mehendarkar f73e694f0c timegm api absent in AIX (#2336) 2026-01-22 18:57:23 -05:00
TH 191bfb2ea4 Fix build error when zstd < 1.5.6 lacks zstd::libzstd CMake target (#2334)
Fix #2313
2026-01-22 18:56:34 -05:00
yhirose ad5839f0d1 Add retry logic to BenchmarkTest test on Windows 2026-01-20 18:43:56 -05:00
yhirose 02dfb97fd6 Add Expect: 100-continue support 2026-01-18 22:38:25 -05:00
yhirose a38a076571 Resolve #2262 (#2332)
* Resolve #2262

* Enhance request handling on Windows by adding early response check for large request bodies

* Enhance early response handling for large requests with long URIs on Windows
2026-01-18 00:38:43 -05:00
yhirose 0e1b52b23e Fix #2325 (#2331)
* Fix #2325

* clang-format
2026-01-16 18:19:14 -05:00
yhirose c0469eba96 Revert "Fix #2325"
This reverts commit 7dec57d1eb.
2026-01-16 17:28:28 -05:00
yhirose 7dec57d1eb Fix #2325 2026-01-16 16:25:06 -05:00
yhirose b85aa76bd2 Fix #2321, #2322, #2326 2026-01-16 11:29:09 -05:00
yhirose cea018f2cd Fix #2324 2026-01-11 21:23:15 -05:00
yhirose 1111219f17 Fix #2324 2026-01-10 21:05:30 -05:00
yhirose a7e1d14b15 Fix warning on Windows 2026-01-10 19:23:35 -05:00
yhirose 6eff49e1fb Problem with CI test on Windows without OpenSSL (#2323)
* Fix problem with 'windows without SSL`

* Fix payload limit enforcement for requests without Content-Length on Windows

- Enable MSG_PEEK on Windows (non-SSL builds) to detect payloads without Content-Length
- Only use MSG_PEEK when payload_max_length is set to a finite value to avoid blocking
- Use read_content_without_length for actual size checking to support any payload limit
- Set 413 Payload Too Large status before rejecting oversized requests

This fixes three test cases on Windows:
- RequestWithoutContentLengthOrTransferEncoding (no payload limit)
- NoContentLengthPayloadLimit (8-byte limit)
- NoContentLengthExceeds10MB (10MB limit)

* clang-format
2026-01-10 19:23:24 -05:00
yhirose bd95e67c23 Release v0.30.1 2026-01-09 21:35:03 -05:00
yhiroseandHritik Vijay 2e2e47bab1 Merge commit from fork
* Ensure payload_max_length_ is respected for compressed payloads

* Fix Denial of service (DOS) using zip bomb

---------

Co-authored-by: Hritik Vijay <hey@hritik.sh>
2026-01-09 21:09:07 -05:00
seragh 59905c7f0d Prevent redefinition of ssize_t (#2319)
On Windows cpp-httplib defines ssize_t, therefore applications needing
to define ssize_t for their own needs or are using libraries that do
require a means to avoid a possible incompatible redefinition.

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2026-01-05 20:03:18 -05:00
yhirose 8d03ef1615 Fix #2318 on macOS 2026-01-02 22:28:15 -05:00
yhirose 23a1d79a66 Fix #2318 2026-01-02 20:45:01 -05:00
yhirose 781c55f120 Release v0.30.0 2025-12-31 22:19:05 -05:00
yhirose 40f7985e02 Update copyright year 2025-12-31 22:18:11 -05:00
PerseoGI f85f30a637 Apple frameworks: match CoreFoundation and CFNetwork linkage (#2317)
* Apple frameworks: match linkage with CoreFoundation and CFNetwork with actual code

* Fix appleframeworks in Meson code
2025-12-31 12:49:59 -05:00
yhirose 6da7f0c61c Fix port reuse problem 2025-12-31 00:34:15 -05:00
yhirose 2ba0e7a797 Fix build error 2025-12-30 19:46:21 -05:00
yhirose ded82448aa clang-format 2025-12-30 17:34:52 -05:00
yhirose 98048a033a Merge commit from fork 2025-12-30 17:32:42 -05:00
yhirose 7ae794a6bf Fix #2315 2025-12-26 16:17:43 -05:00
yhirose 385adefb11 Use HOST and PORT in test.cc 2025-12-26 00:24:19 -05:00
yhirose b7c2f04318 Fix potential arithmatic overflow problem 2025-12-25 22:19:37 -05:00
yhirose d23cf77cd0 Resolve #2313 2025-12-23 20:10:59 -05:00
yhirose 5304464a53 Release v0.29.0 2025-12-23 00:03:49 -05:00
yhirose db98efee5a Fix problem with Proxy test 2025-12-22 23:10:24 -05:00
yhirose cdf0d33258 Fix #2301 2025-12-22 22:37:56 -05:00
Aaron Gokaslan 25688258ad Add another missing std::move for _base_dirs vector (#2314) 2025-12-22 20:43:46 -05:00
Aaron Gokaslan f0990ca96d Use std::move for request redirection (#2311)
Prevents an additional copy
2025-12-17 12:04:37 -05:00
Aaron Gokaslan 0461cb770c Avoid unncessary copying of request and response objects (#2310) 2025-12-17 12:04:17 -05:00
yhirose 51b704b902 Implement SSEClient (#2308)
* Implement SSEClient

* Fix Windows problem
2025-12-15 00:00:42 -05:00
yhirose 7eb03e81fc Refactoring 2025-12-14 17:41:11 -05:00
yhirose 6a6d4161d1 Removed DigestAuthTest.FromHTTPWatch_Online_HTTPCan 2025-12-14 14:45:48 -05:00
Copilot 63b07ada43 Initial plan (#2307)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-12-14 13:54:29 -05:00
Jean-Francois Simoneau 2de4c59bc2 Add Zstd support through meson (#2293)
* Add Zstd support through meson

* Add libzstd-dev to abitest
2025-12-13 23:55:58 -05:00
Jean-Francois Simoneau b7097f1386 Replace httpbin.org with httpcan.org (#2300)
* Replace httpbin.org with httpcan.org

* Fix DigestAuthTest.FromHTTPWatch_Online test
2025-12-13 23:52:05 -05:00
Aaron Gokaslan 681d388247 Use move semantics for auth key and value (#2306) 2025-12-13 22:53:10 -05:00
Aaron Gokaslan ae94d64f67 Remove another unnecessary string copy (#2305) 2025-12-13 22:52:26 -05:00
Aaron Gokaslan 3401877d3d Change single char string literals to chars (#2304) 2025-12-13 22:52:12 -05:00
Aaron Gokaslan bce08e62f9 Remove unnecessary copies for AcceptEntry (#2303) 2025-12-13 22:49:58 -05:00
Carter Green f4ecb96e54 Fix linker error on macOS (#2299) 2025-12-11 20:12:31 -05:00
Aaron Gokaslan c23764269d Use std::move for boundary in set_boundary method (#2298) 2025-12-09 22:24:33 -05:00
Aaron Gokaslan f441cd2a44 Use std::move for content_provider in adapter (#2297) 2025-12-09 22:23:45 -05:00
Miko c3613c6977 Update the split.py file (#2295) 2025-12-08 22:18:01 -05:00
yhirose 87c2b4e584 Fix #2294 2025-12-08 19:32:35 -05:00
yhirose c795ad1c32 Fix #2259. Add query string normalization to preserve parameter order in requests 2025-12-05 21:39:40 -05:00
yhiroseandCopilot 3e0fa33559 Implement ETag and Last-Modified support for static file responses and If-Range requests (#2286)
* Fix #2242: Implement ETag and Last-Modified support for static file responses

* Add ETag and Last-Modified handling for If-Range requests

* Enhance HTTP date parsing with improved error handling and locale support

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/test.cc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor ETag handling: separate strong and weak ETag checks for If-Range requests

* Fix type for mtime in FileStat and improve ETag handling comments

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Resolved code review comments

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor ETag handling: use 'auto' for type inference and improve code readability

* Refactor ETag handling: extract check_if_not_modified and check_if_range methods for improved readability and maintainability

* Code cleanup

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/test.cc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enhance ETag handling and validation in httplib.h and add comprehensive tests in test.cc

* Refactor ETag comparison logic and add test for If-None-Match with non-existent file

* Fix #2287

* Code cleanup

* Add tests for extreme date values and negative file modification time in ETag handling

* Update HTTP-date parsing comments to reference RFC 9110

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 18:58:54 -05:00
yhirose 27b73f050e Fix #2287 2025-12-05 08:50:12 -05:00
yhirose dbd5ca4bf2 Add error handling for stream read timeouts and connection closures 2025-12-04 19:19:18 -05:00
yhirose 143019a38c Fix #2284 2025-12-02 17:24:27 -05:00
yhirose 1d36013fc3 Update README 2025-12-02 17:08:53 -05:00
yhirose 8bba34eebc Add New Streaming API support (#2281) 2025-12-01 21:40:13 -05:00
Aaron Gokaslan 0a9102ff6b Optimize ThreadPool and MatcherBase constructors (#2283)
Add a missing reserve and missing std::move to each ctor respectively. The latter should really be caught by a clang-tidy perf linter.
2025-11-30 21:49:23 -05:00
bigmoonbit c1fa5e1710 chore: fix some typos in comments (#2282)
Signed-off-by: bigmoonbit <bigmoonbit@outlook.com>
2025-11-27 20:34:18 -05:00
yhirose 84796738fc Fix #2248 2025-11-25 22:22:07 -05:00
yhirose adf58bf474 Release v0.28.0 2025-11-25 21:06:51 -05:00
yhirose 337fbb0793 Fix #2279
Enhance request handling: add support for requests without Content-Length or Transfer-Encoding headers
2025-11-25 20:30:43 -05:00
Copilotandyhirose 9e7861b0b4 Add #undef _res after including resolv.h to prevent macro conflicts (#2280)
* Initial plan

* Add #undef _res after including resolv.h to prevent macro conflicts

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

* Complete task - added #undef _res after resolv.h include

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

* Remove accidentally committed codeql build artifacts

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

* Add inline comment explaining why #undef _res is necessary

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>
2025-11-25 20:00:10 -05:00
Clarkok ZhangandClarkok Zhang 27ee115a60 Fix android getaddrinfo issue (#2273)
Co-authored-by: Clarkok Zhang <clarkok8@gmail.com>
2025-11-14 16:48:21 -05:00
Clarkok ZhangandClarkok Zhang 59882752aa Add Client::Post with both content provider and receiver (#2268)
Co-authored-by: Clarkok Zhang <clarkok8@gmail.com>
2025-11-14 14:52:06 -05:00
yhirose 61e9f7ce8f Fix #2270 2025-11-14 14:17:04 -05:00
VZ 1acf18876f CMake: Add HTTPLIB_SHARED option, don't define BUILD_SHARED_LIBS (#2266)
To avoid surprises in the projects consuming the library, don't define
BUILD_SHARED_LIBS option ourselves but just use its value, if provided,
to define HTTPLIB_SHARED option which can be also set directly to
specify whether we should build static or shared library.

Closes #2263.
2025-11-10 22:17:43 -05:00
chansikparkandWor Ker 4b2b851dbb Fix HTTP 414 errors hanging until timeout (#2260)
* Fix HTTP 414 errors hanging until timeout

* All errors (status code 400+) close the connection

* 🧹

---------

Co-authored-by: Wor Ker <worker@factory>
2025-11-02 22:23:42 -05:00
yhirose 551f96d4a2 Remove REMOTE_PORT dependency from UnixSocketTest.PeerPid 2025-10-27 20:40:12 -04:00
yhirose eacc1ca98e Release v0.27.0 2025-10-27 19:57:53 -04:00
yhirose ac9ebb0ee3 Merge commit from fork
* Fix "Untrusted HTTP Header Handling (REMOTE*/LOCAL*)"

* Fix "Untrusted HTTP Header Handling (X-Forwarded-For)"

* Fix security problems in docker/main.cc
2025-10-27 19:54:12 -04:00
yhirose 11eed05ce7 Fix #2255 and #2256 2025-10-27 19:51:55 -04:00
yhirose f3bba0646a Fix benchmark test issue on Windows (#2258) 2025-10-27 18:27:55 -04:00
yhirose 2da189f88c Fix EventDispatcher problem (#2257) 2025-10-27 18:10:52 -04:00
yhirose 6e0f211cff Fix problem with .gitignore for examples 2025-10-27 17:54:27 -04:00
yhirose 318a3fe425 Fix problem with installing OpenSSL for Windows (#2254) 2025-10-25 22:35:21 -04:00
yhirose 2d8d524178 Fix #2251 2025-10-25 22:06:42 -04:00
yhirose afa88dbe70 Fix #2250 2025-10-25 21:36:53 -04:00
yhirose 08133b593b Merge branch 'staticlibs-ssl_error_reporting' 2025-10-25 19:32:31 -04:00
yhirose 8aedbf4547 Add a unit test 2025-10-25 19:31:48 -04:00
yhirose cde29362ef Merge branch 'ssl_error_reporting' of github.com:staticlibs/cpp-httplib into staticlibs-ssl_error_reporting 2025-10-25 19:09:09 -04:00
yhirose bae40fcdf2 Resolve #2237 2025-10-25 16:48:45 -04:00
crueter db561f5552 [cmake] FindBrotli: do not add Brotli:: targets if they already exist (#2249)
Not checking for this is terrible practice.
2025-10-16 09:59:15 -04:00
Andrea Pappacoda 35c52c1ab9 build(meson): use C++17 for gtest >= 1.17.0 (#2241) 2025-09-20 15:06:49 -04:00
Alex Kasko 23ff9a5605 Fix error reporting in SSLClient
When the `SSLClient` is used to connect to a plain-HTTP server (which
can happen in clients due to some end-user misconfiguration) it can
return a failure from the `send()` call without setting the `Error`
reference to the corresponding error code. This can cause problems to
callers, that may expect that, when the check like this is passed on
the response:

```c++
if (res.error() == Error::Success)
```

then they can access the response contents with `res.value()`. When
`SSLClient`'s connection fails - the contents `unique_ptr` is not set
and an attemt to access it causes UB.

This change fixes the `SSLClient::create_and_connect_socket` method
making sure that, the `Error` value is set correctly when the
`is_valid()` check fails.
2025-09-18 13:44:39 +01:00
yhirose 41be1e24e3 Code cleanup 2025-09-15 07:59:53 -04:00
Jonas van den Berg 6e52d0a057 Fix UB by use of dangling references in getaddrinfo_with_timeout (#2232)
* Fix use of dangling references

When the resolve thread is detached, local variables were still used, which could lead to a program crash.

* Add test to verify dangling ref fix

* Add missing brace initialization

* Assert that the remaining fields are really zeroed

* Fix use of chrono literals
2025-09-14 20:05:09 -04:00
apocelipes f72b4582e6 Fix: Fix Windows Cross-Compilation (#2234) 2025-09-14 08:05:51 -04:00
yhirose 89c932f313 Release v0.26.0 2025-08-29 16:05:44 -04:00
yhirose eb5a65e0df Fix #2217 2025-08-29 15:01:59 -04:00
kgokalp 7a3b92bbd9 Fix: handle EAI_ALLDONE from gai_suspend in getaddrinfo_with_timeout (#2228) 2025-08-28 11:08:32 -04:00
yhirose eb11032797 Fix platform problem 2025-08-27 00:31:14 -04:00
yhirose 54e75dc8ef Add manual run 2025-08-26 23:34:18 -04:00
yhirose b20b5fdd1f Add 'release-docker' workflow 2025-08-26 23:18:59 -04:00
yhirose f4cc542d4b Fix Dockerfile problem with CMD 2025-08-26 22:17:54 -04:00
yhirose 4285d33992 Fix #2223 (#2224)
* Fix #2223

* Fix build error
2025-08-26 21:42:13 -04:00
yhirose 92b4f53012 clang-format 2025-08-26 20:29:43 -04:00
tejas b8e21eac89 Initialize start time for server (#2220)
* Initialize start time for server

By initializing start_time_ for server, I hope to measure the time taken to process a request at the end maybe in the set_logger callback and print it.

I only see current usage in client with server retaining the inital min value

* Add test to verify start time is initialized

* Address review comments

* run clang format
2025-08-26 15:34:13 -04:00
Sergey 3fae5f1473 osx: fix inconsistent use of the macro TARGET_OS_OSX (#2222)
* osx: fix inconsistent use of the macro `TARGET_OS_OSX`

Fixed the build error on iOS:

```
httplib.h:3583:3: error: unknown type name 'CFStringRef'
  870 |   CFStringRef hostname_ref = CFStringCreateWithCString(
```

Note, `TARGET_OS_OSX` is defined but is 0 when `TARGET_OS_IOS` is 1,
and vise versa. Hence, `TARGET_OS_MAC` should have been used, that is
set to 1 for the both targets.

* improve: non-blocking getaddrinfo() for all mac target variants

`TARGET_OS_MAC` should have been used, that is set to 1 for all other
targets: OSX, IPHONE (IOS, TV, WATCH, VISION, BRIDGE), SIMULATOR,
DRIVERKIT.
2025-08-26 12:46:51 -04:00
Andrea Pappacoda fe7fe15d2e build(meson): fix new build option names (#2208)
This is a follow-up to commit 4ff7a1c858,
which introduced new simplified build options and deprecated the old
ones. I forgot to also change the various get_option() calls,
effectively rendering the new option names useless, as they would not
get honoured.
2025-08-19 15:22:08 -04:00
Thomas Beutlich fbd6ce7a3f Make code sample compilable (#2207) 2025-08-14 06:57:39 -04:00
Thomas Beutlich dffce89514 #2201 Fix 32-bit MSVC compiler error due to unknown command #warning (#2202) 2025-08-12 17:06:09 -04:00
yhirose 3f44c80fd3 Release v0.25.0 2025-08-07 20:58:39 -04:00
yhirose a2bb6f6c1e Update docker/main.cc 2025-08-07 20:57:37 -04:00
Thomas Beutlich 7012e765e1 CMake: Check pointer size at configure time (#2197)
* Check pointer size at configure time

* Relax check to warning
2025-08-07 17:14:19 -04:00
yhirose b1c1fa2dc6 Code cleanup 2025-08-07 00:09:09 -04:00
yhirose fbee136dca Fix #2193. Allow _WIN32 2025-08-06 23:12:33 -04:00
yhirose 70cca55caf Workaround for chocolatey issue with the latest OpenSSL 2025-08-06 17:54:08 -04:00
yhirose cdaed14925 Update README 2025-08-06 17:41:03 -04:00
yhirose b52d7d8411 ErrorLogger support (#870) (#2195) 2025-08-06 17:38:18 -04:00
Thomas Beutlich acf28a362d #2191 Check for minimum required Windows version (#2192) 2025-08-01 20:16:43 -04:00
yhirose 0b3758ec36 Fix problem with Windows version check 2025-07-30 17:39:40 -04:00
yhirose a5d4c143e5 Release v0.24.0 2025-07-29 19:47:48 -04:00
yhirose c0c36f021d Fix #2184, #2185 (#2190)
* Fix #2184, #2185

* Fix build error

* Update

* Update
2025-07-29 19:29:37 -04:00
yhirose 8e8a23e3d2 Fix #2187 2025-07-24 19:35:47 -04:00
yhirose 890a2dd85d Fix #2189 2025-07-24 17:04:59 -04:00
yhirose ca5fe354fb Release v0.23.1 2025-07-16 17:59:52 -04:00
Benjamin Gilbert dd98d2a24d build(meson): warn/fail on 32-bit machines (#2181)
On 32-bit Windows, meson setup fails with an unclear error:

    meson.build:25:16: ERROR: Could not get define 'CPPHTTPLIB_VERSION'

The actual problem is that httplib.h #errors out.

Have the Meson logic explicitly check for a 32-bit host and warn or error,
matching the check in httplib.h.  Phrase the Windows error in a way that
triggers WrapDB CI's unsupported architecture check.
2025-07-16 12:47:51 -04:00
yhirose 1f110b54d8 Chang #error to #warning for the 32-bit environment check except 32-bit Windows 2025-07-11 22:44:29 -04:00
yhirose 7b6867bcdf Fix #2021 (#2180) 2025-07-10 22:01:41 -04:00
yhirose 8b28577ec6 Resolve #366 2025-07-10 01:07:44 -04:00
yhirose 55b38907dc Resolve #1264 2025-07-10 00:58:52 -04:00
yhirose 53ea9e8bb4 Fix #2111 (#2179) 2025-07-10 00:47:45 -04:00
yhirose ecfd84c171 Release v0.23.0 2025-07-09 23:57:47 -04:00
yhirose b5b2a1d1c8 Change uint64_t to size_t 2025-07-09 18:11:38 -04:00
yhirose 17ba303889 Merge commit from fork
* Fix HTTP Header Smuggling due to insecure trailers merge

* Improve performance
2025-07-09 07:10:09 -04:00
yhirose 802743264c Remove incorrect comment 2025-07-08 23:53:52 -04:00
yhirose 9dbaed75ef Fix #2175 (#2177)
* Fix #2175

* Update
2025-07-08 23:04:34 -04:00
yhirose c551e97297 Add .pre-commit-config.yaml 2025-07-08 21:46:03 -04:00
Andrea Pappacoda 4ff7a1c858 build(meson): simplify build options (#2176)
The "cpp-httplib_" prefix of build options is now dropped, as Meson
build options are already namespaced for each project. The old names
remain as deprecated aliases for the new ones.
2025-07-08 17:23:46 -04:00
yhirose 082acacd45 Merge commit from fork
* Fix Persistency of Unbounded Memory Allocation in Chunked/No-Length Requests Vulnerability

* Revert HTTP status code from 413 to 400
2025-07-08 17:11:13 -04:00
yhirose 52163ed982 Fix #2148 (#2173)
* Fix #2148

* Removed 32bit environment

* buld-error-check-on-32bit

* Use 32bit depedency from Windows
2025-07-07 21:30:08 -04:00
Andrea Pappacoda af7a69bcf6 build(meson): add non_blocking_getaddrinfo option (#2174)
This new option automatically enables the new non-blocking name
resolution when the appropriate libraries are found, automatically
adding them to the list of required dependencies. It will gracefully
fall back to the old behaviour when no library is found.

This complements commit ea850cbfa7.
2025-07-07 21:20:29 -04:00
yhirose 145fc8b021 Proxy test (#2172)
* Add proxy test on CI

* Add Brotli and Zstd dev packages to proxy test workflow

* Fix Docker Compose command for GitHub Actions compatibility

* Add proxy readiness check and netcat dependency

* Use netcat-openbsd instead of virtual netcat package

* Add proxy startup delay and debug logging
2025-07-06 22:00:41 -04:00
yhirose af73377611 Fix #1578 (#2171)
* Fix #1578

* Update README

* Update

* Update

* Update

* Update

* Update

* Update
2025-07-06 21:27:24 -04:00
yhirose a3f5569196 Fix #2082 (#2170) 2025-07-05 20:30:31 -04:00
yhirose a636a094bf Fix #1656 2025-07-05 20:22:57 -04:00
yhirose cb85e573de Fix #1416 (#2169)
* Fix #1416

* Update

* Update
2025-07-05 15:17:53 -04:00
yhirose 120405beac clang-format 2025-07-05 07:13:13 -04:00
KTGH ceff2c1154 Add non-blocking getaddrinfo option to Cmake (#2168)
Adds Cmake option HTTPLIB_USE_NON_BLOCKING_GETADDRINFO default on.
Also adds the HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO

Ref #1601, #2167, and https://github.com/yhirose/cpp-httplib/issues/1601#issuecomment-3021357070
2025-07-05 07:07:59 -04:00
yhirose 0c08c378d7 Simplify benchmark 2025-07-05 00:11:59 -04:00
yhirose 9a0571513e Fix Makefile 2025-07-04 22:30:33 -04:00
yhirose cfb56c0b78 Fix #1818 2025-07-04 21:09:05 -04:00
yhirose 083fe43ad3 Remove httpwatch.com dependency 2025-07-04 20:32:48 -04:00
yhirose 9e36247665 clang-format 2025-07-04 20:31:31 -04:00
yhirose fd03483237 Fix warnings 2025-07-04 18:15:39 -04:00
yhirose d5409ab541 Fix warnings 2025-07-03 19:58:28 -04:00
yhirose 0b875e0747 Remove unnecessary parameters 2025-06-30 21:32:29 -04:00
yhirose 292f9a6c55 Add CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO 2025-06-30 21:14:58 -04:00
yhirose 7c303bb871 Launch proxy server before proxy test 2025-06-29 00:17:19 -04:00
yhirose ea850cbfa7 Fix #1601 (#2167)
* Fix #1601

* clang-format

* Fix Windows problem

* Use GetAddrInfoEx on Windows

* Fix Windows problem

* Add getaddrinfo_a

* clang-format

* Adjust Benchmark Test

* Test

* Fix Bench test

* Fix build error

* Fix build error

* Fix Makefile

* Fix build error

* Fix buid error
2025-06-29 00:13:09 -04:00
yhirose e6ff3d7ac2 Cleaner API (#2166)
* Cleaner API

* Fix Windows build error
2025-06-28 00:37:01 -04:00
yhirose e1ab5a604b Proxy problems (#2165)
* Fix proxy problems

* Auto redirect problem (http → https → https)
2025-06-28 00:14:01 -04:00
yhirose 696c02f2bc Update README 2025-06-26 23:52:03 -04:00
yhirose a183dba9fc clang-format 2025-06-25 17:08:00 -04:00
yhirose d37373a983 Performance investigation 2025-06-25 16:46:49 -04:00
yhirose b2bf172393 Fix #1551 2025-06-24 19:40:20 -04:00
yhirose 1729aa8c1f Issue 2162 (#2163)
* Resolve #2162

* Update
2025-06-24 17:37:30 -04:00
yhirose aabd0634ae Fix warnings created by #2152 2025-06-24 17:12:12 -04:00
herbrechtsmeier de5a255ac6 Fix bad request for multipart form data with boundary split (#2159)
* Add test for multipart form data with boundary split

Add a test for multipart form data requests with a large header which
leads to a split inside the boundary because of the read buffer size
inside the SocketStream class.

* Fix bad request for multipart form data with boundary split

Fix a bad request (400) response for multipart form data requests with
a large header which leads to a split inside the boundary because of the
read buffer size inside the SocketStream class. The parse function
inside the MultipartFormDataParser wrongly erase the receive buffer if
it doesn't find the boundary inside the buffer during first call.
2025-06-24 12:57:32 -04:00
yhirose b6c55c6030 Release v0.22.0 2025-06-24 08:01:53 -04:00
yhirose 28dcf379e8 Merge commit from fork 2025-06-24 07:56:00 -04:00
yhirose 91e79e9a63 Fix #1777 (#2160)
* Add benchmark unit test

* Update

* Update

* Update

* Change the default value of CPPHTTPLIB_IDLE_INTERVAL_USECOND to 1ms
2025-06-24 07:44:10 -04:00
yhirose 27879c4874 Fix #2157 (#2158)
* Fix #2157

* Fix Windows build error: wrap std::max in parentheses to avoid macro conflict

- On Windows, max/min are often defined as macros by windows.h
- This causes compilation errors with std::max/std::min
- Solution: use (std::max) to prevent macro expansion
- Fixes CI build error: error C2589: '(': illegal token on right side of '::'

Fixes: error in coalesce_ranges function on line 5376
2025-06-23 08:35:56 -04:00
DSiekmeier 08a0452fb2 Update README.md (#2156)
According to the curl manpage, the option is called "--max-time".
2025-06-18 07:05:23 -04:00
yhirose 3a1f379e75 Release v0.21.0 2025-06-09 22:19:30 -04:00
Marek Kwasecki fd8da4d8e4 Feature/multipart headers (#2152)
* Adds headers to multipart form data

Adds a `headers` field to the `MultipartFormData` struct.

Populates this field by parsing headers from the multipart form data.
This allows access to specific headers associated with each form data part.

* Adds multipart header access test

Verifies the correct retrieval of headers from multipart form data file parts.

Ensures that custom and content-related headers are accessible and parsed as expected.

* Enables automatic test discovery with GoogleTest

Uses `gtest_discover_tests` to automatically find and run tests, simplifying test maintenance and improving discoverability.

* Removes explicit GoogleTest include

* Refactors header parsing logic

Improves header parsing by using a dedicated parsing function,
resulting in cleaner and more robust code.

This change also adds error handling during header parsing,
returning an error and marking the request as invalid
if parsing fails.

* clang-format corrected

* Renames variable for better readability.

Renames the `customHeader` variable to `custom_header` for improved code readability and consistency.

* typo
2025-06-09 15:59:25 -04:00
yhirose 365cbe37fa Fix #2101 2025-05-25 21:56:28 -04:00
Piotr 4a7aae5469 Detect if afunix.h exists (#2145)
Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-05-23 23:14:48 -04:00
yhirose fd324e1412 Add KeepAliveTest.MaxCount 2025-05-20 21:41:50 +09:00
QWERTIOX 366eda72dc Specify version in meson.build (#2139)
I prefer to specify version when I'm calling dependency() and lack of makes me unable to do it
2025-05-16 10:02:32 -04:00
yhirose c216dc94d2 Code cleanup 2025-05-09 18:45:31 +09:00
yhirose 61893a00a4 Fix #2135 2025-05-03 22:50:47 +09:00
yhirose 3af7f2c161 Release v0.20.1 2025-05-03 21:24:22 +09:00
yhirose a0de42ebc4 clang-format 2025-05-03 17:40:34 +09:00
Ville Vesilehto 7b752106ac Merge commit from fork
* fix(parser): Limit line length in getline

Prevents potential infinite loop and memory exhaustion in
stream_line_reader::getline by enforcing max line length.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>

* fix: increase default max line length to 32k

LONG_QUERY_VALUE test is set at 25k.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>

* test(client): expect read error with too long query

Adds a test case (`TooLongQueryValue`) to verify client behavior
when the request URI is excessively long, exceeding
`CPPHTTPLIB_MAX_LINE_LENGTH`. In this scenario, the server is
expected to reset the connection.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>

---------

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2025-05-03 04:39:01 -04:00
yhirose 9589519d58 Fix #2130 2025-04-17 11:52:22 -04:00
Alexey Sokolov caf7c55785 Fix regression of #2121 (#2126) 2025-04-08 16:08:41 -04:00
yhirose 9e4aed482e Fix style error 2025-04-06 09:02:25 -04:00
yhirose 65d6316d65 Fix #2113 2025-04-05 22:40:08 -04:00
yhirose 3e3a8cc02f Made the max timeout threshold for SSL longer. 2025-04-05 22:38:50 -04:00
KTGH b7e33b08f1 Add missing component comment (#2124)
Fix #2123
2025-03-31 20:34:28 -04:00
Alexey Sokolov 0dbe8ba144 Support zstd also via pkg-config (#2121)
* Support zstd also via pkg-config

It doesn't always provide cmake config

* Find zstd with pkg-config also in non-required case

Code by @sum01, slightly modified
2025-03-29 11:46:22 -04:00
Piotr dbc4af819a Fix compilation error on windows (#2118)
afunix.h uses types declared in winsock2.h, and so has to be included
after it. Including afunix.h first will result in a somewhat unhelpful
compilation error:

error C3646: 'sun_family': unknown override specifier

Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-03-25 08:36:20 -04:00
yhirose 7dbf5471ce Fix the style error and comment 2025-03-24 19:16:48 -04:00
Piotr 72b35befb2 Add AF_UNIX support on windows (#2115)
Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-03-24 19:14:24 -04:00
Jean-Francois Simoneau 65ce51aed7 Fix start_time shadow variable (#2114) 2025-03-18 19:17:47 -04:00
yhirose 787a34ad7f Release v0.20.0 2025-03-16 21:24:53 -04:00
yhirose 7a212cfe40 clang-format 2025-03-16 21:22:53 -04:00
Alexandre Bouvier 0be0526085 cmake: only validate component when the required library is found (#2112) 2025-03-16 21:05:55 -04:00
yhirose 87a5ae64a4 Fix #2097 2025-03-16 20:57:17 -04:00
yhirose 33acccb346 Fix #2109 2025-03-16 20:36:15 -04:00
davidalo c765584e6b Add zstd support (#2088)
* Add zstd support

* Add zstd to CI tests

* Use use zstd cmake target instead of ZSTD. Use cmake variable for found packages

* Add missing comment for HTTPLIB_REQUIRE_ZSTD

* Fix test.yaml rebase error

* Use zstd::libzstd target

* Add include and library paths to ZSTD args

* Run clang-format

* Add zstd to httplibConfig.cmake.in
2025-03-16 15:51:53 -04:00
yhirose 0bda3a7d1a Update benchmark 2025-03-13 21:54:05 -04:00
Florian Albrechtskirchinger 2eaa2ea64f Make random_string() thread-safe (#2110)
By making the random engine thread_local, each thread now has its own
independent random sequence, ensuring safe concurrent access.
Additionally, using an immediately invoked lambda expression to
initialize the engine eliminates the need for separate static seed
variables.
2025-03-13 12:44:43 -04:00
Florian Albrechtskirchinger 94a4028821 Update vendored gtest to 1.12.1 (#2100)
Update googletest to the last version supporting C++11.
2025-03-12 12:16:27 -04:00
Florian Albrechtskirchinger a8d6172250 Avoid static std::string (#2103)
Replace static std::string objects with constexpr character arrays and
use compile-time string length calculations.
2025-03-12 12:12:54 -04:00
Florian Albrechtskirchinger 2f39723d08 Wrap poll()/WSAPoll() in a function and build compiled library on Windows (#2107)
* Wrap poll()/WSAPoll() in a function

Instead of using a macro for poll() on Windows, which breaks when the
implementation is compiled separately, add a detail::poll_wrapper()
function that dispatches to either ::poll() or ::WSAPoll().

* Build compiled library on Windows
2025-03-12 12:12:03 -04:00
Florian Albrechtskirchinger a9ba0a4dff Remove SSLInit (#2102)
Quote: "As of version 1.1.0 OpenSSL will automatically allocate all
resources that it needs so no explicit initialisation is required."
2025-03-12 12:10:02 -04:00
Andrea Pappacoda 37399af996 build(meson): copy MountTest.MultibytesPathName files (#2098)
Fixes the test suite in Meson.
2025-03-11 13:10:30 -04:00
yhirose 48084d55f2 Fix #2096 2025-03-10 23:31:51 -04:00
Florian Albrechtskirchinger 5a1ecc3958 Run 32-bit compiled unit tests on Ubuntu (#2095) 2025-03-06 21:17:41 -05:00
Florian Albrechtskirchinger 85b5cdd78d Move detail::read_file() to test/test.cc (#2092)
The unit test code is the only user of the function.

read_file() now throws an exception if the file isn't found.
2025-03-06 11:58:55 -05:00
Florian Albrechtskirchinger f2928d7152 Switch redirect tests to httpbingo.org (#2090)
Redirect tests fail using httpbin.org or nghttp2.org/httpbin. The
location header value contains a string representation of a Python byte
string (e.g., b'http://www.google.com/'), which results in a 404 error.
2025-03-06 11:55:11 -05:00
Florian Albrechtskirchinger ee0bee3907 Fix HttpWatch tests (#2089)
HttpWatch changed the formatting of the returned JSON. Normalize it by
removing all whitespace.
2025-03-06 07:17:05 -05:00
yhirose 71ba7e7b1b Fix #2068 (#2080)
* Fix #2068

* Add unit test
2025-02-20 23:45:21 -05:00
Florian Albrechtskirchinger ebe7efa1cc Parallelize testing with/without SSL on Windows & set concurrency group (#2079)
* Parallelize testing with/without SSL on Windows

* Set concurrency group in workflows
2025-02-20 20:57:18 -05:00
Florian Albrechtskirchinger 22d90c29b4 Remove select() and use poll() (#2078)
* Revert "Fix typo in meson.build (#2070)"

This reverts commit 5c0135fa5d.

* Revert "build(meson): automatically use poll or select as needed (#2067)"

This reverts commit 2b5d1eea8d.

* Revert "Make poll() the default (#2065)"

This reverts commit 6e73a63153.

* Remove select() and use poll()
2025-02-20 18:51:35 -05:00
Florian Albrechtskirchinger b944f942ee Correct default thread pool size in README.md (#2077) 2025-02-20 12:59:38 -05:00
Florian Albrechtskirchinger 550f728165 Refactor streams: rename is_* to wait_* for clarity (#2069)
- Replace is_readable() with wait_readable() and is_writable() with
  wait_writable() in the Stream interface.
- Implement a new is_readable() function with semantics that more
  closely reflect its name. It returns immediately whether data is
  available for reading, without waiting.
- Update call sites of is_writable(), removing redundant checks.
2025-02-20 12:56:39 -05:00
yhirose a4b2c61a65 Max timeout test refactoring (#2071)
* Simplify code

* Adjust threshold
2025-02-19 22:19:02 -05:00
Florian Albrechtskirchinger 5c0135fa5d Fix typo in meson.build (#2070) 2025-02-19 16:20:44 -05:00
Andrea Pappacoda 2b5d1eea8d build(meson): automatically use poll or select as needed (#2067)
Follow-up to 6e73a63153
2025-02-19 12:47:56 -05:00
yhirose d274c0abe5 Fix typo 2025-02-18 21:33:32 -05:00
yhirose dda2e007a0 Fixed documentation about Unix Domain Sockt (#2066) 2025-02-18 11:40:50 -05:00
yhirose 321a86d9f2 Add *.dSYM to Makefile clean 2025-02-18 05:56:22 -05:00
yhirose ada97046a2 Fix misspelled words 2025-02-18 05:54:22 -05:00
Florian Albrechtskirchinger 6e73a63153 Make poll() the default (#2065)
* Make poll() the default

select() can still be enabled by defining CPPHTTPLIB_USE_SELECT.

* Run tests with select() and poll()
2025-02-18 05:23:23 -05:00
Uros GaberandUrosG cdc223019a server_certificate_verifier extended to reuse built-in verifier (#2064)
* server_certificate_verifier extended to reuse built-in verifier

* code cleanup and SSLVerifierResponse enum clarification as per @falbrechtskirchinger comment

* cleanup

* clang-format

* change local var verification_status_ declaration to auto

* change local var verification_status_ to verification_status

* clang-format

* clang-format

---------

Co-authored-by: UrosG <uros@ub330.net>
2025-02-17 17:24:41 -05:00
Florian Albrechtskirchinger 574f5ce93e Add style check to workflow (#2062)
* Add style check to workflow

* Add example files to style check
2025-02-17 12:14:53 -05:00
Florian Albrechtskirchinger 2996cecee0 Fix code inconsistently formatted and re-format (#2063)
* Fix code inconsistently formatted by clang-format

* Run clang-format
2025-02-17 12:14:02 -05:00
Florian Albrechtskirchinger 32bf5c9c09 Simplify SSL shutdown (#2059) 2025-02-16 17:38:41 -05:00
Florian Albrechtskirchinger 735e5930eb Detect additional CMake build failures (#2058)
Add include_httplib.cc to the main test executable (already done in
Makefile), and add include_windows_h.cc to the main test executable on
Windows to test if including windows.h conflicts with httplib.h.
2025-02-16 15:45:28 -05:00
Florian Albrechtskirchinger 748f47b377 Add workflow_dispatch with Google Test filter and OS selection (#2056)
* Add workflow_dispatch with Google Test filter

Add the workflow_dispatch trigger to the test.yaml workflow. Includes an
input for an optional Google Test filter pattern.

* Add OS selection to workflow_dispatch

* Fix wording
2025-02-16 12:34:28 -05:00
Florian Albrechtskirchinger 4cb8ff9f90 Print timeout exceedance in MaxTimeoutTest (#2060) 2025-02-16 08:43:54 -05:00
Florian Albrechtskirchinger 985cd9f6a2 Fix compilation failures with include <windows.h> (#2057) 2025-02-16 08:39:29 -05:00
Florian Albrechtskirchinger 233f0fb1b8 Refactor setting socket options (#2053)
Add detail::set_socket_opt() and detail::set_socket_opt_time() to avoid
repetition of platform-specific code.
2025-02-14 22:40:24 -05:00
yhirose 03cf43ebaa Release v0.19.0 2025-02-14 14:42:29 -05:00
yhirose 3c4b96024f Don't run CI twice (on push AND pull request) 2025-02-14 14:19:54 -05:00
yhirose d74e4a7c9c Removed incomplete API compatibility check scripts. 2025-02-14 14:10:06 -05:00
Andrea Pappacoda bfa2f735f2 ci: add abidiff workflow (#2054)
This CI workflow checks ABI compatibility between the pushed commit and
the latest tagged release, helping preventing accidental ABI breaks.

Helps with https://github.com/yhirose/cpp-httplib/issues/2043
2025-02-14 14:06:35 -05:00
yhirose b6ab8435d7 Improve ABI check tool on macOS 2025-02-12 12:49:20 -05:00
yhirose 39a64fb4e7 Fix ABI compatibility tool on macOS 2025-02-11 18:40:39 -05:00
yhirose d7c14b6f3a Add API compatibility check tool 2025-02-11 17:49:33 -05:00
yhirose 1880693aef Dropped Visual Studio 2015 support 2025-02-11 11:22:46 -05:00
Florian Albrechtskirchinger dd20342825 Don't run CI twice (on push AND pull request) (#2049) 2025-02-11 06:55:13 -05:00
Brett Profitt a268d65c4f Fix check for URI length to prevent incorrect HTTP 414 errors (#2046) 2025-02-10 21:46:38 -05:00
Florian Albrechtskirchinger b397c768e4 Unify select_read() and select_write() (#2047) 2025-02-10 18:15:19 -05:00
yhirose 8e22a7676a Remome 'global timeout' to 'max timeout' 2025-02-10 18:07:30 -05:00
yhirose 8a7c536ad5 Fix #2034 (#2048)
* Fix #2034

* Fix build error

* Adjust threshold

* Add temporary debug prints

* Adjust threshhold

* Another threshold adjustment for macOS on GitHub Actions CI...

* Performance improvement by avoiding unnecessary chrono access

* More performance improvement to avoid unnecessary chrono access
2025-02-10 06:51:07 -05:00
yhirose 8aad481c69 Fix test.yaml problem 2025-02-08 23:37:41 -05:00
yhirose 5814e121df Release v0.18.7 2025-02-08 15:53:35 -05:00
Florian Albrechtskirchinger 7adbccbaf7 Refine when content is expected (#2044)
Consider Content-Length and Transfer-Encoding headers when determining
whether to expect content. Don't handle the HTTP/2 connection preface
pseudo-method PRI.

Fixes #2028.
2025-02-08 15:51:52 -05:00
yhirose eb10c22db1 Add unit test for #609 2025-02-08 10:17:09 -05:00
yhirose 708f860e3a Fix #2042 2025-02-06 05:56:31 -05:00
yhirose eb30f15363 Release v0.18.6 2025-02-05 19:14:20 -05:00
yhirose 4941d5b56b Fix #2033 (#2039) 2025-02-05 12:46:33 -05:00
Florian Albrechtskirchinger 9bbb4741b4 Run clang-format (#2037) 2025-02-02 22:32:33 -05:00
yhirose 282f2feb77 Add a unit test 2025-02-01 22:11:15 -05:00
alex-cornford 60a1f00618 Support building httplib.h on OpenVMS x86 systems (#2031)
Modify for OpenVMS x86 C++. Make tests on OpenVMS currently not supported due to no cmake support.
Changes tested on OpenVMS clang C++ and Fedora & GCC
2025-01-28 18:44:22 -05:00
yhirose 9104054ca5 Fix README example 2025-01-27 13:37:16 -05:00
Baiyies d69f144a99 Update httplib.h (#2030)
fix 'max'
2025-01-26 08:50:10 -05:00
yhirose 929dfbd348 Update copyright year 2025-01-20 00:32:10 -05:00
yhirose 3047183fd9 Update README 2025-01-20 00:02:02 -05:00
yhirose ef5e4044f1 Update README 2025-01-19 23:46:12 -05:00
yhirose 3779800322 Release v0.18.5 2025-01-17 17:38:03 -05:00
yhirose 986a20fb7d Resolve #2017 (#2022)
* Resolve #2017

* Fix warning

* Update README
2025-01-17 17:37:07 -05:00
yhirose 8311e1105f Fix Windows build problem 2025-01-16 23:26:04 -05:00
yhirose ba6845925d Fix #2014 2025-01-16 23:10:58 -05:00
yhirose 343a0fc073 Fix #2011 2025-01-16 21:38:45 -05:00
yhirose 54f8a4d0f3 Release v0.18.4 2025-01-16 01:00:25 -05:00
yhirose 9c36aae4b7 Fix HTTP Response Splitting Vulnerability 2025-01-16 00:04:33 -05:00
yhirose b766025a83 clangformat 2025-01-16 00:03:10 -05:00
yhirose 9b5f76f833 Fix #2012 2024-12-27 17:19:23 -05:00
sinnrenandfenlog d647f484a4 fix:set_file_content with range request return 416. (#2010)
Co-authored-by: fenlog <bakurise@qq.com>
2024-12-24 09:38:59 -05:00
Sergey Bobrenok 8794792baa Treat out-of-range last_pos as the end of the content (#2009)
RFC-9110 '14.1.2. Byte Ranges':
A client can limit the number of bytes requested without knowing the
size of the selected representation. If the last-pos value is absent,
or if the value is greater than or equal to the current length of the
representation data, the byte range is interpreted as the remainder of
the representation (i.e., the server replaces the value of last-pos
with a value that is one less than the current length of the selected
representation).

https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6
2024-12-23 13:14:36 -05:00
yhirose b85768c1f3 Fix #2005 2024-12-16 17:43:50 -05:00
yhirose e6d71bd702 Add a unit test for Issue #2004 2024-12-12 18:15:22 -05:00
yhirose 258992a160 Changed to use non-blocking socket in is_ssl_peer_could_be_closed 2024-12-03 19:26:08 -05:00
yhirose a7bc00e330 Release v0.18.3 2024-12-03 06:33:00 -05:00
yhirose 11a40584e9 Fix #1998 2024-12-03 00:38:20 -05:00
yhirose 3e86bdb4d8 Fix #1997 (#2001) 2024-12-03 00:11:29 -05:00
Pavel P c817d65695 Fix casting uint64_t to size_t for 32-bit builds (#1999) 2024-12-02 11:09:52 -05:00
yhirose 51dee793fe Release v0.18.2 2024-11-29 20:49:50 -05:00
yhirose 457fc4306e Fix #1993 2024-11-29 20:46:48 -05:00
yhirose 4f5b003e76 Fix #1992 2024-11-28 20:40:38 -05:00
yhirose 5421e27106 Fix a compiler warning 2024-11-28 20:39:26 -05:00
yhirose fe07660f40 Fix #1986 (#1988) 2024-11-27 12:18:35 -05:00
yhirose da2f9e476e Fix #1985 (#1989) 2024-11-27 12:18:23 -05:00
sebastianas 1a7a7ed1c3 test: Don't check for the exact size of compressed content. (#1984)
The testsuite checks for the exact size of the compressed content. The
exact size can change if the zlib library is using a different strategy.
In thise case using zlib-ng results in a slightly larger content leading
to a failure in the test.

Check that the compressed content is less than 10MiB which is a tenth of
the orignal content and proves that compression works.

Signed-off-by: Sebastian Andrzej Siewior <sebastian@breakpoint.cc>
2024-11-25 15:46:41 -05:00
yhirose 413994912d Update vcxproj files 2024-11-16 11:14:13 -05:00
yhirose 01dcf1d0ad Fix #1969 (without unnecessary sleep_for) (#1982) 2024-11-16 10:56:57 -05:00
yhirose 8e378779c2 Update README 2024-11-16 09:45:04 -05:00
yhirose 970b52897c Fix #1980
Fix #1980
2024-11-16 02:09:52 -05:00
yhirose 412ba04d19 Fix problem caused by #1975 2024-11-14 20:33:08 -05:00
yhirose bfef4b3e9b Fix #1975 2024-11-14 17:27:28 -05:00
yhirose 7bd316f3d0 Fix #1977 2024-11-14 16:46:27 -05:00
yhirose 26208363ee Fix warning 2024-11-14 16:46:09 -05:00
yhirose b1b4bb8850 clangformat 2024-11-13 22:50:03 -05:00
yhirose 9dd565b6e3 Resolve #1973 (#1976)
* Fix #1973

* Fixed problems with 'Language for non-Unicode programs' setting on Windows

* Fix problems on English locale
2024-11-13 22:47:09 -05:00
yhirose 924f214303 Added unit test for exception handler 2024-11-02 07:23:44 -04:00
yhirose 5c1a34e766 Release v0.18.1 2024-10-18 17:16:54 -04:00
yhirose fa90d06dd5 Merge branch 'master' of github.com:yhirose/cpp-httplib 2024-10-18 17:16:14 -04:00
Jiwoo Park d869054318 Allow empty header values (#1965) 2024-10-18 10:16:48 -04:00
Peter Boström 0cc1ca9a8d Remove extra semicolon (#1963)
This fixes a -Wc++98-compat-extra-semi instance.
2024-10-15 00:09:04 -04:00
yhirose 3701195033 Merge branch 'master' of github.com:yhirose/cpp-httplib 2024-10-14 21:32:28 -04:00
Peter Boström f884a56258 Remove space between operator"" and _t (#1962)
This should fix a -Wdeprecated-literal-operator instance since this is
deprecated as a result of CWG2521 (iiuc C++23).
2024-10-14 21:32:13 -04:00
yhirose d79633ff52 clangformat 2024-10-11 14:49:46 -04:00
yhirose e0ebc431dc Fix #1959 2024-10-11 13:43:01 -04:00
Andrew McDaniel 131bc6c674 Add documentation for using Unix domain sockets. (#1954)
* Add documentation for using unix domain sockets.

* Formatting
2024-10-03 10:53:25 -04:00
yhirose 10d68cff50 Added a unit test for #1946 2024-09-26 22:24:44 -04:00
zjyhjqs 996acc5253 Feat: add CPack support (#1950) 2024-09-25 00:19:41 -04:00
yhirose 7c4799d0cf Fix #1798 for CMake (#1944) 2024-09-19 18:33:32 -04:00
yhirose c239087332 Fix Mafile errors 2024-09-17 18:37:44 -04:00
Andrea Pappacoda 7018e9263d test(meson): copy files in www directory (#1941)
These files were added in commits
2d01e71286 and
b8315278cb
2024-09-17 18:06:01 -04:00
yhirose 4990b4b4b7 Fix problems with SSLSlientServerTest.* tests 2024-09-17 17:00:17 -04:00
Andrea Pappacoda 5064373c23 test(meson): fix SSLClientServerTest.* tests with OpenSSL 3.2.0 (#1940)
* build(meson): bump minimum version to 0.62.0

This allows making some minor cleanups

* test(meson): fix SSLClientServerTest.* tests with OpenSSL 3.2.0

Since OpenSSL commit
<https://github.com/openssl/openssl/commit/342e3652c791bdb06e08abcc169b4456c83ccd00>,
the default X.509 certificate format generated with the `openssl req`
command has been changed to X.509 v3 from X.509 v1.

For some reason, this change breaks cpp-httplib's SSLClientServerTest.*
tests.

To fix the test failures, this patch passes the '-x509v1' flag instead
of '-x509' when OpenSSL 3.2.0 or newer is detected. To detect the
version of a command line utility, Meson 0.62.0 or later is required.

Fixes <https://github.com/yhirose/cpp-httplib/issues/1798>, but only for
the Meson build system.
2024-09-17 16:58:09 -04:00
yhirose 6c93aea59a Revert "Enabled HostnameToIPConversionTest.YouTube_Online partially"
This reverts commit 6553cdedab.
2024-09-15 01:18:20 -04:00
yhirose 6553cdedab Enabled HostnameToIPConversionTest.YouTube_Online partially 2024-09-13 20:45:59 -04:00
yhirose a61b2427b0 Update benchmark base vertion to 0.18.0 2024-09-13 20:34:30 -04:00
yhirose af4ece3d5f Update benchmark/Makefile 2024-09-12 20:41:56 -04:00
yhirose e64379c3d7 Release v0.18.0 2024-09-12 12:28:35 -04:00
yhirose 5053912534 Updated actions/upload-artifact from v1 to v4 2024-09-12 12:04:25 -04:00
Jean-Francois Simoneau 932b1cbc32 Fix shadow parameter warning (#1936) 2024-09-12 12:02:25 -04:00
yhirose de36ea7755 Fix #1933 on Linux and macOS 2024-09-09 23:07:27 -04:00
yhirose 9f8db2c230 Fix #1933 2024-09-09 22:22:56 -04:00
yhirose 3f00e1b321 Revert "Changed set_file_content to accept only a regular file path."
This reverts commit 7ab9c119ef.
2024-09-09 20:03:47 -04:00
yhirose 7ab9c119ef Changed set_file_content to accept only a regular file path. 2024-09-09 19:59:18 -04:00
yhirose 3f2922b3fa Fix #1929 2024-09-09 19:25:52 -04:00
yhirose 509f583dca Fix problem caused by #1931. 2024-09-09 19:19:52 -04:00
Paul Harris 2d01e71286 Test reading empty zero-length file (#1931) 2024-09-09 17:54:11 -04:00
yhirose e612154694 Issue1431 (#1926)
* Renamed enable_server_host_verification to enable_server_hostname_verification and added Error::SSLServerHostnameVerification

* Add some Open SSL function calls

* Code cleanup

* Fix #1431
2024-09-08 22:45:47 -04:00
yhirose 82fcbe3901 Code cleanup 2024-09-08 20:10:35 -04:00
yhirose dbd2465b56 Add some Open SSL function calls 2024-09-08 20:10:17 -04:00
yhirose ea79494b29 Renamed enable_server_host_verification to enable_server_hostname_verification and added Error::SSLServerHostnameVerification 2024-09-08 20:08:52 -04:00
yhirose f35aff84c2 Fixed FuzzableServer build error 2024-09-08 19:00:23 -04:00
yhirose 7b18ae6f16 Update benchmark 2024-09-08 10:56:13 -04:00
yhirose a79c56d06b Fix #1796 2024-09-08 09:26:19 -04:00
yhirose 3d6e315a4c Fix #1923 2024-09-08 08:38:36 -04:00
yhirose 4c27f9c6ef Made default server and client read/write timeout settings separately 2024-09-07 21:06:23 -04:00
yhirose d173a37d17 Increased CPPHTTPLIB_READ_TIMEOUT_SECOND to 300 from 5 2024-09-07 16:10:54 -04:00
yhirose 7fd346a2ca Fix #1379 2024-09-07 16:07:45 -04:00
yhirose c673d502b9 Update server_and_client.cc 2024-09-07 11:11:57 -04:00
yhirose c43c51362a Add monitor tool 2024-09-07 10:16:15 -04:00
yhirose 3e86d93d13 clangformat 2024-09-07 10:16:03 -04:00
yhirose f6e4e2d0f3 Code cleanup 2024-09-07 10:15:22 -04:00
yhirose 01a52aa8bd Add example/server_and_client.cc 2024-09-07 10:05:53 -04:00
yhirose 8415bf0823 Resolve #1906 2024-09-07 00:05:41 -04:00
orbea 327ff263f5 httplib.h: support LibreSSL (#1922) 2024-09-06 22:19:53 -04:00
yhirose 61c418048d Release v0.17.3 2024-09-06 19:58:02 -04:00
yhirose 9720ef8c34 Code cleanup 2024-09-06 19:48:25 -04:00
yhirose 978a4f6345 Fix KeepAliveTest.SSLClientReconnectionPost problem (#1921) 2024-09-06 13:58:24 -04:00
bgs99 80fb03628b Only match path params that span full path segment (#1919)
* Only match path params that span full path segment

* Fix C++11 build
2024-09-06 08:48:51 -04:00
laowai9189 2480c0342c ‘constexpr’ error (#1918)
httplib.h: In member function ‘constexpr size_t httplib::detail::case_ignore_hash::operator()(const string&) const’:
httplib.h:359:30: error: call to non-‘constexpr’ function ‘const _CharT* std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::data() const [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’
  359 |     return hash_core(key.data(), key.size(), 0);
2024-09-06 07:23:29 -04:00
bgs99 eb6f610a45 Fix find_package for curl (#1920) 2024-09-06 07:22:03 -04:00
yhirose cb74e4191b Performance imporovement for Keep-Alive 2024-09-06 00:03:43 -04:00
yhirose dfa641ca41 Misc 2024-09-05 22:54:48 -04:00
yhirose 969a9f99d5 Adjust sleep 2024-09-05 22:54:28 -04:00
yhirose c099b42ba3 Removed write_format 2024-09-05 22:17:56 -04:00
yhirose b8315278cb Add a missing file 2024-09-05 19:35:43 -04:00
yhirose 485f8f2411 Added one more case to MountTest.Redicect unit test. 2024-09-05 17:49:12 -04:00
yhirose 953e4f3841 Adjust sleep duration 2024-09-05 17:45:09 -04:00
yhirose adf65cfe61 Target C++11 for benchmark 2024-09-05 17:44:51 -04:00
yhirose 12c829f6d3 Fix #1389 and #1907 2024-09-05 17:44:32 -04:00
yhirose 913314f1b1 Fix warning 2024-09-05 17:43:51 -04:00
yhirose ef63f97afe Release v0.17.2 2024-09-05 12:57:26 -04:00
yhirose bda74db01d Fix fuzzing test error 2024-09-05 12:50:05 -04:00
yhirose 9ff3ff9446 Fixed build error 2024-09-05 12:27:50 -04:00
yhirose c75d071615 Add benchmark tool 2024-09-05 12:22:46 -04:00
yhirose b4989130da Peformance improvement by removing tolower function call 2024-09-05 12:09:38 -04:00
yhirose 4fc0303bda clangformat 2024-09-05 12:07:35 -04:00
yhirose 3d9cc51851 Fixed build error on Windows due to max macro in windows.h 2024-09-05 12:02:40 -04:00
Andrea Pappacoda f69587656f build(meson): add libcurl test dependency (#1914)
Prompted by PR #1911
2024-09-04 18:05:03 -04:00
yhirose d5fc340c30 Update README 2024-09-04 12:23:48 -04:00
yhirose d79a547dc9 Merge branch 'solarispika-fix-100-continue' 2024-09-04 10:17:01 -04:00
yhirose bd1da4346a Disable Expect100ContinueTest test on Windows 2024-09-04 09:51:36 -04:00
yhirose 4c2a608a0c Fix GitHub Actions errors 2024-09-04 09:06:27 -04:00
yhirose ee4eb8deaa Merge branch 'fix-100-continue' of github.com:solarispika/cpp-httplib into solarispika-fix-100-continue 2024-09-04 08:45:48 -04:00
Sung, Po Han 7196ac8a07 Fix incorrect handling of Expect: 100-continue
Fix #1808
2024-09-04 17:50:42 +08:00
yhirose c88b09bc6b Release v0.17.1 2024-09-03 21:20:57 -04:00
yhirose 87fab847b8 Fix SIGINT problem in Docker image 2024-09-03 21:20:10 -04:00
yhirose 4e6055f084 Fix problem with Abstract Namespace Unix Domain 2024-09-03 20:56:16 -04:00
yhirose 975cf0dae5 Fix #1908 2024-09-03 18:00:12 -04:00
yhirose 4854a694cd Use IPPROTO_IP 2024-09-03 17:29:28 -04:00
yhirose b1f8e986bf Fix #1908 (#1910)
* Fix #1908

* Code format
2024-09-03 00:47:39 -04:00
yhirose c5ee208775 Fix build error on Mac and Linux 2024-09-02 23:04:38 -04:00
yhirose ddfdacfa49 Fix build error 2024-09-02 22:49:31 -04:00
yhirose 2514ebc20f Fix #1848 2024-09-02 20:38:01 -04:00
yhirose 4f9c6540b2 Fixed warning 2024-09-02 20:33:33 -04:00
mol123 21c9a6a1ff Windows: simplify conditional compilation and fix call to CreateFileMappingW. (#1909) 2024-09-02 18:01:05 -04:00
yhirose 7f6d413ddd Release v0.17.0 2024-09-01 07:53:56 -04:00
yhirose 88277139e7 Added set_ipv6_v6only method (#1905)
* Added `set_ipv6_v6only` method

* Adjust the place where socket_options is called
2024-09-01 07:52:24 -04:00
yhirose 6cdd3493a1 Fix #1788 2024-09-01 01:55:27 -04:00
yhirose 9c91b6f4a6 Fix #1645 2024-09-01 00:11:07 -04:00
yhirose cee838e335 Documentation 2024-08-31 17:42:43 -04:00
yhirose d82c82db2c Add sleep in handle_EINTR 2024-08-31 17:19:52 -04:00
yhirose ba638ff38e Update Docker support 2024-08-31 17:09:20 -04:00
yhirose da0c6579fa Breaking Change! get_header_ methods on Request and Response now take a default value. 2024-08-31 17:07:48 -04:00
yhirose 52a18c78a5 Add docker related files 2024-08-27 00:23:31 -04:00
yhirose 048edec9ed Changed CPPHTTPLIB_KEEPALIVE_MAX_COUNT to 100 2024-08-26 21:10:38 -04:00
yhirose af56b7ec0b Release v0.16.3 2024-08-17 09:53:26 -04:00
Jiwoo Park 6c3e8482f7 Fix KeepAliveTest.SSLClientReconnectionPost (#1895) 2024-08-10 07:19:59 -04:00
yhirose 390f2c41f6 Fix #1878 (#1893)
* Fix #1878
2024-08-08 22:07:46 -04:00
yhirose aa04feebb4 Fix warnings 2024-08-08 20:54:33 -04:00
yhirose 45f3694f82 Fix problem with clean command in Makefile 2024-08-08 19:30:46 -04:00
yhirose c5c54b31e2 Release v0.16.2 2024-08-08 11:48:50 -04:00
Mark Mentovai 69c84c9597 BoringSSL compatibility fixes (#1892)
This patch is necessary to build cpp-httplib in Crashpad, itself in
Chromium, using BoringSSL. Details at [1].

The fixes include:
 - Library version check: tolerate BoringSSL as an alternative to
   OpenSSL 3.
 - Don’t call `OPENSSL_thread_stop`, which is not in BoringSSL.
 - Use `SSL_get_peer_certificate` (deprecated in OpenSSL 3), the old
   name for `SSL_get1_peer_certificate`, because the new name is not in
   BoringSSL.
 - Call `SSL_set_tlsext_host_name` directly instead of making an
   `SSL_ctrl` call that BoringSSL does not support. The feared
   -Wold-style-cast warning that occurs when buidling with OpenSSL is
   not triggered in BoringSSL.

[1] https://chromium.googlesource.com/crashpad/crashpad/+/1a62a0182557c89494676c06611f1ca731bcb2db
2024-08-08 11:47:56 -04:00
yhirose ae63b89cbf Use SOCK_CLOEXEC instead of __linux__ 2024-08-06 17:31:55 -04:00
yhirose ff038f98b7 Merge branch 'thread-safe-cloexec' of github.com:kdombroski/cpp-httplib into kdombroski-thread-safe-cloexec 2024-08-06 17:22:43 -04:00
yhirose e00fd06355 Release v0.16.1 2024-08-06 17:04:22 -04:00
yhirose 521529d24d Fix #1481 (with content provider) (#1527)
* Fix #1481 (with content provider)

* Improve shutdown performance

* Make shutdown action more stable

* Move some tests up

* Simplified

* Simplified
2024-08-06 13:43:00 -04:00
yhirose ed0719f2bc Code format 2024-08-06 07:20:05 -04:00
hanslivingstone 6a848b1a16 Require a minimum of TLS 1.2 (#1889)
TLS 1. is deprecated: https://www.ietf.org/rfc/rfc8996.html
2024-07-30 17:18:33 -04:00
mol123 c8bcaf8a91 Fix build when targeting Windows 7 as platform. (#1869)
* Fix build when targeting Windows 7 as platform.

This change makes more of the code introduced in
https://github.com/yhirose/cpp-httplib/pull/1775
conditional on feature macros.

`CreateFile2`, `CreateFileMappingFromApp` and `MapViewOfFileFromApp` are
available only starting from Windows 8.

 * https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfile2
 * https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-createfilemappingfromapp
 * https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffilefromapp

* Update feature macros used and use `GetFileSizeEx` conditionally.
2024-07-02 10:58:40 -04:00
Hlado 8cd0ed0509 Added move assignment operator to Client class. (#1873) 2024-06-30 11:17:00 -04:00
Hlado 177d8420a1 Added .gitattributes file to prevent git from changing line endings (#1872)
of text files using as data for tests.
2024-06-30 11:16:48 -04:00
Daniel Ludwig 388a8c007c Fix build on Windows with no WINAPI_PARTITION_APP support (#1865) 2024-06-24 15:13:37 -04:00
Andrea Pappacoda bdefdce1ae test: fix GetRangeWithMaxLongLength on 32 bit machines (#1867)
The test used the hardcoded long value for 64 bit machines even on 32
bit ones, leading to test failures. With this patch the max long length
is obtained using std::numeric_limits<long>::max(). Thanks to q2a3z for
the hint!

Fixes: https://github.com/yhirose/cpp-httplib/issues/1795
2024-06-23 17:49:00 -04:00
Zhenlin Huangandjaredhuang 9e4f93d87e Allow hex for ipv6 literal addr in redirect (#1859)
Co-authored-by: jaredhuang <jaredhuang@tencent.com>
2024-06-17 11:44:51 -04:00
yhirose 0b657d28cf Added example/one_time_request.cc. 2024-06-14 18:29:34 -04:00
Rainer SchielkeandCEU\schielke c1a09daf15 avoid memory leaks if linked with static openssl libs (#1857)
* New function SSLServer::update_certs. Allows to update certificates while server is running

* New function SSLServer::update_certs. Added unit test

* avoid memory leaks if linked with static openssl libs

---------

Co-authored-by: CEU\schielke <Rainer.Schielke@heidelberg.com>
2024-06-14 15:40:03 -04:00
yhirose 8438df4a95 Release v0.16.0 2024-06-11 18:20:47 -04:00
Jiwoo Park 67fd7e3d09 Change library name to cpp-httplib (#1854) 2024-06-10 13:18:19 -04:00
Rainer SchielkeandCEU\schielke d44031615d New function SSLServer::update_certs. Allows to update certificates while server is running (#1827)
* New function SSLServer::update_certs. Allows to update certificates while server is running

* New function SSLServer::update_certs. Added unit test

---------

Co-authored-by: CEU\schielke <Rainer.Schielke@heidelberg.com>
2024-06-03 09:37:40 -04:00
Sean Quinn 98cc1ec344 Allow hex for ip6 literal addr, fix #1800 (#1830)
* Allow hex for ip6 literal addr, fix #1800

* Add UT for ipv6 + Universal client implementation

* add /n at EOF
2024-05-26 08:57:07 -04:00
Vladimír Chlup 25b1e0d906 Tweak CI & fix macOS prefix (#1843)
* Use brew prefix or given one

* Polish CI workflow file
2024-05-26 08:24:29 -04:00
Pavel P 05f9f83240 Avoid unreferenced formal parameter warning in get_range_offset_and_length (#1838)
Release builds result in the following warning because `content_length` param was used only inside asserts:

1> cpp-httplib\httplib.h(4933,45): warning C4100: 'content_length': unreferenced formal parameter
2024-05-17 09:56:06 +09:00
Karen Dombroski fb739dbaec threadsafe accept on windows, linux
* Windows has WSAAccept() which will create sockets inheriting flags from
  the server socket

* Linux has accept4() which has a flags argument supporting SOCK_CLOEXEC
2024-05-01 21:58:58 +12:00
Karen Dombroski 50fce538c6 threadsafe CLOEXEC on platforms that support it
SOCK_CLOEXEC is a flag available on some platforms to enable creation of
sockets with CLOEXEC already set
2024-05-01 21:46:50 +12:00
Jiwoo Park 3b6597bba9 Fix query parsing when value has = characters (#1822)
* Implement string divider to replace splitter

* Divide query string in half

* Add a test case for query values containing the '=' character

* Add test cases for string divider

* Fix warnings
2024-04-21 19:17:14 -04:00
KTGH f10720ed69 Move httplibConf.cmake.in & install readme/license (#1826)
* Move httplibConfig.cmake.in to cmake dir

Just makes more sense to put it there I suppose.

* Cmake install README & License

Seems to make sense since you might already do this as a package
manager, or an end user might want them anyways.

The locations are just based on standard Linux locations using
GNUInstallDirs, so it should be sane on other machines too.
2024-04-21 19:14:12 -04:00
rndm13andrndm 2bc550b2f0 Added progress to POST, PUT, PATCH and DELETE requests (#1821)
* Added progress to POST, PUT, PATCH, DELETE requests

* Added tests for post, put, patch, delete progress/cancellation

* fix accidental infinite recursion in delete

---------

Co-authored-by: rndm <rndm@localhost.localdomain>
2024-04-21 19:13:41 -04:00
Jiwoo Park ce36b8a6e5 Highlight notes using markdown features (#1820) 2024-04-18 10:19:54 -04:00
Kent 560854a961 Apply range header base on response status code (#1806)
* Enable ignoring range header to generate customized response

* Apply range header base on response status code
2024-04-11 23:28:21 -04:00
yhirose 2064462c35 Merge branch 'HerrCai0907-fix' 2024-04-11 22:30:00 -04:00
yhirose 07288888ad Code cleanup 2024-04-11 22:26:30 -04:00
yhirose 34d392cf3d Merge branch 'fix' of github.com:HerrCai0907/cpp-httplib into HerrCai0907-fix 2024-04-11 22:22:58 -04:00
yhirose 825c3fbbb1 Removed excess usage of std::move 2024-04-11 21:21:27 -04:00
Rusty ConoverandRusty Conover 00bdf73ec6 fix: increase default receive buffer to 16kb (#1814)
Since TLS packets have a maximum size of 16kb it makes
sense to fully accommodate them on reads.

Co-authored-by: Rusty Conover <rusty@Juno.local>
2024-04-09 16:59:07 -04:00
Jiwoo Park f44ab9b3da Fix range parser when parsing too many ranges (#1812)
* Implement range parser without std::regex

* Add test cases for invalid ranges
2024-04-07 10:06:16 -04:00
Andrea Pappacoda a61f2b89be build(meson): generate new test PEMs (#1813)
Follow-up to commits 548dfff0ae and
b8bafbc291
2024-04-07 10:05:07 -04:00
Jiwoo Park b8bafbc291 Generate missing PEMs for CTest (#1811)
* Generate missing PEMs

* Fix typo

* Copy files using simpler command
2024-04-05 13:50:21 -04:00
yhirose 548dfff0ae Fix #1793 2024-03-09 22:26:17 -05:00
Congcong Cai 4dd2f3d03d fix ambiguous for HandlerWithResponse and Handler in set_error_handler
using lambda expression as Handler in set_error_handler will cause ambiguous.
Template forwarding can forward HandlerWithResponse to the correct overloading function
2024-03-07 19:07:39 +08:00
KTGH 6791a8364d FindBrotli cleanup & fixes (#1786)
* Unimportant cleanup of FindBrotli

Some whitespace, removal of an unused var, and add a comment about
the missing version support

* Fix incorrect var in FindBrotli

Not much to say, it was just the wrong one. Not sure how that happened,
maybe a holdover from when I did an overhaul at some point..

* Tiny useless edit to FindBrotli

Just a little tweak to how I was postfixing those -static strings to the
lib names. Mostly pointless..

* Simplify some FindBrotli code

Nothing much, just reducing redundant stuff & a cleanup of a comment.

* Ignore PkgConf in FindBrotli if looking for static

As per the issue mentioned in this comment, static PkgConf support
is broken upstream. After testing, I found it'll just accept shared
even if you try to find static, so I'm merely disabling that feature
for this FindBrotli module.

That said, you can still get the static libs if you have them installed,
but PkgConf will be ignored even if found in favor of a regular
find_library call. Otherwise you'd end up with shared libs even if you
ask for static (with PkgConf installed).

TLDR: actually fail correctly when static libs aren't found when PkgConf
thought they were.
2024-02-27 19:25:02 -05:00
KTGH d1a1c8a158 Add description & URL to Cmake (#1785)
Doesn't do much, but some packages/builders might find a use for these
vars it provides...
2024-02-27 19:22:44 -05:00
yhirose b4d26badf2 Update github actions 2024-02-17 09:17:51 -05:00
Jiwoo Park c5a0673c93 Use final keyword for devirtualization (#1779) 2024-02-17 09:17:15 -05:00
Sergey ad40bd6a00 Implement file mapping for UWP apps (#1775)
Fixes #1773.
2024-02-08 22:59:34 -05:00
yhirose 5c00bbf36b Release v0.15.3 2024-02-05 22:12:43 -05:00
yhirose 9d6f5372a3 Fix #1772 2024-02-05 22:11:53 -05:00
Ikko Eltociear Ashimine f06fd934f6 Fix typo in gtest-all.cc (#1770)
synthetic -> synthetic
2024-02-05 15:35:33 -05:00
yhirose 80c0cc445e Release v0.15.2 2024-02-02 23:29:30 -05:00
yhirose 762024b890 Fix #1768 2024-02-02 23:17:32 -05:00
yhirose 82a90a2325 Update year 2024-01-29 08:53:01 -05:00
yhirose b7cac4f4b8 Release v0.15.1 2024-01-29 07:40:56 -05:00
yhirose e323374d2a Fix #1766 2024-01-28 17:43:51 -05:00
Jiwoo Park ffc294d37e Reduce object copy (#1767) 2024-01-28 08:18:29 -05:00
yhirose fceada9ef4 Changed to return 416 for a request with an invalid range 2024-01-28 08:13:19 -05:00
yhirose 5f0f73fad9 Reduce duplicate computation for ranges 2024-01-27 19:07:52 -05:00
yhirose 530d6ee098 Release v0.15.0 2024-01-27 17:39:58 -05:00
yhirose 420c9759c6 Fix #1694 2024-01-27 16:13:54 -05:00
yhirose 2ce7c22218 Fix #1747 2024-01-27 12:56:39 -05:00
Wander Nauta 4ef9ed80cd Treat paths with embedded NUL bytes as invalid (#1765)
Fixes #1763.
2024-01-27 08:22:00 -05:00
Jiwoo Park 44b3fe6277 Support move semantics for Response::set_content() (#1764) 2024-01-27 07:53:19 -05:00
Ilya Andreev 449801990f Add a getter for a bearer token from a request (#1755)
* Add a getter for a bearer token from a request

* Replace a method for bearer token getter with a free function
2024-01-15 08:57:22 -05:00
Jean-Francois Simoneau af2928d316 Fix select() return code for fd >= 1024 (#1757) 2024-01-15 08:27:31 -05:00
KTGH d948e38820 Minor cmake fix & cleanup (#1754)
* Reorder cmake docs a bit

Just wanted to group the more related build options together.

Also removed a pointless reference to the old reasoning for the required
min ver since it's 3.14 now anyways.

* Fix outdated cmake comment

We don't use Git to find the version string anymore.
Just updated to match what it's actually used for now.

* Group options and build-tree vars in Cmake

Doesn't really change anything, I just wanted to clean these up a bit.

* Fix how we set HTTPLIB_IS_USING_XXX vars in Cmake

Prevents us acidentally using libs when the user didn't want them
actually used. This could happen if they set the option to OFF but their
own project itself is using the lib, thus we'd find and use it anyways.

Ref #1602 to see an example of this already happening before.
This is merely apply that kind of fix to all 3 of our deps, instead of
just OpenSSL.

* Minor formatting/comment change to Cmake

Pointless, but these things were bothering me..
2024-01-04 18:20:37 -05:00
Matthias Bilger 65218ce222 added missing include of exception (#1752) 2023-12-31 18:59:43 -05:00
Adam Gajda 55e99c4030 Fix -Wold-style-cast warning (#1751) 2023-12-31 18:43:31 -05:00
TheOnlyJoey b63d50671d Fixes Windows std::max macro problems (#1750) 2023-12-30 11:37:58 -05:00
yhirose eba980846b Fix #1628 (OpenSSL 1.1.1 End of Life on September 11, 2023) (#1745) 2023-12-24 08:20:58 -05:00
vmaffione 374d058de7 ThreadPool: optional limit for jobs queue (#1741)
For very busy servers, the internal jobs queue where accepted
sockets are enqueued can grow without limit.
This is a problem for two reasons:
 - queueing too much work causes the server to respond with huge latency,
   resulting in repetead timeouts on the clients; it is definitely
   better to reject the connection early, so that the client
   receives the backpressure signal as soon as the queue is
   becoming too large
 - the jobs list can eventually cause an out of memory condition
2023-12-24 08:20:22 -05:00
yhirose 31cdcc3c3a Update README about MSYS2 and MinGW 2023-12-23 21:37:34 -05:00
yhirose ad9f6423e2 Fix #1744 2023-12-23 11:45:08 -05:00
yhirose cbca63f091 Release v0.14.3 2023-12-21 19:55:25 -05:00
yhirose b4748a226c Fix #1738 2023-12-21 13:33:52 -05:00
Ilya Andreev 5b943d9bb8 Use StatusCode in tests and examples (#1743)
* Use StatusCode in tests and examples

* Use StatusCode in README
2023-12-20 17:28:57 -05:00
Ilya Andreev c86f69a105 Use StatusCode in httplib code (#1742) 2023-12-19 22:17:24 -05:00
Ilya Andreev d39fda0657 Add StatusCode enum (#1739)
* Add StatusCode enum

* Remove changes on RFC 9110

* Add number suffixes to StatusCode constants

* Remove docs for StatusCode constants
2023-12-19 17:57:30 -05:00
Ilya Andreev 37f8dc4382 Change some of status messages based on RFC 9110 (#1740) 2023-12-19 09:22:58 -05:00
yhirose 3a8adda381 Fix #1737 2023-12-17 22:04:36 -05:00
yhirose 8aa38aecaf Fix #1665 2023-12-17 22:03:02 -05:00
yhirose f1dec77f46 Code format 2023-12-17 22:00:33 -05:00
yhirose cddaedaff8 Fix #1736 2023-12-15 19:29:54 -05:00
Renato Foot Guimarães Costallat cefb5a8822 Update README.md (#1731)
Fix "With Progress Callback" code example
2023-12-07 23:21:59 -05:00
davidalo e426a38c3e Fix: Query parameter including query delimiter ('?') not being parsed properly (#1713)
* Fix: Query parameter including query delimiter ('?') not being parsed properly

* Add details::split function with and without m argument to allow split parameters with/without counter

* Revert changes in SplitTest.ParseQueryString
2023-12-07 14:28:41 -05:00
yhirose f14accb7b6 Release v0.14.2 2023-12-04 22:31:12 -05:00
yhirose c5c704cb3b Fix #1724 2023-12-04 21:34:55 -05:00
Jean-Francois Simoneau 115a786581 Fix readability warnings (#1722)
* Fix readability warnings

Did not fix readbility-qualified-auto, will do a separate pull request

* Revert changes where meaning is lost

* Revert some style changes
2023-11-24 09:55:04 -05:00
Jean-Francois Simoneau 5ef4cfd263 Fix bugprone warnings (#1721) 2023-11-20 22:14:00 -05:00
Jean-Francois Simoneau 03fecb2f78 Fix modernize warnings (#1720) 2023-11-20 22:10:04 -05:00
Jean-Francois Simoneau 7fc8682a0a Fix performance-noexcept-move-constructor (#1715) 2023-11-20 13:13:59 -05:00
Jiwoo Park f1431311a4 Minor fixes on test cases (#1709)
* Fix data race

* Replace sleep_for() to wait_until_ready()
2023-11-11 21:28:50 -05:00
Jiwoo Park 1d14e051a5 Remove cryptui on Windows (#1710) 2023-11-11 21:26:57 -05:00
Jiwoo Park 97ae6733ed Run fuzz test in CTest (#1707) 2023-11-09 19:35:15 -05:00
Jiwoo Park 1d6b22b5f0 Fix C6001 (#1701) 2023-10-30 07:13:40 -04:00
yhirose 1a49076b5b Removed unnecessary exception 2023-10-29 19:36:40 -04:00
Jiwoo Park d0e4cb3f07 Include missing stdint.h on fuzz test (#1700)
* Include missing stdint.h

* Remove std:: from uint8_t
2023-10-29 19:26:06 -04:00
yhirose e2813d9d4d Code cleanup. (Removed unnecessary .c_str() calls) 2023-10-23 16:43:12 -04:00
Andrea Pappacoda 20a7f088ce build(meson): copy 1MB.txt test file (#1695)
Since tests are run in the build directory, the 1MB.txt file has to be
copied there.
2023-10-20 17:58:06 -04:00
yhirose f63ba7d013 Fix #1685 2023-10-03 09:59:27 -04:00
yhirose 0a629d7391 Release v0.14.1 2023-09-30 22:26:23 -04:00
PabloMK7 a609330e4c Add optional user defined header writer (#1683)
* Add optional user defined header writer

* Fix errors and add test
2023-09-30 22:13:14 -04:00
Jiwoo Park c029597a5a Update the remote address of www.httpwatch.com (#1664) 2023-09-13 10:33:33 -04:00
yhirose 30b7732565 Release v0.14.0 2023-08-22 20:19:07 -04:00
yhirose 6650632e7f Fix #1638 2023-08-22 19:36:10 -04:00
Sven Panne afe627e7af Avoid a -Warray-bounds false positive in GCC 13. (#1639)
The exact circumstances when this false positive is triggered are quite
tricky to reproduce, but it happened reproducibly with g++ 13.1 and 13.2 in
a close-source SW I'm working on.  The fix even improves performance by a
very tiny bit: There is no need to copy the std::smatch, having a const
reference is enough.

Just as a side note: -Warray-bounds seems to cause trouble in other
projects, too, so e.g. the Linux kernel has disabled since June 2022.
2023-08-14 10:26:54 -04:00
Duncan Ogilvie 67f6ff7fa9 Fix CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR (#1634) 2023-08-03 17:01:40 -04:00
yhirose c7ed1796a7 Release v0.13.3 2023-07-31 21:28:33 -04:00
yhirose 44c62d838e Use memory mapped file for static file server (#1632)
* Use memory mapped file for static file server

* Fix build error
2023-07-31 07:43:50 -04:00
yhirose 6bb580cda8 Fix #1559 2023-07-31 00:27:26 -04:00
yhirose 00a8cb8e5d Code cleanup 2023-07-31 00:22:22 -04:00
yhirose 2e34a39673 Added StaticFileRanges test 2023-07-31 00:22:22 -04:00
yhirose 01b90829bc Removed unnecessary CRLF at the end of multipart ranges data 2023-07-31 00:22:22 -04:00
yhirose e699bd0730 Release v0.13.2 2023-07-29 12:26:19 -04:00
Akın ELDENandakinelden 961a9379d5 Fix #1590 (#1630)
* ClientImpl: Connection=close header control moved from process_request to send_

* Connection=close header control moved from send_ to handle_request

* SSLClient::connect_with_proxy error handling improved

* to_string definition added for Error::ProxyConnection

* Comment improvement

---------

Co-authored-by: akinelden <akin.elden@gmail.com>
2023-07-29 12:09:25 -04:00
yhirose ec87b04aff Fix #1619 2023-07-29 00:53:57 -04:00
yhirose aabf752a51 Fix #1519 2023-07-28 23:37:45 -04:00
yhirose afb0674ccb Fix #1624 2023-07-21 20:36:01 -04:00
bdenhollander ee625232a4 Fix successful decompress reported as Error::Read (#1612)
* Fix successful decompress reported as Error::Read

Streams less than 4096 bytes are sometimes reported as failed reads because stream_.avail_in is not reduced to 0. The next iteration of the loop finds `prev_avail_in == strm_.avail_in` and return false. `ret = inflate(...)` returns Z_STREAM_END on the first iteration of the loop indicating that inflate is finished. This fix prevents the second iteration of the loop from failing.

* Fix successful decompress reported as Error::Read

- Add unit tests for raw deflate that illustrates the decompression failure when there are extra trailing bytes
2023-07-11 18:35:27 -04:00
Andrea Pappacoda 52d8dd41f1 build(meson): use C++14 with GTest >= 1.13.0 (#1618)
GoogleTest, starting with vesion 1.13.0, requires C++14 to build. This
patch enables C++14 if GoogleTest 1.13.0 or newer is detected when
compiling the tests with Meson, making it possible to use new GTest
versions.

You can find GoogleTest's release notes at
<https://github.com/google/googletest/releases/tag/v1.13.0>.
2023-07-11 18:32:41 -04:00
Alexandre Bouvier be07d2d7a9 cmake: fix comment (#1616) 2023-07-08 13:24:36 -04:00
yhirose 0f1b62c2b3 Release v0.13.1 2023-07-08 07:41:24 -04:00
yhirose c30906a541 Code cleanup 2023-07-07 19:43:37 -04:00
bgs99 3533503323 Fix explicit constructor warning (#1614) 2023-07-07 13:17:19 -04:00
yhirose 82acdca638 Release v0.13.0 2023-07-06 18:32:42 -04:00
vmaffione a1e56a567b Result: allow default constructor (#1609) 2023-07-05 12:05:29 -04:00
yhirose 0c17d172a2 Code cleanup 2023-07-05 09:06:21 -04:00
yhirose 5d8e7c761f Updated README 2023-07-05 08:45:05 -04:00
bgs99 17fc522b75 Add named path parameters parsing (Implements #1587) (#1608)
* Add named path parameters parsing

* Select match mode based on pattern

* Add examples and comments to README

* Add documentation to matchers
2023-07-05 07:44:19 -04:00
yhirose f1daa5b88b Fix #1607 2023-07-04 20:27:11 -04:00
yhirose fe9a1949a6 Fixed an example in README 2023-06-28 11:28:08 -04:00
yhirose 50cba6db9f Fix #1603 2023-06-27 21:23:23 -04:00
Nathan Moinvaziri 18592e7f98 Ability to turn off linking against OpenSSL if already detected. (#1602)
We already detect OpenSSL in our tree for another project, but don't want to
link httplib against OpenSSL or with SSL support.

Allow us to `set(HTTPLIB_IS_USING_OPENSSL FALSE)`.
2023-06-27 20:09:42 -04:00
yhirose bd9612b81e Changed to use auto (#1594) 2023-06-17 02:18:34 -04:00
yhirose 7ab5fb65b2 Code cleanup (#1593) 2023-06-16 18:08:28 -04:00
Andre EisenbachandAndre Eisenbach 8df5fedc35 Fix -Wold-style-cast warnings (#1591)
Removed multiple old-style (C) casts and replaced them with the
appropriate _cast's. This makes httplib a better citizen inside projects
that are compiled with all warnings enabled.

Unfortunately one warning remains as a result of invoking an
OpenSSL macro (at least on Linux), that would have to be fixed
upstream.

Also fixed a few casts for invocations of setsockopt.
setsockopt takes a const void* for the value pointer, not a char*.
Well, except on Windows ...

Co-authored-by: Andre Eisenbach <git@4ae.us>
2023-06-16 17:30:24 -04:00
Jiwoo Park 4a61f68fa4 Don't overwrite the last redirected location (#1589)
* Don't overwrite the last redirected location

* Check the last redirected location
2023-06-16 14:56:16 -04:00
Jiwoo Park 067890133c Use nghttp2-hosted httpbin.org (#1586)
* Use nghttp2-hosted httpbin.org

* Add CPPHTTPLIB_DEFAULT_HTTPBIN macro to choose the default httpbin.org
2023-06-15 11:12:41 -04:00
yhirose d3076f5a70 v0.12.6 2023-06-10 07:02:38 +09:00
yhirose ed129f057f Fixed C++11 warnings and code format 2023-06-09 20:49:46 +09:00
Jiwoo Park eab5ea01d7 Load in-memory CA certificates (#1579)
* Load in-memory CA certs

* Add test cases for in-memory cert loading

* Don't use the IIFE style
2023-06-09 16:34:51 +09:00
Petr Hosek 3e287b3a26 Provide a CMake option to disable C++ exceptions (#1580)
This allows disabling the use of C++ exceptions at CMake configure time.
The value is encoded in the generated httplibTargets.cmake file and will
be used by CMake projects that import it.
2023-06-06 16:56:26 +09:00
v1gnesh 4f33637b43 Add support for zOS (#1581)
Signed-off-by: v1gnesh <v1gnesh@users.noreply.github.com>
2023-06-06 14:14:06 +09:00
db-srcandDaniel Boles 698a1e51ec Move, not copy, Logger and Handler functors (#1576)
* Explicitly #include <utility> for use of std::move

* Move not copy Logger arg from Client to ClientImpl

* Move not copy, set_error_handler Handler to lambda

* Remove null statement in non-empty if/else block

I guess it was a relic from a time before the other statement was added.

---------

Co-authored-by: Daniel Boles <daniel.boles@voltalis.com>
2023-06-02 15:40:00 +09:00
yhirose 27c0e1186c Release v12.0.5 2023-05-30 16:09:42 +09:00
c54c71a3e5 Add HTTPLIB_INSTALL CMake option (#1575)
* Add HTTPLIB_INSTALL CMake option

* Proper formatting of HTTPLIB_INSTALL block

Co-authored-by: Jiwoo Park <jiwoo_90@naver.com>

---------

Co-authored-by: Niccolò Iardella <niccolo.iardella@doriansrl.it>
Co-authored-by: Jiwoo Park <jiwoo_90@naver.com>
2023-05-30 16:08:58 +09:00
yhirose 3409c00e6f Fixed warnings 2023-05-27 01:10:21 +09:00
yhirose f8ef5fab64 Release v0.12.4 2023-05-27 00:57:43 +09:00
yhirose 5b397d455d Fix more CRLF injection problems. 2023-05-22 22:56:16 +09:00
yhirose f977558a28 Release v0.12.3 2023-04-30 10:44:36 +09:00
Oleg Shparber c2e156e0e0 Fix leaked handle in create_socket (#1554)
Fixes resource leak problem detected by Coverity Scan.
2023-04-09 12:18:44 -04:00
yhirose 7aba2938d3 Fix #1548 2023-04-08 15:36:13 -04:00
yhirose d587548250 Fix #1545 2023-04-08 14:53:55 -04:00
Jiwoo Park 21f9c51556 Remove whitespace in CMake generator expression (#1552) 2023-04-07 15:40:52 -04:00
yhirose 985ceba525 Updated README 2023-04-04 07:17:59 -07:00
Sergey KazminandSergey Kazmin e62a4b02e5 fix (#1525)
Co-authored-by: Sergey Kazmin <sergey.kazmin@kaspersky.com>
2023-04-04 07:12:15 -07:00
Octavio Valle ff34749572 Initialize sockaddr_un to fix valgrind uninitialised byte message. (#1547) 2023-04-03 08:28:01 -07:00
Jiwoo Park e5804d4a50 Don't loading system certs from Keychain on iOS (#1546) 2023-04-01 06:26:30 -07:00
Jiwoo Park 3956a2b790 Fix ServerTest.ClientStop test case (#1542) 2023-03-30 09:50:51 -04:00
Jiwoo Park b33aa52dc2 Fix lifetime issues in test using detail::scope_exit() (#1535)
* Fix lifetime issues in test using detail::scope_exit()

* Remove checking joinable threads
2023-03-28 00:01:34 -04:00
yhirose 76230db97f Simplified scope_exit 2023-03-25 21:52:39 -04:00
yhirose a66a013ed7 Release v0.12.2 2023-03-25 21:47:14 -04:00
yhirose f4b02dfdc1 Fix #1533 2023-03-25 21:13:07 -04:00
yhirose 4cf218643e Code format 2023-03-25 21:12:40 -04:00
yhirose 8f96b69a25 Update test.yaml to use microsoft/setup-msbuild@v1.1 2023-03-23 18:53:27 -04:00
Johannes Flügel d262033ded Prevent overflow in hash function str2tag_core() (#1529)
* str2tag_core(): prevent overflow

* Update httplib.h

works for all sizes of unsigned int and if there exists a #define for max
2023-03-22 14:16:32 -04:00
yhirose 5745eabe69 Updated test.yaml to use ctions/checkout@v3 2023-03-21 18:54:54 -04:00
yhirose 5f18642271 Close #1531 2023-03-20 11:47:06 -04:00
yhirose 88a9278872 Fix #1486 2023-03-11 17:04:08 -05:00
yhirose 9bb3ca8169 Fix #1459 (#1523) 2023-03-10 22:21:42 -05:00
yhirose f2f4728489 Release v0.12.1 2023-03-10 17:53:19 -05:00
yhirose d1b616286f Add note for macOS regarding system certs 2023-03-10 17:48:19 -05:00
Mathieu GaillardandMathieu Gaillard df74526f91 Fix multipart Content-Type headers with both boundary and charset parameters (#1516)
* Fix multipart Content-Type headers with both boundary and charset parameters

* Improve code readability

* Add missing forward declaration

---------

Co-authored-by: Mathieu Gaillard <gaillard@adobe.com>
2023-03-08 23:57:17 -05:00
Joel Rosdahl 9f7ae0737a Fix typos (#1517) 2023-03-08 17:03:20 -05:00
yhirose 1ebb8412c5 Use SSL_ERROR_ZERO_RETURN to check if the SSL peer is closed. 2023-03-07 08:15:16 -05:00
Petr MenšíkandJiwoo Park 7b69999c37 Reuse gtest on system (#1493)
* Reuse gtest on the system

Try to use gtest on the system if found. Avoid using recent CMake
features.

* Set CMP0135 only when using FetchContent

* Add /bigobj option for MSVC

* Support also cmake between 3.14 and 3.20

Older versions provided only GTest::Main target, not currently used
gtest_main. Provide backward compatibility also to old cmake versions.

* Remove redundant variable checking

---------

Co-authored-by: Jiwoo Park <jiwoo_90@naver.com>
2023-03-05 21:35:35 -05:00
yhirose c7e959a948 Fix #1481 2023-03-04 18:14:00 -05:00
yhirose ba5884e779 Fix #1481 (#1513) 2023-03-03 23:45:19 -05:00
yhirose cdaa5c48db Code cleanup 2023-03-03 22:41:57 -05:00
Alexandre Bouvier bab5c0e907 cmake: fix find_dependency (#1509) 2023-03-01 08:38:58 -05:00
Alexandre Bouvier 016838fd10 cmake: support components (#1504) 2023-02-25 09:56:56 -05:00
yhirose 75053bf855 Fix #1498 (#1501)
* Fix #1498

* Fixed build error
2023-02-22 13:19:36 -05:00
yhirose ae3a6dd2a9 Fixed an issue with example/Makefile on MacOS 2023-02-21 22:00:10 -05:00
Sergey KazminandSergey Kazmin 6d963fbe8d Support loading system certs from Keychein on MacOS (#1474)
* Support loading system certs from Keychein on MacOS

* review improvements: add deps to meson.build and improve conditional expressions in cmake

* fix tabs

* fix tabs

* review improvements

* fix after review

* additionally load root certs from the system root keychain

* cmake fix

* fix

* small refactoring

* small refactoring

---------

Co-authored-by: Sergey Kazmin <sergey.kazmin@kaspersky.com>
2023-02-17 12:06:55 -05:00
jingTian-z 88f6245c84 feat: Add Request::get_file_multi_value func. (#1495)
Support to get multiple values of a key.

perf: Rename function names, variable names etc.
2023-02-16 21:51:06 -05:00
yhirose 0e7d2f9f93 Resolve #1482 2023-02-14 11:40:47 -05:00
yhirose 4e6ded1f36 Release v0.12.0 2023-02-07 10:27:40 -05:00
yhirose d663588491 Removed is_writable() from DataSink (Resolve #1478, too) (#1483) 2023-02-04 13:53:42 -05:00
yhirose 439caf5b79 Fix #1479 2023-02-01 19:11:11 -05:00
yhirose c4ba43ca6f Removed incorrect comment 2023-01-24 09:07:42 -05:00
Jiwoo Park 20cba2ecd9 Support CTest (#1468)
* Add test/CMakeLists.txt to enable testing with CTest

Add HTTPLIB_TEST option
Downlaod GoogleTest source using FetchContent module
Generate cert files to test httplib with OpenSSL

* Generate cert2.pem with a new certificate request

* Use the latest GoogleTest library
2023-01-21 03:43:22 -05:00
yhirose 0ff2e16d69 Issue 52666: cpp-httplib:server_fuzzer: Timeout in server_fuzzer 2023-01-21 01:09:19 -05:00
Ray Beck 51607ec752 add to_human_string (#1467)
* add to_human_string

* replace to_string with to_human_string

* fix test
2023-01-19 07:01:34 -05:00
yhirose 7992b14896 Release v0.11.4 2023-01-09 17:08:11 -05:00
Ray Beck 7e420aeed3 add support for requests with both MultipartFormDataItems and Content Providers (#1454)
* add support for requests with both MultipartFormDataItems and ContentProviders

* rework implementation

* use const auto & and fix offset calculation

* fix zero items

* snake case variables

* clang-format

* commonize get_multipart_content_provider, add Put() with MultipartFormDataProviderItems

* fix linker multiple definition error

* add test MultipartFormDataTest.DataProviderItems
2023-01-08 18:38:14 -05:00
yukunandyukun.yu 227d2c2050 Add EINTR and EAGAIN judge for accept (#1438)
* Add EINTR and EAGAIN judge for accept

* Add EINTR signal tests

* Cancel win32 and win64 compile on signal unittest

Co-authored-by: yukun.yu <yukun.yu@alibaba-inc.com>
2022-12-20 19:34:51 -05:00
yhirose 93e53c91f7 Updated unit test 2022-12-10 11:45:56 -05:00
Jiwoo Park 58cffd3223 std::condition_variable::notify_one/all() should be called after unlocking mutex (#1448)
* Move next job in task queue rather than copy

* Notify waiting thread after unlocking mutex

* Add unit test for TaskQueue

* Don't use C++14 feature in test code
2022-12-09 17:37:48 -05:00
Ingo Bauersachs 8f32271e8c Support LOCAL_ADDR and LOCAL_PORT header in client Request (#1450)
Having the local address/port is useful if the server is bound to
all interfaces, e.g. to serve different content for developers
on localhost only.
2022-12-06 08:23:09 -05:00
yhirose c8c1c3d376 Fix #1442 2022-12-01 17:21:40 -05:00
yhirose 9f512acb42 Removed code for upsupported OpenSSL 2022-12-01 13:41:18 -05:00
yhirose c0b461a3b7 Release v0.11.3 2022-11-29 19:26:10 -05:00
yhirose 74fe5a5029 Fix #1426 2022-11-27 10:53:11 -05:00
yhirose 9d0a9d4e23 Fix #1437 2022-11-27 10:21:24 -05:00
TheMarpe 5758769ad3 Windows CMake directory install fix (#1434)
* Fixed install convention pre 3.25

* Simplified CMAKEDIR installation directory
2022-11-24 22:51:55 -05:00
Ray Beck e7eadc3605 add SYSTEM to include to prevent warnings (#1428) 2022-11-24 22:23:11 -05:00
yhirose 07c6e58951 Fix #1421 2022-11-15 11:57:14 -05:00
Pavel Artemkin 87994811a1 undef poll at the end if CPPHTTPLIB_USE_POLL (#1427)
* undef poll at the end if CPPHTTPLIB_USE_POLL

* win32 only
2022-11-13 21:49:49 -05:00
yhirose 42feb7e8be Updated README 2022-11-10 08:27:30 -05:00
yhirose 26196b70af Add test case for 'Issue 52666 in oss-fuzz: cpp-httplib:server_fuzzer: Timeout in server_fuzzer' 2022-11-03 13:07:04 -04:00
Changbin ParkandChangbin Park 93a51979c4 Get client process id over ip/port when server runs on UNIX socket. (#1418)
* handle socket options for UNIX socket same as others

 * set FD_CLOEXEC by default
 * invoke `socket_options` callback if set

* Offer Client info even on UNIX socket based Server

HTTP Request header "REMOTE_PORT" contains client process id if possible
when Server works on UNIX socket.

* retrigger checks

* retrigger checks

* add support macOS

Co-authored-by: Changbin Park <changbin.park@ahnlab.com>
2022-11-03 10:25:18 -04:00
Changbin ParkandChangbin Park ad7edc7b27 avoid lockup in ThreadPool::shutdown() on legacy host (#1417)
It is found on CentOS 7.0.1406 which is very early version of CentOS 7.

Co-authored-by: Changbin Park <changbin.park@ahnlab.com>
2022-11-03 10:23:45 -04:00
Andreas Kempf 27cd4e6ffe Support compilation on AIX (#1402)
Disable the ifaddrs.h include statement and the USE_IF2IP flag on AIX.
AIX is a Unix OS but does not offer the ifaddrs.h header.
2022-10-14 10:49:48 -04:00
yhirose cae5a8be1c Added more fuzzing corpus 2022-09-24 08:31:21 -04:00
yhirose 8e10d4e8e7 Release v0.11.2 2022-09-12 11:36:14 -04:00
Changbin ParkandChangbin Park b57f79f438 Detecting client disconnection (#1373)
* SocketStream need to check connectivity for writability.

When the stream is used for SSE server which works via chunked content
provider it's not possible to check connectivity over writability
because it's wrapped by DataSink. It could make the server stalled, when
the provider wants to only keep connection without send data and certain
amount of clients consumes entire threadpool.

* add unittest for SocketStream::is_writable()

SocketStream::is_writable() should return false if peer is disconnected.

* revise broken unittest ServerTest.ClientStop

DataSink could be unwritable if it's backed by connection based. Because
it could be disconnected by client right after enter centent provider.

Co-authored-by: Changbin Park <changbin.park@ahnlab.com>
2022-08-30 21:11:19 -04:00
Andrea Pappacoda a9cf097951 build: set soversion to major.minor (#1357)
Release 0.11 broke backwards compatibility, meaning that different
cpp-httplib versions are compatible with each other only if the major
and minor version numbers are the same.

This patch reflects this in the build systems.

See #1209 for some more context.
2022-08-12 13:48:40 -04:00
yhirose 5c3624e1af Updated example/uploader.sh 2022-08-06 14:43:56 -04:00
yhirose cba9ef8c0b Issue 49740 in oss-fuzz: cpp-httplib:server_fuzzer: Timeout in server_fuzzer 2022-08-06 08:08:08 -04:00
yhirose 4f8407a3a7 Refactoring the previous commit 2022-08-04 20:56:02 -04:00
Gopinath K 656e936f49 add multipart formdata for PUT requests. (#1351)
* httplib.h

  add multipart formdata for PUT in addition to POST as some REST
  APIs use that.

  Factor the boundary checking code into a helper and use it from
  both Post() and Put().

* test/test.cc

  add test cases for the above.
2022-08-04 20:42:13 -04:00
yhirose d92c314466 Release v0.11.1 2022-08-02 19:44:25 -04:00
yhirose b747fb111d Updated README 2022-08-02 19:42:11 -04:00
yhirose 7e0a0b2d0c Updated README 2022-08-02 19:30:15 -04:00
Changbin ParkandChangbin Park 362d064afa UNIX domain socket support (#1346)
* Add support UNIX domain socket

* `set_address_family(AF_UNIX)` is required

* add unittest for UNIX domain socket

* add support UNIX domain socket with abstract address

Abstract address of AF_UNIX begins with null(0x00) which can't be
delivered via .c_str() method.

* add unittest for UNIX domain socket with abstract address

Co-authored-by: Changbin Park <changbin.park@ahnlab.com>
2022-08-01 06:57:25 -04:00
Ata Yardımcıandata.yardimci 1bd88de2e5 Fix test build warning (#1344)
Co-authored-by: ata.yardimci <ata.yardimci@erstream.com>
2022-07-31 16:51:06 -04:00
Rockybillyandata.yardimci 0b541ffebc Add get_socket_fd method to Client and ClientImpl, add according unit… (#1341)
* Add get_socket_fd method to Client and ClientImpl, add according unit test

* Change name get_socket_fd to get_socket

* Change name get_socket to socket

Co-authored-by: ata.yardimci <ata.yardimci@erstream.com>
2022-07-31 08:27:38 -04:00
yhirose 106be19c3e Issue 49512: cpp-httplib:server_fuzzer: Timeout in server_fuzzer 2022-07-30 23:27:29 -04:00
yhirose 25d72bf881 Release v0.11.0 2022-07-29 20:45:55 -04:00
Mehmet İbrahimoğluandiamttaM 9d5b5297cc ssize_t redefinition on Windows - int/int64 vs long/long long (#1337)
* ssize_t redefinition on Windows - int/int64 vs long/long long

* Define ssize_t as __int64 for _WIN64, not long long

Co-authored-by: iamttaM <9880090+oculusbytes@users.noreply.github.com>
2022-07-29 20:42:31 -04:00
Mehmet İbrahimoğluandiamttaM 462884bebb With SSL enabled and NOMINMAX not defined, there is a conflict with 'max', which this fixes (#1334)
Co-authored-by: iamttaM <9880090+oculusbytes@users.noreply.github.com>
2022-07-27 08:16:06 -04:00
ZHANG Xiang b1cc24b795 Update README.md (#1332)
Update error list. Introduce `httplib::to_string` in the example code.
2022-07-25 06:48:52 -04:00
yhirose f0eb55b327 Changed to use const std::string & as much as possible instead of const char * (#1331)
* Changed to use `const std::string &` as much as possible instead of `const char *`

* Fix problems on Windows
2022-07-22 22:44:33 -04:00
yhirose 6dc285b5ca Merge branch 'kuguma-use_exception_ptr' 2022-07-15 17:33:53 -04:00
yhirose 07e614eef7 clangformat and README update 2022-07-15 17:32:38 -04:00
yhirose 916b2a8fd3 Merge branch 'use_exception_ptr' of https://github.com/kuguma/cpp-httplib into kuguma-use_exception_ptr 2022-07-15 17:19:39 -04:00
Kai Aoki 869f5bb279 fix ExceptionHandlerTest.ContentLength 2022-07-15 11:50:26 +09:00
yhirose 3e21338f82 Update README 2022-07-14 20:59:48 -04:00
yhirose 37bb3c6a77 No longer support VS 2013 and older #1325 (#1326)
* Fixed a warning

* No longer support VS 2013 and older (Fix #1325)
2022-07-14 20:57:41 -04:00
Kai Aoki d4ab2fa0e6 fix double ref and case of exceptions that are not std::exception 2022-07-15 01:45:10 +09:00
Kai Aoki 72d3f4896a Update httplib.h
use std::exception_ptr
2022-07-12 00:10:57 +09:00
yhirose 5e6f973b99 Release v0.10.9 2022-07-08 17:40:05 -04:00
yhirose 7ed77b02ad Disable YouTubeNoSSLDigest 2022-07-08 17:40:05 -04:00
yhiroseandGavin1937 127a64d5a0 Skip preamble and epilogue in multipart/form-data (Fix #1317) (#1320)
* fix: skip MIME preamble (#1317)

* Skip epilogue in multipart/form-data

Co-authored-by: Gavin1937 <71205842+Gavin1937@users.noreply.github.com>
2022-07-08 17:26:50 -04:00
yhirose caa31aafda Accept large data transfer over SSL (Fix #1261, Close #1312) 2022-07-02 07:50:33 -04:00
yhirose dae318495f Revert "Accept large data transfer over SSL (#1261)"
This reverts commit 307b729549.
2022-07-02 07:18:59 -04:00
Andrea Pappacoda 305a7abcb9 fix: update CPPHTTPLIB_VERSION to 0.10.8 (#1305) 2022-06-08 16:44:10 -04:00
yhirose 219d13b718 Fix #1303 2022-06-07 09:52:08 -04:00
conghuawang df20c27696 resolve http server can't send file large than 2GB (Fix #1290) (#1294)
* resolve problem: http server can't send file large than 2GB.
add unit test for http server send large file.
add /bigobj compile option to msvc x64.

* disable unit test "ServerLargeContentTest" due to out-of-memory on GitHub Actions.
2022-05-27 11:56:20 -04:00
yhirose a5a62768c0 Fix #1292 (#1296) 2022-05-27 11:54:43 -04:00
yhirose 4001637beb Added CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 2022-05-26 10:16:32 -04:00
Martín Córdova 47044c05a8 Fix compile error with MINGW-64 GCC-12.1.0 (#1283) 2022-05-24 07:16:54 -04:00
Andrea Pappacoda a449d82723 build(cmake): minor tweaks (#1274)
- Enable THREADS_PREFER_PTHREAD_FLAG to use -pthread where supported
- Remove low-level compile features (closes #1272)
- Remove unneeded DESTINATION options where possible
2022-05-17 07:02:44 -04:00
yhirose fee8e97b4e Rename fuzzing test corpus for #1264 2022-05-03 14:53:59 -04:00
yhirose 72d9ed4056 Added fuzzing corpus for #1264 2022-05-02 18:06:14 -04:00
Andrea Pappacoda 1be1b3a86d build(meson): don't require python3 (#1267)
Thanks to abf3a67dd0 the use of python3
isn't required anymore to configure the build, so I moved the
find_program('python3') inside the "if compile" block.

This makes it possible to configure cpp-httplib on systems where python
isn't available with tools like muon: https://sr.ht/~lattis/muon/
2022-04-30 17:40:47 -04:00
yhirose 9452c0a4b6 Release v0.10.7 2022-04-28 10:21:14 -04:00
Yoshiki Matsuda 307b729549 Accept large data transfer over SSL (#1261)
* Add large data transfer test

* Replace `SSL_read` and `SSL_write` with `ex` functions

* Reflect review comment

* Fix return value of `SSLSocketStream::read/write`

* Fix return value in the case of `SSL_ERROR_ZERO_RETURN`

* Disable `LargeDataTransfer` test due to OoM in CI
2022-04-27 21:08:39 -04:00
mylogin 696239d6e1 Link Windows crypto libs only when CPPHTTPLIB_OPENSSL_SUPPORT is set (#1254) 2022-04-20 22:04:55 -04:00
Andrea Pappacoda 6929d90353 build(meson): allow using OpenSSL 3.0 (#1256)
Following 0857eba17b cpp-httplib is fully compatible with OpenSSL versions newer than 1.1.1
2022-04-20 21:39:52 -04:00
yhirose 348d032029 Updated README 2022-04-19 23:02:30 -04:00
Andrea Pappacoda d1d3fcdfd5 build(meson): mark *_encrypted_pem as test deps (#1255)
Meson only runs required targets. The key_encrypted_pem and
cert_encrypted_pem targets added in 020b0db090
and 8191fd8e6c weren't added to the list
of targets required by the test target, so the generation of the
encrypted certs was skipped, resulting in the failure of
BindServerTest.BindAndListenSeparatelySSLEncryptedKey.
2022-04-19 07:12:00 -04:00
Eli Schwartz abf3a67dd0 meson: fix regression that broke extracting version (#1253)
* meson: fix regression that broke extracting version

In commit 33f67386fe the code that
heuristically parsed the version broke due to the version being moved
around into a more easily accessible define.

While we are at it, pass the exact path of httplib.h to un-break usage
as a meson subproject. This was broken in commit
8ecdb11979 which checked the return code
of trying to get the version; it was always broken, but formerly failed
in silence and resulted in no version number.

* meson: use the compiler builtins to extract the version from the header

As a convenient string define, it is now possible to ask the
preprocessor what the version of cpp-httplib is. This can be used from
meson too, in order to avoid encoding C++ file structure into python
regexes.
2022-04-19 07:11:51 -04:00
yhirose d87abeecf0 Release v0.10.6 2022-04-17 17:34:48 -04:00
yhirose 4e28e4f741 Fix #1251 2022-04-17 11:53:41 -04:00
yhirose 80a55cedeb Removed Repl.it examples 2022-04-16 21:11:17 -04:00
yhirose d05c343602 Release v0.10.5 2022-04-16 21:02:30 -04:00
yhirose 33f67386fe Fix #1249 2022-04-16 08:54:56 -04:00
yhirose 56d8168dc4 clangformat 2022-04-16 08:52:55 -04:00
greenfish 5d87cc0558 resolve compiler warnings (#1246)
* resolve compiler warnings

- check `WSAStartup` return.
- `const` is not suitable for `std::move`.

* resolve compiler warnings

- bool startup => bool is_valid_.
- remove `const` not removed.
2022-04-14 11:46:10 -04:00
yhiroseandKotarou cb41947eb4 Fix #1235 (#1243)
* Fix #1235

* fix BindIPAddress error (#1242)

* Code cleanup

* Added a unit test

* Commented out 'SSLClientTest.SetInterfaceWithINET6'

* Fixed incorrect return value from if2ip

* Removed if_nametoindex call

Co-authored-by: Kotarou <2918558+CyberKoo@users.noreply.github.com>
2022-04-13 21:32:46 -04:00
Kotarou 0857eba17b replace deprecated OpenSSL functions with evp functions (#1241) 2022-04-11 13:40:58 -04:00
Andrea Pappacoda 020b0db090 build(meson): generate key_encrypted.pem (#1221)
8191fd8e6c only added one of the two files
2022-03-20 12:21:45 -04:00
Andrea Pappacoda bf0760fde4 fix: update user agent (#1218) 2022-03-18 18:12:51 -04:00
yhirose bb8e45383e Update README 2022-03-17 08:38:15 -04:00
yhirose a1df576e4f Fix #1212 2022-03-16 22:00:40 -04:00
yhirose 7fb0254794 Fix #1215 2022-03-16 12:50:13 -04:00
yhirose c82d1e52cc Fix #1214 2022-03-16 10:56:33 -04:00
yhirose 846151b605 Added a unit test case for large multipart form data 2022-03-16 09:37:13 -04:00
Alexandre Bouvier e44e31dd5b Add soversion (#1209) 2022-03-03 21:06:08 -05:00
yhirose f7b9501662 clangformat 2022-02-27 14:31:22 -05:00
yhirose e12fe4cbbb Performance improvement 2022-02-27 14:30:49 -05:00
yhirose 49d2e1f135 Fix problem with InvalidPort test 2022-02-27 14:29:34 -05:00
Sebastien Blanchet 8191fd8e6c Add optional private key password to SSLServer ctor (#1205) 2022-02-27 14:16:15 -05:00
yhirose d73395e1dc Release v0.10.3 2022-02-22 10:21:27 -05:00
au-ee 64d001162b CPPHTTPLIB_NO_DEFAULT_USER_AGENT skips default user agent (#1201) 2022-02-22 09:39:26 -05:00
yhirose bb00a23116 Apply clangformat 2022-02-09 17:16:47 -05:00
yhirose 63d6e9b91b Removed up.sh and down.sh 2022-02-09 17:16:18 -05:00
Edwin Kofler 66eed5681a Fix typo in Dockerfile (#1187) 2022-02-06 09:15:15 -05:00
Andrea Pappacoda 8ecdb11979 build(meson): always install a pkg-config file (#1182)
A pkg-config file was previously installed only if cpp-httplib was being
built as a compiled library.

Since architecture-independent .pc files
can exist in /usr/share/pkgconfig, it can be useful to install one even
when installing the header-only version (for example, it could be used
by third party projects to easily find out if cpp-httplib is installed
and its version, using something like Meson's `dependency()` or CMake's
`pkg_check_modules()`).

The change makes the Meson build behave a bit more like the CMake one,
as it also always installs a CMake Config file, but here the pkg-config
file gets installed to the correct architecture-independent directory
(`datadir` represents /usr/share on Linux and simiar systems).

Lastly, I made some minor cleanups.
2022-02-03 19:50:49 -05:00
Andrea Pappacoda 894fcc8e02 test: add missing "_Online" suffix (#1183)
This test fails reproducibly in a Debian build chroot, and they generally don't have internet access
2022-01-30 12:34:52 -05:00
Rockybilly 7f43f0f3ff User-Agent update cpp-httplib/0.10.2 (#1181) 2022-01-28 12:27:27 -05:00
Gregor Jasny 87e03dd1ce Report connection timeout as separate event (#1171) 2022-01-26 17:32:40 -05:00
yhirose e5cacb465d Fix #1172 (#1173)
This change is based on RFC7230, § 3.5 'Message Parsing Robustness': "Although the line terminator for the start-line and header fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR."
2022-01-26 13:34:23 -05:00
yhirose ee8371f753 Added 'PostLarge' unit test for #1169 2022-01-22 09:52:27 -05:00
yhirose 081723f983 Add another fuzz test corpus 2022-01-20 15:27:26 -05:00
yhirose b61f36579c Fix #1166 2022-01-20 15:24:09 -05:00
ArnaudBienner 33f53aa458 Fix set_content_provider example in README.md (#1163) 2022-01-13 12:26:34 -05:00
yhirose 412ab5f063 Added example/Dockerfile.hello 2022-01-11 00:18:20 -05:00
yhirose 11e02e901c Fixed unit test 2021-12-31 16:10:57 -05:00
yhirose 65a8f4cf44 Added hosted_at. (Resolve #1113) 2021-12-31 15:35:52 -05:00
yhirose 27d128bbb4 Fix problems in #1154 2021-12-31 14:55:40 -05:00
yhirose 070f9bec58 Code cleanup 2021-12-31 13:27:47 -05:00
c00candzhangsen f817032513 fix socket option setting for windows (#1154)
* fix socket option setting for windows

* misc

Co-authored-by: zhangsen <zhangsen@cyberkl.com>
2021-12-31 10:07:59 -05:00
yhirose 17abe221c0 Fix is_file problem on Windows (#1153) 2021-12-30 14:54:57 -05:00
vitaly-ivanov 4a7a81e039 Work around silly Win defines to support BoringSSL (#1148)
* Work around silly Win defines to support BoringSSL

* changes wrapped into ifdef(_WIN32) just in case
2021-12-30 11:08:51 -05:00
yhirose 37fd4eb643 Code cleanup 2021-12-26 07:53:56 -05:00
yhirose 865b0e4c03 Resolve #1145 2021-12-26 07:53:06 -05:00
yhirose b324921c1a Release v0.10.1 2021-12-24 21:01:06 -05:00
yhirose 63f72caf30 Fix "Issue 42689 in oss-fuzz: cpp-httplib:server_fuzzer: Timeout in server_fuzzer" 2021-12-24 20:58:09 -05:00
yhirose 99ac17b90a Fix #1140 2021-12-23 23:19:14 -05:00
yhirose 4b0ed9ee88 Release v0.10.0 2021-12-21 18:15:41 -05:00
yhirose 20056f6cda Update test.cc 2021-12-19 14:19:10 -05:00
yhirose 3b35279b16 Added SSLServer::ssl_context() 2021-12-18 00:21:41 -05:00
yhirose 27deb44df5 Update SSL related code 2021-12-18 00:15:38 -05:00
yhirose 24a3ef949b Performance improvement for multipart form data file upload. 2021-12-18 00:15:10 -05:00
yhirose bc3e098964 Updated .gitignore 2021-12-17 22:36:02 -05:00
yhirose c247dcdd7b Added uploader.sh 2021-12-17 22:34:00 -05:00
yhirose 793ae9855e Fix #1041 (#1132)
* Fix #1041

* Fixed problem with is_socket_alive

* Adjust the way to check if the sockt is still alive.

* Revert "Adjust the way to check if the sockt is still alive."

This reverts commit 6c673b21e5.

* Adjust is_socket_alive according to the code review
2021-12-16 21:06:17 -05:00
yhirose 9fa426d51b Added more fuzzing corpus 2021-12-14 18:35:20 -05:00
yhirose cec6288a99 Resolve #1131 2021-12-14 07:58:21 -05:00
yhirose 9639578c2a Release v0.9.10 2021-12-11 19:26:22 -05:00
yhirose 743ecbd365 Issue1121 (#1122)
* Fixed test/Makefile problem when cleaning *.pem files

* Fix #1121
2021-12-11 19:07:12 -05:00
yhirose 084c643973 Fixed README 2021-12-10 22:33:59 -05:00
Andrea Pappacoda 824e7682e4 test: add missing _Online suffixes (#1110) 2021-11-27 09:54:05 -05:00
Andrea Pappacoda f9074684dd build(meson): drop Git-based version detection (#1109)
See 3051152103
2021-11-27 09:47:09 -05:00
yhirose ddff782133 Release v0.9.9 2021-11-26 20:46:38 -05:00
yhirose 3051152103 Fix #1102 (#1108) 2021-11-26 20:44:58 -05:00
yhirose 06026bb47d Code formating 2021-11-23 10:53:05 -05:00
yhirose 226388ae27 Resolve #1100 2021-11-23 10:47:30 -05:00
Scott Graham ea7548b4cc Remove stray ; causing warning with -Wextra-semi (#1099) 2021-11-22 07:16:07 -05:00
Rodolpheandrodolphe c7486ead96 accept protobuf encoding (#1096)
Co-authored-by: rodolphe <rodolphe@zen.ly>
2021-11-17 13:14:31 -05:00
yhirose 90a291214c Update Makefile 2021-11-15 23:08:49 -05:00
Andrea Pappacoda c111c42a86 build(meson): feature args in pkg-config file (#1092)
Follow-up for #1090. The args are now also added to the pkg-config file.
2021-11-15 22:50:33 -05:00
yhirose 6fb5b63018 Fix #1093. Remove meson-build tests from GitHubActions (#1094) 2021-11-15 22:49:40 -05:00
yhirose ec56dfa35e Fix #1085 (#1091) 2021-11-15 14:37:10 -05:00
Andrea Pappacoda 943cd51b67 build(meson): pass feature args to dependency consumers (#1090)
Fixes #1087
2021-11-15 14:03:25 -05:00
Yuji Hirose 301faa074c Added test case for #1065 2021-10-29 07:29:23 -04:00
DavidKorczynski dc0481e832 Add CIFuzz integration (#1079)
Signed-off-by: David Korczynski <david@adalogics.com>
2021-10-27 12:19:21 -04:00
zhenyolka 4f8fcdbaf7 Allow to specify server IP address (#1067)
* Allow to specify server IP address

* Reimplement in set_hostname_addr_map

* Add tests for set_hostname_addr_map

* Fix tests after implement set_hostname_addr_map

* SpecifyServerIPAddressTest.RealHostname typo
2021-10-16 15:05:55 -04:00
CarlosLeeGit b80aa7fee3 support custom ssl ctx configuration for SSLServer (#1073) 2021-10-15 07:13:16 -04:00
yhirose c384be02c9 Fixed GitHub Actions build error 2021-10-14 10:52:05 -04:00
yhirose d17ac3bb40 Fix "Issue 39922 in oss-fuzz: cpp-httplib:server_fuzzer: Timeout in server_fuzzer" 2021-10-14 08:55:29 -04:00
yhirose c7554ccac2 Fix #1069 (#1070) 2021-10-09 20:35:58 -04:00
yhirose 35ef1c7bae Fix #1063 2021-10-03 18:37:59 -04:00
David Pfahler d87d0672a8 Split the header only if needed (#1060)
* Update split.py

* Update split.py

* Update split.py

* Update split.py
2021-09-28 18:11:50 -04:00
estshorter 3da42fd1e8 Avoid min/max macro expansion on Windows (#1057) 2021-09-25 08:53:15 -04:00
yhirose 503aa61325 Fix problem with an empty parameter in set_base_dir 2021-09-20 17:40:05 -04:00
null e4c276d0c2 doc: fix typo in README (#1056)
fixed typo in README.md, replacing `Sutudio` with `Studio`.
2021-09-18 11:33:23 -04:00
yhirose e07f7691a8 Update README 2021-09-17 21:26:31 -04:00
yhirose 623ab4a96e Updated README regarding Visual Studio support 2021-09-17 11:36:08 -04:00
Zizheng Tai e1efa337a2 Make Client move-constructible (#1051) 2021-09-16 14:05:42 -04:00
Andrea Pappacoda 549cdf2f7d test: avoid infinite loop when IPV6 is unsupported (#1054) 2021-09-16 14:04:43 -04:00
yhirose 3c522386e9 Fix "Issue 38551 in oss-fuzz: cpp-httplib:server_fuzzer: Timeout in server_fuzze" 2021-09-12 19:24:48 -04:00
yhirose c202aa9ce9 Read buffer support. (Fix #1023) (#1046) 2021-09-12 00:26:02 -04:00
Andrea Pappacoda e3e28c6231 meson: add tests (#1044)
This integrates the "main" test suite (test/test.cc) in Meson.

This allows to run the tests in the CI with the Meson-built version of
the library to ensure that nothing breaks unexpectedly.

It also simplifies life of downstream packagers, that do not have to
write a custom build script to split the library and run tests but can
instead just let Meson do that for them.
2021-09-11 14:26:48 -04:00
yhirose 4e05368086 Fix #1054 2021-09-11 14:13:49 -04:00
yhirose e1afe74fe2 Fix #1037 2021-09-10 22:42:14 -04:00
yhirose 461acb02f5 Comment out SlowPostFail test for now 2021-09-10 22:37:31 -04:00
Gregor Jasny 415edc237c Set error variable for failed write_data (#1036) 2021-09-05 16:15:46 -04:00
Andrea Pappacoda e20ecd2574 Full Meson support (#1033)
* Full Meson support
cpp-httplib can be now built with Meson even in compiled library mode.

The library is built with LTO, supports OpenSSL, zlib and Brotli,
and the build system also generates a pkg-config file when needed.

Compared to the CMake file this one is quite small (more than five times
smaller!), and maintaining it won't be an issue :)

* meson: automatic versioning
2021-09-04 11:33:53 -04:00
yhirose ab477b5631 Fix "Issue 37742 in oss-fuzz: cpp-httplib:server_fuzzer: Timeout in server_fuzzer" 2021-09-02 22:57:57 -04:00
yhirose 0823d5c7f2 Fixed #1031 2021-08-30 17:16:31 -04:00
yhirose 1cc6930363 Append '_Online' suffix to Unit test names that access external servers 2021-08-23 13:02:19 -04:00
yhirose 4297500928 Fix #1024 2021-08-17 09:28:17 -04:00
Thomas BehnandThomas Behn a58f042614 Don't define INVALID_SOCKET if it has been defined already (i.e. by libpcap) (#1021)
Co-authored-by: Thomas Behn <Thomas.Behn@meinberg.de>
2021-08-10 08:29:27 -04:00
yhirose 469c6bc2b6 Fix #1017 2021-08-02 15:44:50 -04:00
Joel Rosdahl 887074efd2 Add test of httplib.h split into .h + .cc (#1015)
In order to test the split version (.h + .cc via split.py):

- Added a test_split program in the test directory whose main purpose is
  to verify that it works to compile and link the test case code against
  the split httplib.h version.
- Moved types needed for test cases to the “header part” of httplib.h.
  Also added forward declarations of functions needed by test cases.
- Added an include_httplib.cc file which is linked together with test.cc
  to verify that inline keywords have not been forgotten.

The changes to httplib.h just move code around (or add forward
declarations), with one exception: detail::split and
detail::process_client_socket have been converted to non-template
functions (taking an std::function instead of using a type parameter for
the function) and forward-declared instead. This avoids having to move
the templates to the “header part”.
2021-07-31 09:53:30 -04:00
Joel Rosdahl 9c2c15ca45 Add missing template method implementations (#1013)
When using the split version of httplib.h the templated implementation
of e.g. Client::set_connection_timeout ends up in httplib.cc and
therefore results in a linker error since the needed template
specialization has not been instantiated. Fix this by moving the
implementation of template methods into the part that ends up in
httplib.h after the split.

Fixes #1008.
2021-07-30 10:05:49 -04:00
Joel Rosdahl 1b3b098329 Avoid hardcoded ports in RedirectToDifferentPort.Redirect test (#1012)
The RedirectToDifferentPort.Redirect test assumes that port 8080 and
8081 are available on localhost. They aren’t on my system so the test
fails. Improve this by binding to available ports instead of hardcoded
ones.
2021-07-30 10:04:02 -04:00
Joel Rosdahl 6f7075e3aa Improve split script (#1011)
- Added shebang and made the script executable.
- Added help text.
- Added -o/--out argument for specifying output directory.
- Added -e/--extension argument for specifying file extension of the
  implementation file.
- Made the script find httplib.h next to split.py instead of the current
  working directory. This makes it possible to call the script from
  another directory.
- Simplified code structure slightly.
- Improved variable naming to follow Python conventions.
2021-07-30 10:03:05 -04:00
yhirose ccbddd8842 Allow LargeRandomData test only on Windows 2021-07-22 22:17:31 -04:00
yosh-matsuda 879dd261c2 Fix gzip compression/decompression over 4 GiB data size (#1002)
* Fix gzip compression/decompression over 4 GiB data size

* Add gzip test for large random data
2021-07-22 22:07:40 -04:00
xxrl 52f5eb5980 [Fix] ca_cert_path/ce_cert_store lose (#1004)
When redirect from http to https, user setting for ca_cert will lose

issue: #1003
2021-07-22 21:41:41 -04:00
Gregor Jasny ea2f69a0d7 Add httplib::Error to std::string function (#999)
Fixes: #978
2021-07-19 21:17:44 -04:00
Gregor Jasny 9f2064a8ed Fix remaining test warnings (#1001)
* Use portable way to encode ESC

'\e' is a GNU extension

* Use length specifier for size_t
2021-07-19 21:17:18 -04:00
yhirose e3750d9ddf Simplefied server APIs 2021-07-17 18:09:30 -04:00
yhirose c1eee3012e Fix #998 2021-07-17 17:18:56 -04:00
Gregor Jasny 6b08babbd2 Use googletest 1.11.0 (#1000)
* Update googletest to version 1.11.0

* Fix test warnings
2021-07-17 13:21:03 -04:00
yhirose 215b81342e Added a test case for #996 2021-07-15 08:24:06 -04:00
yhirose 06bfa7e08b Fix #979 2021-07-14 22:49:49 -04:00
yhirose 3d83cbb872 Improve string compare performance 2021-07-12 23:51:56 -04:00
yhirose 8a803b30f6 Fix #990 2021-07-12 23:46:25 -04:00
yhirose 80be649de7 Fix #961 2021-06-26 18:26:33 -04:00
yhirose 9648f950f5 Updated README 2021-06-18 08:45:50 -04:00
Gregor Jasny 6b9ffc8bec Remove dead code (#965) 2021-06-18 07:20:34 -04:00
Simon Edlund d903053faf Update httplib.h (#964)
operator""_ replaced by operator""_t
2021-06-17 10:57:25 -04:00
yhirose 676f1b5a26 Updated the user agent string 2021-06-14 08:43:12 -04:00
Baruch Nissenbaum b8dec12f15 Limit SSL_ERROR_WANT_READ retries to 1 sec (#957)
retry with 1ms delays to prevent CPU hoggin
2021-06-14 08:41:20 -04:00
yhirose fc9b223acc Updated copyright year 2021-06-11 14:45:35 -04:00
CncGpp ba824089d7 Fix code err code 401 when the password is empty in base_auth. (#958) 2021-06-11 14:39:33 -04:00
Andrea Pappacoda 1a2faf09e0 Add header-only Meson support (#955)
* Add header-only Meson support
This allows users to call `dependency('httplib')` and have the include
directory automatically configured

* Rename `httplib` to `cpp-httplib`
2021-06-05 16:45:00 -04:00
yhirose 5a43bb8149 Implemented #946 in a different way 2021-06-02 13:45:47 -04:00
yhirose 0104614656 Code refactoring 2021-06-02 08:11:31 -04:00
yhirose 77a77f6d2d Added set_default_headers on Server 2021-05-23 19:06:28 -04:00
Mathias Laurin 089b9daa1c Fix virtual call in ClientImpl::~ClientImpl() (#942)
* Fix virtual call in ClientImpl::~ClientImpl()

This fixes a warning in clang tidy:

> Call to virtual method 'ClientImpl::shutdown_ssl' during
> destruction bypasses virtual dispatch

ClientImpl::~ClientImpl() calls lock_socket_and_shutdown_and_close()
that itself calls shutdown_ssl().  However, shutdown_ssl() is virtual
and C++ does not perform virtual dispatch in destructors, which results
in the wrong overload being called.

This change adds a non-virtual shutdown_ssl_impl() function that is
called from ~SSLClient().  We also inline sock_socket_and_shutdown_and_close()
and removes the virtual call in ~ClientImpl().

* Inline and remove lock_socket_and_shutdown_and_close()

The function only has one caller.
2021-05-22 20:15:20 -04:00
yhirose ba34ea4ee8 Fix #944 2021-05-22 19:24:50 -04:00
Baruch Nissenbaum 2917b8a005 Explicit cast from size_t to uInt (#941)
* Explicit cast from size_t to uInt

* static_cast<uInt> instead of C style cast
2021-05-19 18:03:59 -04:00
Joseph Huang dcf24d45a2 fix ssesvr use of deleted function (#938) 2021-05-18 23:19:15 -04:00
yhirose 75fdb06696 Added a missing member in copy_settings. 2021-05-15 09:14:44 -04:00
Alex Hornung e00ad37580 Add option to bypass URL encode of path (#934) 2021-05-15 08:48:25 -04:00
Vincent Stumpf 5cfb70c2b4 Fix some shadowed variable warnings (#935) 2021-05-15 08:46:16 -04:00
Alessio Pollero 2a70c45697 Fix client.cc code, since res.error() without operator overloading… (#921)
* Fix client.cc code, since res.error() without operator overloading causing error in Xcode

* Add unit test to check new error to string with operator overloading

* Add inline as requested in code review comment
2021-05-01 13:29:23 -04:00
Aswin Raj Kharel c58b00580e reserving before encoding (#912) 2021-04-24 16:19:14 -04:00
Ken Schalk 7c60e69c33 Remove redunant call to close_socket (#911) 2021-04-23 17:07:19 -04:00
yhirose 33e94891ee Updated test.cc 2021-04-22 08:04:46 -04:00
yhirose 73e0729f63 Change sink.write() to return boolean 2021-04-22 07:14:08 -04:00
yhirose 21c529229c Fixed timeout issues 2021-04-22 07:14:08 -04:00
yhirose 63643e6386 Code format 2021-04-13 20:52:49 -04:00
yhirose 6cc2edce99 Added set_address_family 2021-04-13 20:49:52 -04:00
yhirose d122ff3ca8 Code formatting 2021-04-13 12:38:45 -04:00
James Young 14c6d526b4 Use newer version-flexible TLS/SSL method (#904) 2021-04-13 09:11:38 -04:00
Philipp Hasper 28e07bca16 Fixed minor code smells (#901) 2021-04-09 14:55:21 -04:00
yhirose faa5f1d802 Additional changes for #889 2021-04-05 16:13:41 -04:00
yhirose 9d3365df54 Fix #889 2021-04-05 11:40:53 -04:00
yhirose 6ff84d34d1 Another simpler implementation of #890 (#891) 2021-04-02 18:25:04 -04:00
yhirose b845425cd0 Fix #878 2021-03-16 19:42:44 -04:00
yhirose 89519c88e2 Fix #874 2021-03-10 15:57:56 -05:00
yhirose ff813bf99d Fix #863 2021-02-17 15:36:56 -05:00
yhirose cf475bcb50 Fix #860 2021-02-12 12:21:43 -05:00
yhirose bc80d7c789 Fixed ClientStop test problem 2021-02-06 20:12:30 -05:00
yhirose b7566f6961 Resolve #852 2021-02-02 22:09:35 -05:00
Nikolas 0542fdb8e4 Add exception handler (#845)
* Add exception handler

* revert content reader changes

* Add test for and fix exception handler

* Fix warning in test

* Readd exception test, improve readme note, don't rethrow errors, remove exception handler response
2021-01-28 17:19:11 -05:00
yhirose 78c474c744 Update README 2021-01-27 11:59:42 -05:00
yhirose 88411a1f52 Fix #846 2021-01-27 14:35:32 +00:00
yhirose ae6cf70bc4 Updated README 2021-01-26 08:38:28 -05:00
yhirose 68d1281759 Resolve #839 2021-01-23 13:33:27 -05:00
yhirose 0308d60cb2 Resolve #831 (#835) 2021-01-23 12:23:06 -05:00
yhirose 59f5fdbb33 Resolve #840 2021-01-23 11:43:01 -05:00
Anonymous 13184f5f80 Return Server& from handler setters (#836)
* Update httplib.h

* Update httplib.h

* Update httplib.h

* Update httplib.h
2021-01-22 09:20:21 -05:00
Eric Lee 8d9a477edb No content check (#823)
* No content check

* unit test for no content

* fixing merge conflict break

* oops during manual merge conflict
2021-01-13 17:51:40 -05:00
yhirose 85b4abbf16 Updated the simple example 2021-01-07 19:56:33 -05:00
yhirose e42a358da8 Updated README 2021-01-07 19:00:24 -05:00
yhirose f008fe4539 Added middleware support (#816) 2021-01-07 18:40:52 -05:00
yhirose ddf41d29ef Added const *char and size_t interface 2021-01-06 22:39:58 -05:00
yhirose 3f88a46c4a Code format 2020-12-31 18:14:28 -05:00
yhirose 242706ea34 Fix #820 2020-12-31 18:14:05 -05:00
yhirose a9f5f8683f Fixed warnings on Visual C++ 2020-12-31 11:35:11 -05:00
yhirose 60c2213893 Fix #817 2020-12-31 10:58:44 -05:00
yhirose eb2d28bca2 Code cleanup 2020-12-31 10:37:06 -05:00
Yuri Santos 86f637a246 Added encode_uri_param tests with UTF-8 characters (#818)
Remove additional lines between tests
2020-12-30 23:06:36 -05:00
yhirose 2c07ec4600 Code cleanup 2020-12-29 09:39:19 -05:00
yhirose 871d8d67b0 Made Request paramater const in handle_file_request 2020-12-28 22:03:44 -05:00
Anonymous 7299713195 Fix readme Response::set_chunked_content_provider (#811)
`Response::set_chunked_content_provider` has formal parameters, not one.
2020-12-25 19:55:57 -05:00
yhirose 96afa7e108 Updated README 2020-12-21 13:40:32 -05:00
yhirose 55f57af0b9 Update README 2020-12-21 09:27:36 -05:00
yhirose 6b35cd0116 Updated README 2020-12-21 08:15:06 -05:00
yhirose 99f2229e48 Updated README 2020-12-19 22:43:31 -05:00
yhirose b9641048fc Switch to hghttp2.org/httpbin for redirect test. (#538) 2020-12-19 20:27:52 -05:00
yhirose e9c6c6e609 Code format 2020-12-19 20:14:53 -05:00
yhirose 40db42108f Fixed problem with invalid requests including spaces in URL path 2020-12-19 12:03:08 -05:00
Anonymous 24bb1387d6 Update README.md (#806) 2020-12-19 11:12:44 -05:00
Jeremie Rahm d0bd4afb0b Ensure socket is closed after processing in SSLServer (#804) 2020-12-18 19:29:36 -05:00
Yuri Santos 78ea786abd [PR] Special function to encode query params (#801)
* Special function to encode query params

* Fix #include <iomanip>

* Added unescaped charsets to encode_query_param

* Unit tests for encode_query_param
2020-12-18 17:51:11 -05:00
Miosame 9cac2c9ceb typo: specitic => specific (#802) 2020-12-18 15:12:21 -05:00
Anonymous 0cff3245df Extend built-in extension MIME mapping (#799)
* Update README.md

* Update httplib.h

* Update httplib.h

* Update httplib.h

* Update httplib.h

* Remove duplicate cases

Someone left a bunch of duplicate cases, idiot, couldn't have been me.

* Reformat

Modify spacing and whatnot

* Update README.md
2020-12-18 09:32:19 -05:00
yhirose 0e3925db3f Fixed build error 2020-12-18 00:07:48 +00:00
yhirose c9a13d214b Changed not to use string_view 2020-12-17 18:48:27 -05:00
yhirose 0954af2d4c Use user-defined literals for file extention match 2020-12-17 18:27:04 -05:00
yhirose 7c1c952f5a Don't allow invalid status code format (It sould be a three-digit code.) 2020-12-15 20:25:24 -05:00
yhirose a6edfc730a Added a unit test for static file with range 2020-12-15 18:47:51 -05:00
yhirose c1264bfedc Fix problem with mp4 w/ Range header 2020-12-14 22:41:05 -05:00
yhirose 90a5b6ceb0 Updated README 2020-12-04 19:39:39 -05:00
yhirose eb240ad2e5 Code cleanup 2020-12-03 16:03:12 -05:00
yhirose 88c961f37e Removed std::atomic<Error> error_ 2020-12-01 15:17:34 +00:00
yhirose b952376968 Fixed warning 2020-12-01 03:50:55 +00:00
yhirose 5dd605d3a2 Fix #762 2020-11-30 21:49:31 -05:00
Seunghwan Hong 9c0c98b1ed Add keep_alive_timeout guide on README.md (#778) 2020-11-30 07:11:22 -05:00
yhirose 615867322d Fixed build errors and apply clangformat 2020-11-29 12:29:18 -05:00
David Wu 02d3cd5909 Fix multiple threading bugs including #699 and #697 2020-11-29 12:29:15 -05:00
yhirose 47e5af15ea Updated README 2020-11-22 09:22:40 -05:00
yhirose a5c239c174 Fix #765 2020-11-21 16:35:31 -05:00
yhirose c2afc5ca44 Added chunked content provider support on client 2020-11-21 08:46:50 -05:00
yhirose cee062d4c9 Fixed unit tests due to the change in #763 2020-11-19 21:04:46 -05:00
Unkorunk b21dc8cbe0 Fix incorrect content_encoding for Brotli (#763) 2020-11-19 12:39:20 -05:00
372046933andtaoxu e1133a2dcb std::tolower is undefined if the argument's value is neither representable as unsigned char nor equal to EOF (#761)
Co-authored-by: taoxu <taoxu@bilibili.com>
2020-11-19 09:21:40 -05:00
yhirose e273fec93c Fixed Visual Studio setup 2020-11-19 09:17:59 -05:00
yhirose 95d0b073bd Fix #754 2020-11-15 08:13:08 -05:00
vawenandjigarcia@vaxtor.es 9c7d841b37 Fix: #746 Compile error under VS2015 (#747)
Co-authored-by: jigarcia@vaxtor.es <jigarcia@vaxtor.es>
2020-11-10 08:41:53 -05:00
yhirose f086bf5310 Fix #738 2020-11-08 18:16:23 -05:00
yhirose 6613d7b7ad Fixed warnings 2020-11-08 17:59:11 -05:00
yhirose 6adf130bf3 Fix #739 2020-11-07 21:54:47 -05:00
miketstsandMichael Tseitlin b6b2eaf5bc Add unit test SSLConnectTimeout (#741)
Add unit test for issue #682 fixed in PR #728, which does not contain
the test of its own.

The test creates a fake SSL server, inherited from SSLServer, which
does not create an SSL context. When an SSL client attempts to send it
a request, it gets a timeout error. Prior to PR #728, the client would
wait indefinitely

Co-authored-by: Michael Tseitlin <michael.tseitlin@concertio.com>
2020-11-07 09:41:20 -05:00
yhirose eb4b7c70a9 Fix #737 2020-11-07 09:33:22 -05:00
yhirose 84661ea6ed Refactoring 2020-11-05 07:06:53 -05:00
yhirose 041122908c Fix problem with invalid range 2020-11-05 00:20:59 -05:00
yhirose 401de608df Fixed debug option problem. 2020-11-05 00:20:59 -05:00
yhirose 726c64cf10 Code format 2020-11-05 00:20:59 -05:00
Omkar Jadhav e1f781a21a [oss-fuzz] Enable msan for fuzz tests (#734)
* Disable openssl as we're not using it in fuzz tests

* Increase timeout in ClientStop test
2020-11-04 11:30:56 -05:00
Omkar Jadhav 72b81badad Fix issues reported by oss-fuzz (#729)
* Fix oss-fuzz issue #26529

* Add test for oss-fuzz issue  #26598

* Fix oss-fuzz issue  #26632

* Revert change and add new test cases
2020-11-03 11:36:02 -05:00
yhirose 17428a8fbf Fixed warning 2020-11-03 09:16:28 -05:00
Daniel OttigerandDaniel Ottiger 6e1879dfae ssl-verify-host: fix verifying ip addresses containing zero's (#732)
* ssl-verify-host: fix verifying ip addresses containing zero's

If the subject alternate name contained an ip address with an zero
(like 10.42.0.1) it could not successfully verify.
It is because in c++ strings are null-terminated
and therefore strlen(name) would return a wrong result.
As I can not see why we can not trust the length returned by openssl,
lets drop this check.

* ssl-verify-host: add test case

lets try to validate against 127.0.0.1

Co-authored-by: Daniel Ottiger <daniel.ottiger@ch.schindler.com>
2020-11-02 20:27:34 -05:00
miketstsandMichael Tseitlin eb1d2e04bc SSL_connect and SSL_accept in non-blocking mode (#728)
SSL connection is performed in two steps:
First, a regular socket connection is established.
Then, SSL_connect/SSL_accept is called to establish SSL handshake.

If a network problem occurs during the second stage, SSL_connect on
the client may hang indefinitely.

The non-blocking mode solves this problem.

Co-authored-by: Michael Tseitlin <michael.tseitlin@concertio.com>
2020-11-02 17:05:08 -05:00
yhirose c909ffa758 Fix #731 2020-11-01 21:03:47 -05:00
yhirose ff5677ad19 Updated README 2020-10-27 20:35:56 -04:00
yhirose 8b1b31ac20 Fix #723 2020-10-27 20:32:19 -04:00
yhirose 953600c177 Fixed compiler error for old compiler. 2020-10-27 12:23:37 -04:00
yhirose 536e7eb7f2 Revert "Fix #697". (It broke unit test...)
This reverts commit 6d66721ba1.
2020-10-25 20:22:39 -04:00
yhirose 6d66721ba1 Fix #697 2020-10-25 16:55:54 -04:00
yhirose 3b29cd0bdc Fix #698 2020-10-25 12:14:54 -04:00
yhirose 109b624dfe Fix #708 (#713)
* Fix #708

* Rename ContentReceiver2 to ContentReceiverWithProgress
2020-10-22 11:48:43 -04:00
yhirose 0ed70c4d9f Fixed unit test errors 2020-10-21 14:34:27 -04:00
yhirose a50b7591ca Fix #714 2020-10-21 13:02:33 -04:00
yhirose bf8fc11b53 Code cleanup 2020-10-20 21:24:47 -04:00
Omkar Jadhav bc4a613b6d Fix suffix-byte-range issue (#711) 2020-10-20 11:11:27 -04:00
yhirose 4bb001351c Fix #705 2020-10-19 22:13:24 -04:00
yhirose e155ba44bb Fix #706 2020-10-19 15:23:35 -04:00
Muchamad Arifin Dwi P a4a9637738 Fix #700 null pointer exception (#702) 2020-10-16 20:44:14 -04:00
Omkar Jadhavandyhirose 5292142046 Add cpp-httplib to oss-fuzz (#684)
* *Add server fuzzer target  and seed corpus
* Add fuzz_test option to Makefile

* Fix #685

* Try to fix Github actions on Ubuntu

* Added ReadTimeoutSSL test

* Comment out `-fsanitize=address`

* Rebase upstream changes

* remove address sanitizer temporarily

* Add separate Makefile for fuzzing

* 1. Remove special char from dictionary
2. Clean fuzzing/Makefile

* Use specific path to avoid accidently linking openssl version brought in by oss-fuzz

* remove addition of flags

* Refactor Makefile

* Add missing newline

* Add fuzztest to github workflow

* Fix

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2020-10-15 08:11:40 -04:00
Snape3058andElla cc5147ad72 Replace shared_ptr with unique_ptr for better performance (#695)
* Backport std::make_unique from C++14.

* Replace shared_ptr with unique_ptr for better performance.

Co-authored-by: Ella <maxutong16@otcaix.iscas.ac.cn>
2020-10-15 08:09:11 -04:00
Andrew Gasparovic fffbf1a669 Use move semantics instead of copy for functions (#692)
* Use move semantics instead of copy for functions

In some cases, a few more copies could be prevented by changing function definitions to accept parameters by const-ref, rather than by value, but I didn't want to change public signatures.

* Fix two use-after-move errors
2020-10-11 19:00:36 -04:00
lightvector d37bc0fb4d Allow client to specify boundary and use more entropy by default (#691) (#694) 2020-10-11 15:34:54 -04:00
Andrew Gasparovic 6d60dc8839 Add cache_control parameter to set_mount_point (#688)
* Add `cache_control` parameter to `set_mount_point`

Specifies the Cache-Control header value to return when specified. For example:

```
svr.set_mount_point("/assets", "public/assets", "public, max-age=604800, immutable");
```

* Add default for cache_control

Default to "no-cache", which is implicitly what is happening today.

* Change set_mount_point to accept Headers

* Don't use C++17 destructuring
2020-10-10 20:46:08 -04:00
Wang Gao b713a3a651 fix MSVC2015 error: std::tolower to ::lower (#689) 2020-10-10 12:02:50 -04:00
yhirose 7e8db1dc68 Comment out -fsanitize=address 2020-10-08 23:14:53 -04:00
yhirose 316add860b Added ReadTimeoutSSL test 2020-10-08 22:55:09 -04:00
yhirose 79ce6f1745 Try to fix Github actions on Ubuntu 2020-10-08 21:49:47 -04:00
yhirose 09fdf4eacd Fix #685 2020-10-08 21:37:42 -04:00
Omkar Jadhavandyhirose 143b2dd15a Fix memory leak due caused due to X509_STORE (#671)
* Fix memory leak due caused due to X509_STORE

* Add test for repro and address sanitizer to compiler flags

* Add comment

* Sync

* Associate ca_store with ssl context within set_ca_cert_store()

* Split SlowPost test

* Fix #674

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2020-10-02 13:17:37 -04:00
yhirose e2c4e9d95c Fix #674 2020-09-29 19:22:28 -04:00
yhirose d87082f04b Split SlowPost test 2020-09-29 19:17:34 -04:00
yhirose cc14855ba0 Fix #661 2020-09-26 04:50:09 -04:00
yhirose 56c418745f Fixed conction close problem with HTTP 1.0 client 2020-09-25 20:58:49 -04:00
yhirose 4ce9911837 Add <sstream> 2020-09-25 18:17:32 -04:00
yhirose 559c407552 Adjusted SlowRequest test 2020-09-25 18:13:10 -04:00
yhirose a2f4e29a7b Add set_keep_alive_timeout 2020-09-25 17:57:33 -04:00
mi01 b8cf739d27 Add cctype header (#656) 2020-09-16 16:32:49 -04:00
yhirose aec2f9521d Updated documentation 2020-09-15 10:11:46 -04:00
yhirose 7b55ecdc59 Fixed #650 2020-09-12 16:11:14 -04:00
tmahring e9575bcb78 don't replace plus with space in headers (#649)
* don't replace plus with space in headers

* fixed forward handling with changed header parsing

* add test for boundaries containing plus chars
2020-09-10 20:27:01 -04:00
Jonas MinnbergandJonas Minnberg 308aeb187b Undefined if2ip() also on Android since getifaddrs() does not exist. (#648)
Co-authored-by: Jonas Minnberg <jonas@minnberg.se>
2020-09-10 07:52:01 -04:00
Jodi the Tigger 05d18f2bc5 Fix -D build flags containing escaped quotes (#645)
Fixes #638

Done by removed unneeded quotes from cmake's add_compiler_definitions()
2020-09-08 21:35:50 -04:00
Ivan Fefer 3da4a0ac69 Add compression buffer size customization (#644)
* add compression buffer size customization and small brotli refactor

* allocat brotli buffer once

* add init to brotli decoder buffer
2020-09-08 12:18:14 -04:00
yhirose 9d12b3f20e Fixed warnings and refactoring 2020-09-03 20:33:30 -04:00
852a374748 Fix server crash caused due to regex complexity while matching headers. (#632)
* Fix parsing to parse query string with single space char.

When passed ' ' as a query string, the server crashes cause of illegal memory access done in httplib::detail::split. Have added checks to make sure the split function has a valid string with length > 0.

* Fix parsing to parse query string with single space char.

* Fix server crash caused due to regex complexity while matching headers.

While parsing content-type header in multipart form request the server crashes due to the exhaustion of max iterations performed while matching the input string with content-type regex.
Have removed the regex which might use backtracking while matching and replaced it with manual string processing. Have added tests as well.

* Remove magic number

Co-authored-by: Ivan Fefer <fefer.ivan@gmail.com>

Co-authored-by: yhirose <yhirose@users.noreply.github.com>
Co-authored-by: Ivan Fefer <fefer.ivan@gmail.com>
2020-09-03 13:17:52 -04:00
Ivan Fefer 3b5bab3308 Fix gzip_decompressor in case of one chunk being exactly equal to buffer size (#636)
* add larget chunks test

* revert test

* Fix gzip decoder in case of chunk being equal to buffer size

* add test
2020-09-03 12:20:02 -04:00
yhirose 69e75f4a67 Fix #635. HTTPS request stucked with proxy (#637) 2020-09-03 12:17:53 -04:00
Omkar Jadhav b0fd4befb1 Fix query parsing issues (#629)
* Fix parsing to parse query string with single space char.

When passed ' ' as a query string, the server crashes cause of illegal memory access done in httplib::detail::split. Have added checks to make sure the split function has a valid string with length > 0.

* Fix parsing to parse query string with single space char.
2020-08-28 09:43:28 -04:00
yhirose 3e80666a74 Fix #628 2020-08-27 19:45:28 -04:00
yhirose 3e4567bae8 Updated Repl.it examples 2020-08-27 14:16:35 -04:00
yhirose db075d8cf9 Added Repl.it examples 2020-08-27 14:06:57 -04:00
yhirose 16df0ef37e Code cleanup 2020-08-26 12:18:49 -04:00
Ivan Fefer f1a2ac5108 Avoid copying of content provider if possible (#627) 2020-08-26 08:56:51 -04:00
yhirose e5743b358d Updated README 2020-08-25 20:26:53 -04:00
Ivan Fefer 1184bbe4cb Fix typo in README (#625) 2020-08-25 06:38:16 -04:00
Felix Kästner 9dcffda7ae Fix cmake execute_process working directory (#622)
Correct the working directory of the execute_process call, to get the version from git tags.
The working directory should be the the directory of this libary. Otherwise, if you include httplib
as a git submodule and call add_subdirectory, the execute_process command will be run in a wrong
directory leading to error.
2020-08-24 17:23:01 -04:00
yhirose e5903635e2 Fix #619 2020-08-22 12:54:43 -04:00
yhirose 510b4eaaae Fix #613 2020-08-17 13:40:06 -04:00
yhirose d7e63b4316 Update README 2020-08-16 20:49:54 -04:00
yhirose e5dd410256 Added set_content_provider without content length 2020-08-15 05:53:49 -04:00
yhirose 951e46929e Fix #609 2020-08-14 15:01:09 -04:00
yhirose c8adac30f4 Fix #564 again 2020-08-13 08:07:25 -04:00
yhirose b2b4f7635f Fix #608 2020-08-11 12:11:05 -04:00
R Edgar 649b1d2172 Fix nullptr_t issue (#605)
Clang complains that `nullptr_t` should be `std::nullptr_t
2020-08-09 16:45:53 -04:00
yhirose dc5f9ba164 Better error handling on client (#601) 2020-08-08 20:50:24 -04:00
yhirose cf084e1db1 Fixed example build errors 2020-08-08 00:10:08 -04:00
KTGH e0e5898601 Overhaul FindBrotli to fix weird issues (#604)
Should get rid of the issue about not being able to create an ALIAS on
MinGW, as well as the "No REQUIRED_VARS" issue.

Fixes #603 (hopefully)
2020-08-06 07:08:29 -04:00
yhirose dfec2de5b5 Update README 2020-08-03 23:37:05 -04:00
yhirose 04002d57bd Added set_default_headers (Fix #600) 2020-08-03 22:05:37 -04:00
yhirose 38a7706c8b Removed old Keep-Alive functions 2020-08-03 22:04:40 -04:00
KTGH abaf875c42 Fix FindBrotli when no Brotli installed (#598)
Woops.

Ref https://github.com/yhirose/cpp-httplib/issues/582#issuecomment-667537002
2020-08-01 17:08:49 -04:00
PixlRainbow 5f76cb01c7 fix #592 -- add check for static-linked OpenSSL (#595) 2020-08-01 08:10:42 -04:00
yhirose ae54e833ea Code cleanup 2020-07-31 23:48:42 -04:00
yhirose 0dd3659de5 Updated README 2020-07-31 18:54:53 -04:00
KTGH 999f6abd2c Fix for Cmake on systems without Git (#594)
If you didn't have Git installed, execute_process never declared the
error variable, which led to it never parsing the header for a version.
So if Git wasn't installed, the version variable never got declared,
which caused errors.

This fixes that.
2020-07-31 13:46:12 -04:00
KTGH 48da75dd35 Fix FindBrotli for static libs (#593)
It wasn't linking them.
2020-07-31 13:45:21 -04:00
yhirose 4f84eeb298 Bearer Token auth support. Fix #484 2020-07-31 12:37:14 -04:00
yhirose a5b4cfadb9 Brotli suport on server. Fix #578 2020-07-31 10:23:57 -04:00
yhirose 3e906a9b8c Fix #591 2020-07-30 18:26:18 -04:00
yhirose 110393eadb Class name change 2020-07-30 17:27:07 -04:00
yhirose 98caa8c058 Updated README 2020-07-30 17:21:49 -04:00
yhirose ef65f09608 OpenSSL support on Visual Studio project 2020-07-30 17:19:12 -04:00
ThePiso e130cf3a3b The piso patch 1 (#590)
* Update httplib.h

When you disconnect and reconnect from the network, your network stack rewrites and updates /etc/resolv.conf accordingly. This configuration file is needed by the DNS resolver in the C library. The C library reads the DNS configuration from /etc/resolv.conf the first time, and caches it. It doesn't check, with every lookup, if the contents of /etc/resolv.conf have changed.
the solution is to add a call to res_init(), defined in resolv.h

* Update httplib.h
2020-07-30 10:11:02 -04:00
yhirose 8a348f17fd Resolved #192 2020-07-30 01:47:54 -04:00
yhirose 797d1f27e8 Fix #357 2020-07-29 23:12:05 -04:00
yhirose 6cde600922 Simplified simplecli.cc 2020-07-29 16:04:28 -04:00
KTGH 342c3ab293 Add Brotli Cmake support (#584)
Had to create a custom FindBrotli package, as not all users have
PkgConfig installed (which Brotli uses). This file gets installed
alongside httplibConfig.cmake for the end-users convenience.

Set BROTLI_USE_STATIC_LIBS to ON if you want to find the static libs
instead of default shared.

Adds the HTTPLIB_REQUIRE_BROTLI (default off) and HTTPLIB_USE_BROTLI_IF_AVAILABLE
(default on) options, which work in the same manner as the other optional/required
dependency options.

Moved the scattered linking and definitions to a single call.

Updated some documentation about the new options.

Improved the in-tree support by setting the HTTPLIB_IS_USING_XYZ
variables in the main CMakeLists (as well as having them in the
httplibConfig.cmake file).

Fixes #582
2020-07-28 17:04:29 -04:00
yhirose 6cce7951fc Fixed build error on non Windows environments with OpenSSL 2020-07-27 22:34:35 -04:00
yhirose e9058e5639 Fixed build error on Windows with OpenSSL 2020-07-27 22:32:31 -04:00
yhirose 2538a85486 Fix #581 2020-07-27 22:07:04 -04:00
KTGH 8c501022b3 Fix Cmake build for MinGW (#580)
Seems certain targets/hosts failed without these, as "_MSC_VER" is
undefined on MinGW, which caused the 'pragma comment(lib "libname")' to
fail.

Fixes #575
2020-07-26 12:27:03 -04:00
yhirose 9f5db2d1aa Updated README 2020-07-25 20:53:38 -04:00
yhirose 12540fe8d3 Brotli support on client 2020-07-25 20:44:02 -04:00
yhirose 29a06f852a Update README 2020-07-25 11:24:06 -04:00
yhirose 0e9cfd9f49 SSE client example 2020-07-25 11:20:57 -04:00
yhirose 90da199aba Disable compression when content-type is text/event-stream 2020-07-25 10:46:52 -04:00
yhirose 366d073490 Fixed build errors 2020-07-25 09:40:35 -04:00
yhirose 9ca1fa8b18 Fix #576 2020-07-25 09:37:57 -04:00
yhirose 15c4106a36 Added a unit test 2020-07-22 08:07:59 -04:00
yhirose 72ce293fed Removed set_timeout_sec and left set_base_dir 2020-07-20 17:15:16 -04:00
yhirose b476b55771 Fix #557 2020-07-20 17:04:50 -04:00
yhirose 0db9d21eb0 Fix #571 2020-07-19 18:40:55 -04:00
yhirose 5ddaf949d0 Fixed build error on Windows 2020-07-19 18:32:28 -04:00
yhirose 457a5a7501 Added compressor class 2020-07-19 17:44:45 -04:00
Daniel Ottiger 2ce080c2cb include <wincrypt.h> as otherwise CertOpenSystemStoreW can not be found (#568)
- visual studio 2019, version 16.6.3
- 64 bit target
2020-07-15 10:17:18 -04:00
yhirose 6ad25b6cf0 Fix #566 2020-07-12 20:41:02 -04:00
yhirose 3dff60eb16 Fix #565 2020-07-10 08:18:28 -04:00
yhirose 5038314b21 Fix #564 2020-07-08 13:56:06 -04:00
yhirose 6e1297cab0 Fix #150 (#556) 2020-07-07 18:55:46 -04:00
yhirose 7de743c962 Code format 2020-07-04 00:11:32 -04:00
UmiadeandUmiade 964fb5e5ca Fix: regex can't match when proxy was set to some web debugger(e.g. Fiddler) (#553)
Co-authored-by: Umiade <hanyuchao@corp.netease.com>
2020-07-03 07:17:04 -04:00
yhirose c4f3f9529b Fix #534 (#546) 2020-07-02 21:57:50 -04:00
Ilya Tsybulsky 887def9490 Fix logger never called when write_content_with_provider returns false (#549) 2020-07-01 17:09:43 -04:00
Ilya Tsybulsky bad6b2d22f fix-the-code-won't compile-with-sdl-checks-on (#550) 2020-07-01 17:09:19 -04:00
rundong08 3d47a51430 Fixed comparison of integers of different signs. (#544) 2020-06-29 21:19:56 -04:00
Ron Klein 0a2cb20223 fix documentation typo (#539)
fix "adress" --> "address"
2020-06-22 18:12:22 -04:00
yhirose ce502a73e1 Fix #531 2020-06-22 14:56:18 -04:00
yhirose 010e4479f4 Fixed test errors due to httpbin.org 2020-06-22 14:53:20 -04:00
Ahmet Karaahmetoğlu 70e193374a Fix #530 (#535) 2020-06-21 15:08:40 -04:00
yhirose 6b22409217 Code format 2020-06-18 23:33:07 -04:00
yhirose 969cccd52a Use && for parameter of boundary 2020-06-18 23:32:09 -04:00
yhirose 4a9c048bbc Fixed problem with set_socket_options 2020-06-18 23:31:41 -04:00
yhirose bfabbec8c7 Fix #528 2020-06-18 12:20:01 -04:00
yhirose 3e9c06cf79 Fixed #527 2020-06-18 12:18:43 -04:00
yhirose 29677540ae Removed unnecessary yeid. 2020-06-16 21:33:10 -04:00
yhirose 71fcfeb912 Removed unnecessary code 2020-06-16 21:21:03 -04:00
yhirose c7d22e451f Fixed timeout calculation bugs 2020-06-16 21:20:47 -04:00
yhirose 42f9f9107f Updated version in the User Agent string 2020-06-16 17:53:15 -04:00
yhirose 7cd25fbd63 Fix #499 2020-06-16 17:46:23 -04:00
yhirose 3dfb4ecac2 Fix #522 2020-06-15 23:09:46 -04:00
yhirose 144114f316 Fixed warnings on Windows 2020-06-13 23:20:21 -04:00
yhirose 0cc108d45e Updated ClientStop test 2020-06-13 23:18:59 -04:00
yhirose 0743d78c9b Fixed ClientStop test error. 2020-06-14 03:01:41 +00:00
yhirose e022b8b80b Refactoring to make it ready for KeepAlive connection on Client 2020-06-13 21:42:23 -04:00
yhirose 34282c79a9 Changd thread count in ClientStop 2020-06-13 01:45:08 -04:00
yhirose f80b6bd980 Added Endpoint structure in Client 2020-06-13 01:26:57 -04:00
yhirose 5af7222217 Fixed Client::stop problem with more than one requests on threads 2020-06-12 11:04:37 -04:00
KTGH ec00fe5d5b Use git to get full project version (#519)
This gets us the full version (aka with the patch version), instead of
just major and minor version from user agent.

Falls back to the user agent if it fails.
2020-06-10 18:23:45 -04:00
yhirose 24bdb736f0 Fix #506 2020-06-09 19:58:01 -04:00
yhirose d0dc200633 Code format 2020-06-09 19:17:58 -04:00
Nicolas Schneider 919a51091f replace usage of [[deprecated]] with CPPHTTPLIB_DEPRECATED (#513) 2020-06-03 13:12:31 -04:00
Nicolas Schneider 05e8b22989 fix cast warning (#512) 2020-06-03 07:44:16 -04:00
Nicolas Schneider 00dcd6b004 check for [[deprecated]] support via feature test macro (#511)
The [[deprecated]] specifier is a C++14 feature, so it might not always
be available on a C++11 compiler.
2020-06-03 07:43:56 -04:00
yhirose a42c6b99d3 Code cleanup 2020-06-02 19:06:16 -04:00
Wang Gao 812cb5bc3d fix get value function (#509) 2020-06-02 19:05:04 -04:00
yhirose aea60feb85 Code cleanup 2020-06-01 13:22:02 -04:00
yhirose b3a4045300 Fix #503 2020-05-28 19:19:18 -04:00
KTGH 5fcd8f7795 Add automatic versioning to Cmake (#505)
It pulls the version from the user-agent string in the header, so it
will not need to be manually adjusted. This version file is installed so
that you can check for a specific version with find_package(httplib)

Also added HTTPLIB_INCLUDE_DIR (root path without header name), and
HTTPLIB_LIBRARY (only if compiled).

Added HTTPLIB_VERSION, and HTTPLIB_FOUND (although it's recommended
to check if the target exists).

Updated CMakeLists documentation for all this.
2020-05-28 17:09:20 -04:00
yhirose d9fe3fa020 Fix #504 2020-05-28 17:08:05 -04:00
yhirose d8612ac02d Fixed build error... 2020-05-28 12:51:52 -04:00
yhirose 83ee6007da Fix #500 2020-05-28 12:06:11 -04:00
yhirose 3eaa769a2d Fix #481, #483, #487 2020-05-26 18:34:32 -04:00
yhirose b91540514d Fix #494 2020-05-25 10:50:24 -04:00
yhirose ab563ff52c Fix #496 2020-05-25 10:38:47 -04:00
KTGH 8cad160c0a Add HTTPLIB_COMPILE option to Cmake (#493)
This option (default OFF) automatically splits the file (with split.py)
into a header & source file, then compiles it as a shared/static
library. This requires an installed Python v3 executable to work.

This also adds a HTTPLIB_IS_COMPILED boolean that's available after a
finfind_package(httplib) call.

Note that the minimum Cmake version increased to 3.12 because of FindPython3.
Hopefully this isn't a problem, as it's already 3 years old at this point.
2020-05-24 16:07:44 -04:00
yhirose be7962f140 Fix #489 2020-05-24 15:18:34 -04:00
yhirose 509b8570b0 Updated README 2020-05-23 19:08:17 -04:00
yhirose 630f3465a9 Deprecated set_timeout_sec, added set_connection_timeout. 2020-05-23 18:00:24 -04:00
yhirose 9af1a4a08f Fixed problem with stop on windows 2020-05-23 13:49:49 -04:00
yhirose 0654e5dab4 Changed CPPHTTPLIB_IDLE_INTERVAL_USECOND to 0 2020-05-23 08:44:03 -04:00
yhirose 62e036f253 Fixed #488 again 2020-05-22 18:24:01 -04:00
yhirose f0adfb2e0c Fix #488 2020-05-22 12:18:07 -04:00
yhirose 139c816c16 Fixed the location of Client2 2020-05-19 21:02:58 -04:00
KTGH 9505a76491 Bringing Cmake back (#470)
* Revert "Removed CMakeLists.txt. (Fix #421)"

This reverts commit 8674555b88.

* Fail if cmake version too old

Previous behaviour is just a warning.

* Improve CMakeLists

Adds automatic dependency finding (if they were used).
Adds a way to require a specific version in the find_package(httplib) call.

You should link against the httplib::httplib IMPORTED target, which is
created automatically.

Add options to allow for strictly requiring OpenSSL/ZLIB

HTTPLIB_REQUIRE_OPENSSL & HTTPLIB_REQUIRE_ZLIB require the libs be found, or the build fails.

HTTPLIB_USE_OPENSSL_IF_AVAILABLE & HTTPLIB_USE_ZLIB_IF_AVAILABLE silently search for the libs.
If they aren't found, the build still continues, but shuts off support for those features.

* Add documentation to CMakeLists.txt

Has info on all the available options and what targets are produced.

Also put some things about installation on certain platforms.
2020-05-19 19:07:18 -04:00
yhirose 29fd136afd Code cleanup and format 2020-05-16 17:35:04 -04:00
yhirose f5598237b2 Fixed many redirects problem on Proxy 2020-05-16 17:34:03 -04:00
Daniel Ottiger 01058659ab make write timeout configurable (like the read timeout already is) (#477)
In case we want to send a lot of data,
and the receiver is slower than the sender.

This will first fill up the receivers queues and after this
eventually also the senders queues,
until the socket is temporarily unable to accept more data to send.

select_write is done with an timeout of zero,
which makes the select call used always return immediately:
(see http://man7.org/linux/man-pages/man2/select.2.html)

This means that every marginal unavailability will make it return false
for is_writable and therefore httplib will immediately abort the transfer.

Therefore make this values configurable in the same way
as the read timeout already is.

Set the default write timeout to 5 seconds,
the same default value used for the read timeout.
2020-05-16 17:31:46 -04:00
yhirose 66f698fab6 Fixed build errors with some examples 2020-05-16 00:50:52 -04:00
yhirose b9a9df4d73 Fixed problem with writing large data 2020-05-15 22:21:58 -04:00
yhirose 25aa3ca982 Added std::ostream os in DataSink. 2020-05-15 21:26:13 -04:00
yhirose 2d67211183 Added more unit tests for the simple interface 2020-05-14 18:25:18 -04:00
yhirose f4c5d94d74 Updated version in the User Agent string 2020-05-14 18:07:02 -04:00
yhirose 63a96aeb20 Improved Client2 interface 2020-05-14 12:51:34 -04:00
yhirose bbb83d12c1 Removed default parameter values in Client and SSLClient constructors 2020-05-14 08:51:32 -04:00
yhirose 2d4b42b70b Removed url 2020-05-14 01:43:06 -04:00
yhirose 1919d08f71 Added Client2 2020-05-14 01:36:56 -04:00
yhirose 824c02fcd3 Code cleanup 2020-05-14 01:08:36 -04:00
yhirose 2c0613f211 Fix #472 2020-05-13 21:48:14 -04:00
Saika Fatih be45ff1ff1 A detail about Gzip support (#475)
* Typos fixed

* README.md edited.libz should be linked for GZIP support.
2020-05-12 17:38:51 -04:00
Saika Fatih 803ebe1e20 Typos fixed (#474) 2020-05-12 13:18:58 -04:00
yhirose ba685dbe48 Fixed potential infinite loop with content receiver 2020-05-10 20:45:57 -04:00
yhirose 49c4c2f9c1 Fix #459 2020-05-10 20:39:16 -04:00
yhirose 58909f5917 Fix #466 2020-05-10 15:58:53 -04:00
yhirose 5982b5c360 Fix #471 2020-05-10 14:18:03 -04:00
yhirose eb1fe5b191 Fixed warnings 2020-05-09 15:08:49 -04:00
yhirose 5e01587ed6 Fixed problem created in the previous commit 2020-05-09 13:43:06 -04:00
yhirose 5935d9fa59 Commented out the unit test for digest auth. 2020-05-09 13:32:51 -04:00
PixlRainbow 5bb4c12c6b Fix #465 (#467)
update digest header username to use username parameter instead of "hello" test value
2020-05-09 08:29:08 -04:00
yhirose 85637844c9 Updated README 2020-05-07 21:13:45 -04:00
Daniel Ottiger d043b18097 keepalive: support multiple post using content provider (#461) 2020-05-07 08:31:14 -04:00
yhirose 31bb13abd2 Removed TravisCI badge from README 2020-05-04 22:19:17 -04:00
yhirose 8728db7477 Apply IPV6_V6ONLY only when socket is AF_INET6 2020-05-04 22:16:43 -04:00
yhirose 1c50ac3667 Stop using TravisCI anymore due to IPv6 issue 2020-05-04 22:14:03 -04:00
yhirose cf386f97fd Merge branch 'master' of https://github.com/yhirose/cpp-httplib 2020-05-04 22:13:17 -04:00
Daniel Ottiger b2203bb05a server: support dual-stack server socket (#450)
According to RFC 3493 the socket option IPV6_V6ONLY
should be off by default, see
https://tools.ietf.org/html/rfc3493#page-22 (chapter 5.3).

However this does not seem to be the case on all systems.
For instance on any Windows OS, the option is on by default.

Therefore clear this option in order to allow
an server socket which can support IPv6 and IPv4 at the same time.
2020-05-04 22:13:12 -04:00
yhirose f5b806d995 Added a test case for #396. 2020-05-04 21:26:14 -04:00
yhirose 3895210f19 Code format 2020-05-04 21:25:59 -04:00
yhirose d45250fd88 Appled HANDLE_EINTR to send and select system calls 2020-05-01 21:38:23 -04:00
yhirose 528cacdc0d Changed CPPHTTPLIB_THREAD_POOL_COUNT back to 8. (#454) 2020-05-01 21:23:02 -04:00
Matthew DeVore ed1b6afa10 Fix crash caused by header field regex complexity (#457) 2020-05-01 12:44:13 -04:00
yhirose 08fc7085e5 Fixed #456 2020-04-30 19:40:23 -04:00
yhirose 8333340e2c Chagned to use inline function instead of macro 2020-04-27 12:36:39 -04:00
yhirose 98a0887571 Merge branch 'je-ik-sketch-eintr-handling' 2020-04-27 12:33:21 -04:00
Jan Lukavsky b0a189e50e Sketch handling EINTR errors 2020-04-27 17:36:44 +02:00
yhirose 776b3ffbf9 Code format 2020-04-25 18:01:48 -04:00
yhirose a061b97677 Adjust appveyor.yml 2020-04-25 18:01:12 -04:00
yhirose d359e3a5f7 Renave queue_adjust to on_idle (#442) 2020-04-25 17:56:55 -04:00
evg82 5928e0af1a TaskQueue method to internal size adjust (#442)
I use a custom TaskQueue, with variable number of workers, adding workers on demand is an easy task when new connection arrive (in enqueue function) however i need another funtion to be called even (or better) went no new connections arrives to reduce workers count. I only added a new virtual method in TaskQueue class to allow custom class to adjust workers size over time. Even if this methods is called frequenlty custom class can keep a "last_update" counter to check if need to adjust worker count or any other internal task. Without this function i need an external thread to make this adjust task.
2020-04-25 17:55:20 -04:00
yhirose a5005789ff Fixed Visual Studio compiler warnings with x64 platform (Resolve #440 and #446) (#448) 2020-04-25 17:13:14 -04:00
yhirose fae30af47d Updated appveyor.yml 2020-04-25 15:48:19 -04:00
Hoa Thiên Vũ 2feea0c9ab Fixed error: ‘ULONG_MAX’ was not declared in this scope on line 1921 (#445)
* Fixed error:
ULONG_MAX is defined in the limits.h header file. Putting #include <climits>
```
httplib.h: In function ‘bool httplib::detail::read_content_chunked(httplib::Stream&, httplib::ContentReceiver)’:
httplib.h:1921:22: error: ‘ULONG_MAX’ was not declared in this scope
     if (chunk_len == ULONG_MAX) { return false; }
                      ^~~~~~~~~
httplib.h:1921:22: note: suggested alternative: ‘_SC_ULONG_MAX’
     if (chunk_len == ULONG_MAX) { return false; }
                      ^~~~~~~~~
                      _SC_ULONG_MAX
```

* Move #include <climits> to after #include <cassert>
2020-04-24 12:02:19 -04:00
yhirose a2e4af54b7 Fix #399 2020-04-23 23:09:04 -04:00
yhirose d0b123be26 Support remote_addr and remote_port REMOTE_PORT header in client Request (#433) 2020-04-23 22:12:12 -04:00
Matthew DeVore df138366e4 Fail to read a chunk if its length is >= ULONG_MAX (#444)
We cannot trivially support such large chunks, and the maximum value
std::strtoul can parse accurately is ULONG_MAX-1. Error out early if the
length is longer than that.
2020-04-23 10:59:15 -04:00
Matthew DeVore c49441ae64 Do not throw exceptions when parsing request chunks (#441)
detail::read_content_chunked was using std::stoul to parse the
hexadecimal chunk lengths for "Transfer-Encoding: chunked" requests.
This throws an exception if the string does not begin with any valid
digits. read_content_chunked is not called in the context of a try block
so this caused the process to terminate.

Rather than use exceptions, I opted for std::stroul, which is similar to
std::stoul but does not throw exceptions. Since malformed user input is
not particularly exceptional, and some projects are compiled without
exception support, this approach seems both more portable and more
correct.
2020-04-23 09:05:45 -04:00
yhirose e1506fa186 Code cleanup 2020-04-22 21:43:16 -04:00
yhirose ad9fd3bd93 Fix #436 2020-04-22 21:42:58 -04:00
yhirose 05e0253195 Fixed test error 2020-04-21 23:07:51 -04:00
yhirose da26b517a3 Added url::Get interface 2020-04-21 23:00:39 -04:00
yhirose 2b7a968468 Added a unit test for URL interface 2020-04-21 21:21:31 -04:00
yhirose 240cc85ccb Fixed regex problem for recirect location 2020-04-21 21:18:29 -04:00
yhirose 129e2f00b8 Removed unnecessary noexcept 2020-04-20 19:42:05 -04:00
Daniel Ottiger da746c6e67 SSLClient::set_ca_cert_store: mark as inline (#435) 2020-04-20 12:53:39 -04:00
yhirose 3451da940d Code format 2020-04-19 22:05:04 -04:00
yhirose 38a6b3e69f Fixed warning 2020-04-19 22:04:29 -04:00
yhirose d1037ee9fd Close #433 2020-04-18 16:38:15 -04:00
Daniel Ottiger 2ece5f116b Pass certs and keys from memory (#432)
* SSLServer: add constructor to pass ssl-certificates and key from memory

* SSLClient: add constructor to pass ssl-certificates and key from memory

* add TestCase for passing certificates from memory to SSLClient/SSLServer
2020-04-18 16:26:06 -04:00
yhirose c2b6e4ac04 Fix #431 2020-04-17 21:48:16 -04:00
yhirose 8674555b88 Removed CMakeLists.txt. (Fix #421) 2020-04-13 20:56:21 -04:00
yhirose 85327e19ae Fix #425 2020-04-12 15:33:08 -04:00
Alexandre TailleferandAlexandre Taillefer ed8efea98b Added support for DELETE request body (#418)
* Added support for DELETE request body

* Fixed DELETE request body test case typo

Co-authored-by: Alexandre Taillefer <alexandre.taillefer@pwc.ca>
2020-04-07 15:51:52 -04:00
Max 1ccddd1b0b SSL_shutdown() only if not already closed by remote (#413) 2020-04-06 10:30:21 -04:00
yhirose 992f3dc690 Code cleanup 2020-04-03 09:33:29 -04:00
yhirose 402d47e2cd Fix #407 2020-03-31 19:42:53 -04:00
yhirose 171fc2e353 Fix #403. Added more status codes based on MDN document 2020-03-26 20:50:40 -04:00
SoenkeHeeren ced4160d05 add http status code 201 to show the right status message in return headers (#402) 2020-03-26 12:20:32 -04:00
yhirose 5b51aa6851 Revert "Add 1000-concurrency-result report"
This reverts commit b0af78e340.
2020-03-24 17:30:32 -04:00
Igor [hyperxor] dc13cde820 Minor improvements in httplib classes (#395) 2020-03-23 06:54:13 -04:00
yhirose b0af78e340 Add 1000-concurrency-result report 2020-03-21 15:49:26 +00:00
Andrew Gasparovic 914c8860e8 Accept content by value to allow moving
Previously, calling set_content always resulted in 's' being copied. With this change, there will still be the same number of copies made (1) when doing `set_content(my_value, ...)`, but there will be no copies if a caller elects to do `set_content(std::move(value), ...)` or `set_content(some_function_that_returns_a_temporary(), ...)` instead.
2020-03-21 00:39:07 -04:00
yhirose dc6a72a0fd Fix #387 2020-03-17 18:03:52 -04:00
yhirose 685533ba50 Fixed warnings on Windows due to max/min macro 2020-03-16 13:58:09 -04:00
yhirose 6e46ccb37c Updated README 2020-03-15 12:05:12 -04:00
Aristo Chen ac18b70a0f Update calculation formula for progress percentage (#386) 2020-03-15 08:29:27 -04:00
yhirose e1acb949e7 Fix #382 2020-03-13 18:43:29 -04:00
yhirose ab96f49766 Fixed problem that line end char is missing on start messagein simple
server example
2020-03-13 16:36:33 -04:00
Oleg Vorobiov 7b3cea5317 Prevent an implicit capture of 'this' via '[=]' (#381) 2020-03-12 12:31:22 -04:00
yhirose 26deffe0c6 Not to send 'EXCEPTION_WHAT' header to client 2020-03-10 17:44:36 -04:00
Rafael Leira e07c5fec01 simplest way to catch handler exceptions 2020-03-10 17:44:36 -04:00
miketstsandMichael Tseitlin 6e473a7c5c Fix conversion to ‘int’ from ‘long int’ warning (#377)
Co-authored-by: Michael Tseitlin <michael.tseitlin@concertio.com>
2020-03-10 14:48:14 -04:00
yhirose c74129a1c2 Fix #372 (#374) 2020-03-09 23:59:00 -04:00
yhirose 18e750b4e7 Code cleanup 2020-03-09 19:47:28 -04:00
Matthew DeVore bf7700d192 Fix exception that occurs with libc++ regex engine (#368)
The regex that parses header lines potentially causes an unlimited
amount of backtracking, which can cause an exception in the libc++ regex
engine.

The exception that occurs looks like this and is identical to the
message of the exception fixed in
https://github.com/yhirose/cpp-httplib/pull/280:

	libc++abi.dylib: terminating with uncaught exception of type
	std::__1::regex_error: The complexity of an attempted match
	against a regular expression exceeded a pre-set level.

This commit eliminates the problematic backtracking.
2020-02-28 06:31:39 -05:00
yhirose 3da925d6fe Adjusted a unit test. 2020-02-25 08:29:29 -05:00
yhirose 319417f26d Updated README 2020-02-24 21:50:07 -05:00
yhirose 4c3b119dde Code format 2020-02-24 21:17:02 -05:00
yhirose 6de8684328 Merge branch 'hyperxor-remove_code_duplication_in_getting_query' 2020-02-24 21:15:01 -05:00
hyperxor ccc9a9b3f4 Remove code duplication in converting params to query 2020-02-25 02:30:34 +03:00
rymis f2bb9c45d6 Support for deflate compression (#360) 2020-02-24 11:48:00 -05:00
yhirose 9a663aa94e Added a unit test. 2020-02-21 11:48:47 -05:00
yhirose d0d744d520 Fixed compiler warning with Visual C++. Close #358 2020-02-20 17:30:04 -05:00
yhirose fce8e6fefd Merge pull request #356 from samhocevar/fix-shadow-declarations
Fix a few shadowed variable compilation warnings.
2020-02-17 09:01:47 -05:00
Sam Hocevar 180aa32ebf Fix a few shadowed variable compilation warnings. 2020-02-17 06:58:30 +01:00
330 changed files with 123179 additions and 32809 deletions
+2
View File
@@ -0,0 +1,2 @@
/test/www*/dir/*.html text eol=lf
/test/www*/dir/*.txt text eol=lf
+70
View File
@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: 2025 Andrea Pappacoda <andrea@pappacoda.it>
# SPDX-License-Identifier: MIT
name: abidiff
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
defaults:
run:
shell: sh
jobs:
abi:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
container:
image: debian:testing
steps:
- name: Install dependencies
run: apt -y --update install --no-install-recommends
abigail-tools
ca-certificates
g++
git
libbrotli-dev
libssl-dev
libzstd-dev
meson
pkg-config
python3
zlib1g-dev
- uses: actions/checkout@v4
with:
path: current
- uses: actions/checkout@v4
with:
path: previous
fetch-depth: 0
- name: Checkout previous
working-directory: previous
run: |
git switch master
git describe --tags --abbrev=0 master | xargs git checkout
- name: Build current
working-directory: current
run: |
meson setup --buildtype=debug -Dcpp-httplib_compile=true build
ninja -C build
- name: Build previous
working-directory: previous
run: |
meson setup --buildtype=debug -Dcpp-httplib_compile=true build
ninja -C build
- name: Run abidiff
run: abidiff
--headers-dir1 previous/build
--headers-dir2 current/build
previous/build/libcpp-httplib.so
current/build/libcpp-httplib.so
+62
View File
@@ -0,0 +1,62 @@
name: benchmark-ab
# Manual A/B throughput comparison between two refs.
#
# This is a measurement, not a test: it never fails the build on a slow result.
# Absolute req/s from a shared runner is meaningless on its own, so both refs
# are built and measured alternately in the same job and only the ratio of the
# medians is reported, with a permutation test to say whether the difference
# stands out from the run-to-run noise.
#
# Non-SSL and Linux only for now.
on:
workflow_dispatch:
inputs:
base:
description: "Baseline ref"
required: false
default: "origin/master"
head:
description: "Ref to compare (defaults to the ref this run was started on)"
required: false
default: ""
rounds:
description: "Measurement rounds per ref (9+ recommended; below 4 the test can never reach significance)"
required: false
default: "9"
duration:
description: "Load duration per measurement"
required: false
default: "5s"
connections:
description: "Concurrent connections"
required: false
default: "10"
permissions:
contents: read
jobs:
ubuntu:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: install bombardier
run: go install github.com/codesenberg/bombardier@latest
- name: run A/B benchmark
run: |
export PATH="$(go env GOPATH)/bin:$PATH"
HEAD_REF="${{ inputs.head }}"
if [ -z "$HEAD_REF" ]; then HEAD_REF="${{ github.sha }}"; fi
./benchmark/ab.sh \
--base "${{ inputs.base }}" \
--head "$HEAD_REF" \
--rounds "${{ inputs.rounds }}" \
--duration "${{ inputs.duration }}" \
--connections "${{ inputs.connections }}"
+106
View File
@@ -0,0 +1,106 @@
name: benchmark-run
# Runs the committed benchmark (`just bench`) and records the numbers.
#
# This is a measurement, not a test: nothing here fails the build. Unlike
# benchmark-ab, which compares two refs inside one job, this just reports the
# absolute throughput of the current ref alongside Crow for reference.
#
# Absolute req/s is only meaningful against other runs on the same runner type,
# so compare like with like when reading the history.
#
# Non-SSL only. Windows is excluded: benchmark/Makefile depends on `nc`, `&`
# and `kill`, so it would need a PowerShell rewrite first.
on:
workflow_dispatch:
inputs:
duration:
description: "Load duration per server"
required: false
default: "5s"
connections:
description: "Concurrent connections"
required: false
default: "10"
crow:
description: "Also benchmark Crow v1.3.1 for reference"
type: boolean
required: false
default: true
permissions:
contents: read
jobs:
bench:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: checkout
uses: actions/checkout@v4
# macos runners ship without Go.
- name: setup Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: install bombardier
run: go install github.com/codesenberg/bombardier@latest
# crow_all.h includes <asio.hpp>, which no runner has out of the box.
- name: install asio
if: ${{ inputs.crow }}
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
sudo apt-get update && sudo apt-get install -y libasio-dev
else
brew install asio
fi
- name: run benchmark
run: |
# Without pipefail the `tee` below swallows a build failure and the
# job reports success having measured nothing.
set -o pipefail
export PATH="$(go env GOPATH)/bin:$PATH"
CROW_FLAGS="-std=c++17"
if [ "$RUNNER_OS" = "macOS" ]; then
CROW_FLAGS="$CROW_FLAGS -I$(brew --prefix asio)/include"
fi
if [ "${{ inputs.crow }}" = "true" ]; then TARGET=bench-all; else TARGET=bench; fi
make -C benchmark "$TARGET" \
CROW_CXXFLAGS="$CROW_FLAGS" \
BENCH="bombardier -c ${{ inputs.connections }} -d ${{ inputs.duration }} localhost:8080" \
2>&1 | tee /tmp/bench.txt
# pipefail only catches a failed build. Each Makefile recipe ends in
# `kill`, so a bombardier that never ran still leaves make happy — check
# that the measurements are actually there.
- name: check results were produced
run: |
expected=1
if [ "${{ inputs.crow }}" = "true" ]; then expected=2; fi
got=$(grep -c "Reqs/sec" /tmp/bench.txt || true)
if [ "$got" -lt "$expected" ]; then
echo "::error::expected $expected benchmark result(s), found $got"
exit 1
fi
- name: record results
if: always()
run: |
{
echo "## Benchmark (${{ matrix.os }})"
echo ""
echo "- ref: \`${{ github.ref_name }}\` (${{ github.sha }})"
echo "- connections=${{ inputs.connections }} duration=${{ inputs.duration }}"
echo ""
echo '```'
cat /tmp/bench.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
+42
View File
@@ -0,0 +1,42 @@
name: CIFuzz
# The fuzzers only build httplib.h and the targets under test/fuzzing, so a
# pull request that touches neither has nothing for CIFuzz to exercise. Fuzzing
# is by far the longest job in CI (10 minutes of fuzzing on top of building the
# OSS-Fuzz image), and skipping it for documentation-only changes keeps the
# full 600 seconds for the pull requests that do reach the parsers.
on:
pull_request:
paths:
- 'httplib.h'
- 'test/fuzzing/**'
- '.github/workflows/cifuzz.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'cpp-httplib'
dry-run: false
language: c++
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'cpp-httplib'
fuzz-seconds: 600
dry-run: false
language: c++
- name: Upload Crash
uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
+37
View File
@@ -0,0 +1,37 @@
name: docs
on:
push:
branches: [master]
paths:
- 'docs-src/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
with:
# Full history so sitemap <lastmod> reflects each page's real last commit date
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install docs-gen
run: cargo install docs-gen
- name: Build
run: docs-gen build docs-src docs
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: docs
- id: deployment
uses: actions/deploy-pages@v4
+51
View File
@@ -0,0 +1,51 @@
name: Release Docker Image
on:
release:
types: [published]
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history and tags
- name: Extract tag (manual)
if: github.event_name == 'workflow_dispatch'
id: set_tag_manual
run: |
# Checkout the latest tag and set output
git fetch --tags
LATEST_TAG=$(git describe --tags --abbrev=0)
git checkout $LATEST_TAG
echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT
- name: Extract tag (release)
if: github.event_name == 'release'
id: set_tag_release
run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64 # Build for both amd64 and arm64
# Use extracted tag without leading 'v'
tags: |
yhirose4dockerhub/cpp-httplib-server:latest
yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag_manual.outputs.tag || steps.set_tag_release.outputs.tag }}
+37
View File
@@ -0,0 +1,37 @@
name: 32-bit Build Test
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
test-win32:
name: Windows 32-bit (MSVC x86)
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Build (Win32)
shell: cmd
run: |
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:
name: ARM 32-bit (cross-compile)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install cross compiler
run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf
- name: Build (ARM 32-bit)
run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp
+621 -9
View File
@@ -1,17 +1,629 @@
name: test
on: [push, pull_request]
on:
push:
pull_request:
workflow_dispatch:
inputs:
gtest_filter:
description: 'Google Test filter'
test_linux:
description: 'Test on Linux'
type: boolean
default: true
test_macos:
description: 'Test on MacOS'
type: boolean
default: true
test_windows:
description: 'Test on Windows'
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
env:
# 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:
build:
runs-on: ${{ matrix.os }}
style-check:
# Uses the macOS runner's pre-installed Homebrew so clang-format tracks
# whatever version `brew install clang-format` currently resolves to on
# the maintainer's own Mac, instead of a version pinned in this file.
runs-on: macos-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
continue-on-error: true
steps:
- name: checkout
uses: actions/checkout@v4
- name: install clang-format
run: |
brew update
brew install clang-format coreutils
- name: run style check
run: |
clang-format --version
cd test && make style_check
build-and-test-on-32bit:
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')
strategy:
matrix:
os: [macOS-latest, ubuntu-latest]
config:
- arch_flags: -m32
arch_suffix: :i386
name: (32-bit)
steps:
- name: checkout
uses: actions/checkout@v1
- name: make
run: cd test && make
- name: checkout
uses: actions/checkout@v4
- name: install libraries
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y libc6-dev${{ matrix.config.arch_suffix }} libstdc++-13-dev${{ matrix.config.arch_suffix }} \
libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \
zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \
libzstd-dev${{ matrix.config.arch_suffix }}
- name: build and run tests
run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
ubuntu:
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')
strategy:
fail-fast: false
matrix:
tls_backend: [openssl, mbedtls, wolfssl]
name: ubuntu (${{ matrix.tls_backend }})
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: install OpenSSL
if: matrix.tls_backend == 'openssl'
run: sudo apt-get install -y libssl-dev
- name: install Mbed TLS
if: matrix.tls_backend == 'mbedtls'
run: sudo apt-get install -y libmbedtls-dev
- name: install wolfSSL
if: matrix.tls_backend == 'wolfssl'
run: sudo apt-get install -y libwolfssl-dev
- name: build and run tests (OpenSSL)
if: matrix.tls_backend == 'openssl'
run: cd test && make test_split && make test_openssl_parallel
env:
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
- name: build and run tests (wolfSSL)
if: matrix.tls_backend == 'wolfssl'
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
- name: run fuzz test target
if: matrix.tls_backend == 'openssl'
run: cd test && make fuzz_test
- name: build and run WebSocket heartbeat test
if: matrix.tls_backend == 'openssl'
run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
- name: build and run WebSocket TLS thread safety test
if: matrix.tls_backend == 'openssl'
run: cd test && make test_websocket_thread_safety && ./test_websocket_thread_safety
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
# Ubuntu 26.04's apt ships Mbed TLS 3.6, giving 3.x coverage that
# ubuntu-latest (24.04 = 2.28) and macOS (Homebrew = 4.x) no longer provide.
# Uses the 26.04 public-preview image; fold into the main ubuntu matrix once
# ubuntu-latest moves to 26.04.
ubuntu-2604-mbedtls:
runs-on: ubuntu-26.04
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
name: ubuntu-26.04 (mbedtls 3.x)
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: install Mbed TLS
run: sudo apt-get install -y libmbedtls-dev
- name: build and run tests (Mbed TLS)
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
# BoringSSL is Google's fork of OpenSSL. It has no API stability guarantee
# and is not packaged by distros, so we build it from source. cpp-httplib
# treats it as an OpenSSL backend variant via the OPENSSL_IS_BORINGSSL
# macro (see httplib.h). This job is best-effort: continue-on-error keeps
# upstream API drift from blocking PRs while still surfacing breakage.
ubuntu-boringssl:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
continue-on-error: true
name: ubuntu (boringssl, best-effort)
env:
# Tracking HEAD keeps us honest about upstream churn. If breakage
# becomes routine, replace HEAD with a 40-char commit SHA; the
# resolve step uses the SHA directly when it matches that shape.
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(nproc)" --target install
- name: build and run tests (BoringSSL)
# Override OPENSSL_SUPPORT to point the existing OpenSSL Makefile path
# at BoringSSL's prefix. BoringSSL defines OPENSSL_IS_BORINGSSL in
# <openssl/base.h>, which httplib.h and test.cc use to switch on API
# differences (e.g. SAN-only hostname verification, no CN fallback).
#
# BoringSSL's public headers (<openssl/stack.h>) use std::enable_if_t,
# so consumers must compile with C++14 or later. cpp-httplib itself
# supports C++11, but anyone pairing it with BoringSSL inherits this
# constraint. EXTRA_CXXFLAGS appends after the Makefile's -std=c++11
# and the later flag wins.
run: |
cd test
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -lpthread"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# macOS counterpart of the BoringSSL job. Same best-effort posture; the
# extra framework links cover the macOS Keychain integration that
# httplib.h auto-enables for any TLS backend on macOS.
macos-boringssl:
runs-on: macos-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
continue-on-error: true
name: macos (boringssl, best-effort)
env:
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(sysctl -n hw.ncpu)" --target install
- name: build and run tests (BoringSSL)
run: |
cd test
# CoreFoundation/Security frameworks satisfy the Keychain integration
# auto-enabled in httplib.h for macOS TLS builds.
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -framework CoreFoundation -framework Security"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
# When gai_suspend() hits the connection timeout, gai_cancel() is called
# but does not block; the resolver worker can later write back into the
# destroyed stack frame. To make the worker actually reach that write,
# the test job runs a loopback UDP responder (test/dns_test_fixture.py)
# that delays its reply past the test's 1s timeout, and uses an iptables
# NAT rule so glibc's lookups land on that fixture instead of a real
# nameserver. With ASAN's detect_stack_use_after_return enabled, the
# late write-back is reported as a stack-use-after-return.
issue-2431-repro:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
name: issue-2431 repro (Linux + ASAN)
# Bound the whole job in case anything in the test harness hangs
# unexpectedly. With the fixture in place a normal run is well under
# a minute either way (ASAN abort on broken HEAD, clean pass on fix).
timeout-minutes: 5
env:
DNS_FIXTURE_PORT: "15353"
DNS_FIXTURE_DELAY: "3"
steps:
- name: checkout
uses: actions/checkout@v4
- name: install libraries
run: |
sudo apt-get update
sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \
libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2
- name: start loopback DNS test fixture
run: |
# Force glibc through its DNS code path: Ubuntu's default
# nsswitch short-circuits to NOTFOUND through mdns4_minimal,
# which would skip the buggy code entirely.
sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf
# Run the loopback fixture (delayed UDP responder).
python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
>/tmp/dns_fixture.log 2>&1 &
echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null
# Wait for the fixture to start listening.
for _ in $(seq 1 50); do
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
break
fi
sleep 0.1
done
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \
|| { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; }
# Send the test process's DNS lookups to the loopback fixture.
# NAT only the local OUTPUT chain; conntrack handles the reply path.
sudo iptables -t nat -I OUTPUT -p udp --dport 53 \
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
# Sanity check: a query must take at least the fixture delay
# and resolve to NXDOMAIN (proving traffic reaches the fixture).
start=$(date +%s)
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
elapsed=$(( $(date +%s) - start ))
if [ "$elapsed" -lt 2 ]; then
echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2
exit 1
fi
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
- name: build test binary
run: cd test && make test
- name: run GetAddrInfoAsyncCancelTest
run: |
cd test
ARCH=$(uname -m)
CPPHTTPLIB_TEST_ISSUE_2431=1 \
ASAN_OPTIONS=detect_stack_use_after_return=1 \
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
setarch "$ARCH" -R \
./test --gtest_filter='GetAddrInfoAsyncCancelTest.*'
- name: tear down test fixture
if: always()
run: |
sudo iptables -t nat -F OUTPUT || true
if [ -f /tmp/dns_fixture.pid ]; then
sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true
fi
macos:
runs-on: macos-latest
if: >
(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')
strategy:
fail-fast: false
matrix:
tls_backend: [openssl, mbedtls, wolfssl]
name: macos (${{ matrix.tls_backend }})
steps:
- name: checkout
uses: actions/checkout@v4
- name: install Mbed TLS
if: matrix.tls_backend == 'mbedtls'
run: brew install mbedtls
- name: install wolfSSL
if: matrix.tls_backend == 'wolfssl'
run: brew install wolfssl
- name: build and run tests (OpenSSL)
if: matrix.tls_backend == 'openssl'
run: cd test && make test_split && make test_openssl_parallel
env:
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
- name: build and run tests (wolfSSL)
if: matrix.tls_backend == 'wolfssl'
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
- name: run fuzz test target
if: matrix.tls_backend == 'openssl'
run: cd test && make fuzz_test
- name: build and run WebSocket heartbeat test
if: matrix.tls_backend == 'openssl'
run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
- name: build and run WebSocket TLS thread safety test
if: matrix.tls_backend == 'openssl'
run: cd test && make test_websocket_thread_safety && ./test_websocket_thread_safety
- 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
permissions:
contents: read
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_windows == 'true')
strategy:
fail-fast: false
matrix:
config:
- with_ssl: false
compiled: false
run_tests: true
name: without SSL
- with_ssl: true
compiled: false
run_tests: true
name: with SSL
- with_ssl: false
compiled: true
run_tests: false
name: compiled
name: windows ${{ matrix.config.name }}
steps:
- name: Prepare Git for Checkout on Windows
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout
uses: actions/checkout@v4
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Setup msbuild on windows
uses: microsoft/setup-msbuild@v2
- name: Cache vcpkg packages
id: vcpkg-cache
uses: actions/cache@v4
with:
path: C:/vcpkg/installed
key: vcpkg-installed-windows-gtest-curl-zlib-brotli-zstd
- name: Install vcpkg dependencies
if: steps.vcpkg-cache.outputs.cache-hit != 'true'
run: vcpkg install gtest curl zlib brotli zstd
- name: Install OpenSSL
if: ${{ matrix.config.with_ssl }}
shell: pwsh
run: |
# Chocolatey's openssl package hardcodes a versioned slproweb URL, and
# slproweb keeps only the newest build of each branch. The package
# therefore 404s on every OpenSSL release until someone respins it.
# Read slproweb's own manifest instead: it is updated at the same time
# as the downloads it points at, so the URL is always live.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is slow with it
$manifest = 'https://raw.githubusercontent.com/slproweb/opensslhashes/master/win32_openssl_hashes.json'
$entry = (Invoke-RestMethod $manifest).files.PSObject.Properties.Value |
Where-Object {
$_.bits -eq 64 -and $_.arch -eq 'INTEL' -and
-not $_.light -and $_.installer -eq 'exe' -and $_.basever -like '4.*'
} |
Sort-Object { [version]$_.basever } | Select-Object -Last 1
if (-not $entry) { throw 'No 64-bit OpenSSL 4.x installer found in the manifest' }
Write-Host "Installing OpenSSL $($entry.basever) from $($entry.url)"
$installer = Join-Path $env:RUNNER_TEMP 'Win64OpenSSL.exe'
Invoke-WebRequest $entry.url -OutFile $installer
$actual = (Get-FileHash $installer -Algorithm SHA512).Hash.ToLower()
if ($actual -ne $entry.sha512.ToLower()) {
throw "SHA512 mismatch: expected $($entry.sha512), got $actual"
}
# Same silent flags the Chocolatey package used. The installer is Inno
# Setup, so /DIR pins the location CMake already looks in. The inner
# quotes matter: ArgumentList joins on spaces, so an unquoted /DIR
# would install to C:\Program and only fail later, at load time.
$dir = 'C:\Program Files\OpenSSL'
$proc = Start-Process $installer -Wait -PassThru -ArgumentList `
'/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-', "/DIR=`"$dir`""
if ($proc.ExitCode -ne 0) { throw "Installer exited with $($proc.ExitCode)" }
# Catch a misplaced install here rather than at link or load time.
if (-not (Test-Path "$dir\lib\VC\x64\MD\libcrypto.lib")) {
throw "OpenSSL import libraries missing under $dir"
}
if (-not (Get-ChildItem "$dir\bin\libcrypto-*.dll" -ErrorAction SilentlyContinue)) {
throw "OpenSSL runtime DLLs missing under $dir\bin"
}
"$dir\bin" | Out-File $env:GITHUB_PATH -Append -Encoding utf8
"OPENSSL_CONF=$dir\bin\openssl.cfg" | Out-File $env:GITHUB_ENV -Append -Encoding utf8
- name: Configure CMake ${{ matrix.config.name }}
run: >
cmake -B build -S .
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake
-DHTTPLIB_TEST=ON
-DHTTPLIB_COMPILE=${{ matrix.config.compiled && 'ON' || 'OFF' }}
-DHTTPLIB_USE_OPENSSL_IF_AVAILABLE=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
-DHTTPLIB_REQUIRE_ZLIB=ON
-DHTTPLIB_REQUIRE_BROTLI=ON
-DHTTPLIB_REQUIRE_ZSTD=ON
-DHTTPLIB_REQUIRE_OPENSSL=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
- name: Build ${{ matrix.config.name }}
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
- name: Run tests ${{ matrix.config.name }}
if: ${{ matrix.config.run_tests }}
shell: pwsh
working-directory: build/test
run: |
$shards = 4
$procs = @()
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 || '-*_Online' }}" `
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
}
$procs | Wait-Process
$failed = $false
for ($i = 0; $i -lt $shards; $i++) {
$log = "shard_${i}.log"
$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 (exit=$($proc.ExitCode)) ==="
Get-Content $log
if (Test-Path "${log}.err") { Get-Content "${log}.err" }
$failed = $true
}
}
if ($failed) { exit 1 }
Write-Host "All shards passed."
env:
VCPKG_ROOT: "C:/vcpkg"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
+79
View File
@@ -0,0 +1,79 @@
name: benchmark
on:
push:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
ubuntu:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: checkout
uses: actions/checkout@v4
- name: build and run
run: cd test && make test_benchmark && ./test_benchmark
macos:
runs-on: macos-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: checkout
uses: actions/checkout@v4
- name: build and run
run: cd test && make test_benchmark && ./test_benchmark
windows:
runs-on: windows-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Prepare Git for Checkout on Windows
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: checkout
uses: actions/checkout@v4
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Cache vcpkg packages
id: vcpkg-cache
uses: actions/cache@v4
with:
path: C:/vcpkg/installed
key: vcpkg-installed-windows-gtest
- name: Install vcpkg dependencies
if: steps.vcpkg-cache.outputs.cache-hit != 'true'
run: vcpkg install gtest
- name: Configure and build
shell: pwsh
run: |
$cmake_content = @"
cmake_minimum_required(VERSION 3.14)
project(httplib-benchmark CXX)
find_package(GTest REQUIRED)
add_executable(httplib-benchmark test/test_benchmark.cc)
target_include_directories(httplib-benchmark PRIVATE .)
target_link_libraries(httplib-benchmark PRIVATE GTest::gtest_main)
target_compile_options(httplib-benchmark PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
"@
New-Item -ItemType Directory -Force -Path build_bench/test | Out-Null
Set-Content -Path build_bench/CMakeLists.txt -Value $cmake_content
Copy-Item -Path httplib.h -Destination build_bench/
Copy-Item -Path test/test_benchmark.cc -Destination build_bench/test/
cmake -B build_bench/build -S build_bench `
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
cmake --build build_bench/build --config Release
- name: Run with retry
run: ctest --output-on-failure --test-dir build_bench/build -C Release --repeat until-pass:5
env:
VCPKG_ROOT: "C:/vcpkg"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
+20
View File
@@ -0,0 +1,20 @@
name: No Exceptions Test
on: [push, pull_request]
jobs:
test-no-exceptions:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev
- name: Run tests with CPPHTTPLIB_NO_EXCEPTIONS
run: |
cd test && make test_split EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS" && make test_openssl_parallel EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS"
+62
View File
@@ -0,0 +1,62 @@
name: test_offline
on:
push:
pull_request:
workflow_dispatch:
inputs:
test_linux:
description: 'Test on Linux'
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
env:
GTEST_FILTER: "-*.*_Online"
jobs:
ubuntu:
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')
strategy:
matrix:
tls_backend: [openssl, no-tls]
name: ubuntu (${{ matrix.tls_backend }})
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: install OpenSSL
if: matrix.tls_backend == 'openssl'
run: sudo apt-get install -y libssl-dev
- name: disable network
run: |
sudo iptables -A OUTPUT -o lo -j ACCEPT
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A OUTPUT -j REJECT
sudo ip6tables -A OUTPUT -o lo -j ACCEPT
sudo ip6tables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo ip6tables -A OUTPUT -j REJECT
- name: build and run tests (OpenSSL)
if: matrix.tls_backend == 'openssl'
run: cd test && make test_split && make test_openssl_parallel
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
- name: build and run tests (No TLS)
if: matrix.tls_backend == 'no-tls'
run: cd test && make test_no_tls_parallel
- name: restore network
if: always()
run: |
sudo iptables -F OUTPUT
sudo ip6tables -F OUTPUT
+37
View File
@@ -0,0 +1,37 @@
name: Proxy Test
on: [push, pull_request]
jobs:
test-proxy:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
strategy:
matrix:
tls_backend: [openssl, mbedtls]
name: proxy (${{ matrix.tls_backend }})
steps:
- uses: actions/checkout@v4
- name: Install common dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev netcat-openbsd
- name: Install OpenSSL
if: matrix.tls_backend == 'openssl'
run: sudo apt-get install -y libssl-dev
- name: Install Mbed TLS
if: matrix.tls_backend == 'mbedtls'
run: sudo apt-get install -y libmbedtls-dev
- name: Run proxy tests (OpenSSL)
if: matrix.tls_backend == 'openssl'
run: cd test && make proxy
env:
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
- name: Run proxy tests (Mbed TLS)
if: matrix.tls_backend == 'mbedtls'
run: cd test && make proxy_mbedtls
env:
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
+56 -2
View File
@@ -1,23 +1,75 @@
tags
AGENTS.md
CLAUDE.md
docs-src/pages/AGENTS.md
plans/
work/
# Ignore executables (no extension) but not source files
example/server
!example/server.*
example/client
!example/client.*
example/hello
!example/hello.*
example/simplecli
!example/simplecli.*
example/simplesvr
!example/simplesvr.*
example/benchmark
!example/benchmark.*
example/redirect
example/sse
!example/redirect.*
example/ssecli
!example/ssecli.*
example/ssecli-stream
!example/ssecli-stream.*
example/ssesvr
!example/ssesvr.*
example/upload
!example/upload.*
example/one_time_request
!example/one_time_request.*
example/server_and_client
!example/server_and_client.*
example/accept_header
!example/accept_header.*
example/wsecho
!example/wsecho.*
example/*.pem
test/httplib.cc
test/httplib.h
test/test
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
test/test_split
test/test_split_mbedtls
test/test_split_wolfssl
test/test_split_no_tls
test/test_websocket_heartbeat
test/test_websocket_thread_safety
test/test_thread_pool
test/test_benchmark
test/test.xcodeproj/xcuser*
test/test.xcodeproj/*/xcuser*
test/*.o
test/*.pem
test/*.srl
test/*.log
test/_build_*
benchmark/server*
*.swp
build/
Debug
Release
*.vcxproj.user
@@ -27,5 +79,7 @@ Release
*.db
ipch
*.dSYM
*.pyc
.*
!/.travis.yml
!/.gitattributes
!/.github
+12
View File
@@ -0,0 +1,12 @@
repos:
- repo: local
hooks:
- id: clang-format
name: clang-format
# Uses whatever `clang-format` resolves to on PATH (the Homebrew
# install on macOS) instead of a version pinned here, so it tracks
# the same version CI installs via `brew install clang-format`.
entry: clang-format
language: system
files: ^(httplib\.h|(example|fuzzing|test)/[^/]+\.(cpp|cc|h))$
args: [-i]
-14
View File
@@ -1,14 +0,0 @@
# Environment
language: cpp
os:
- linux
- osx
# Compiler selection
compiler:
- clang
# Build/test steps
script:
- cd ${TRAVIS_BUILD_DIR}/test
- make all
+478 -19
View File
@@ -1,29 +1,488 @@
cmake_minimum_required(VERSION 3.7.0)
project(httplib)
#[[
Build options:
* Standard BUILD_SHARED_LIBS is supported and sets HTTPLIB_SHARED default value.
* HTTPLIB_USE_OPENSSL_IF_AVAILABLE (default on)
* HTTPLIB_USE_WOLFSSL_IF_AVAILABLE (default off)
* HTTPLIB_USE_MBEDTLS_IF_AVAILABLE (default off)
* HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on)
* HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on)
* HTTPLIB_USE_ZSTD_IF_AVAILABLE (default on)
* HTTPLIB_BUILD_MODULES (default off)
* HTTPLIB_REQUIRE_OPENSSL (default off)
* HTTPLIB_REQUIRE_WOLFSSL (default off)
* HTTPLIB_REQUIRE_MBEDTLS (default off)
* HTTPLIB_REQUIRE_ZLIB (default off)
* HTTPLIB_REQUIRE_BROTLI (default off)
* HTTPLIB_REQUIRE_ZSTD (default off)
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on when supported)
* HTTPLIB_COMPILE (default off)
* HTTPLIB_INSTALL (default on)
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
* HTTPLIB_TEST (default off)
* BROTLI_USE_STATIC_LIBS - tells Cmake to use the static Brotli libs (only works if you have them installed).
* OPENSSL_USE_STATIC_LIBS - tells Cmake to use the static OpenSSL libs (only works if you have them installed).
set(CMAKE_CXX_STANDARD 11)
-------------------------------------------------------------------------------
# Include
After installation with Cmake, a find_package(httplib COMPONENTS OpenSSL wolfssl MbedTLS ZLIB Brotli zstd) is available.
This creates a httplib::httplib target (if found and if listed components are supported).
It can be linked like so:
target_link_libraries(your_exe httplib::httplib)
The following will build & install for later use.
Linux/macOS:
mkdir -p build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
sudo cmake --build . --target install
Windows:
mkdir build
cd build
cmake ..
runas /user:Administrator "cmake --build . --config Release --target install"
-------------------------------------------------------------------------------
These variables are available after you run find_package(httplib)
* HTTPLIB_HEADER_PATH - this is the full path to the installed header (e.g. /usr/include/httplib.h).
* HTTPLIB_IS_USING_OPENSSL - a bool for if OpenSSL support is enabled.
* HTTPLIB_IS_USING_WOLFSSL - a bool for if wolfSSL support is enabled.
* HTTPLIB_IS_USING_MBEDTLS - a bool for if MbedTLS support is enabled.
* HTTPLIB_IS_USING_ZLIB - a bool for if ZLIB support is enabled.
* HTTPLIB_IS_USING_BROTLI - a bool for if Brotli support is enabled.
* HTTPLIB_IS_USING_ZSTD - a bool for if ZSTD support is enabled.
* HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES - a bool for if support of loading system certs from the Apple Keychain is enabled.
* HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO - a bool for if nonblocking getaddrinfo is enabled.
* HTTPLIB_IS_COMPILED - a bool for if the library is compiled, or otherwise header-only.
* HTTPLIB_INCLUDE_DIR - the root path to httplib's header (e.g. /usr/include).
* HTTPLIB_LIBRARY - the full path to the library if compiled (e.g. /usr/lib/libhttplib.so).
* httplib_VERSION or HTTPLIB_VERSION - the project's version string.
* HTTPLIB_FOUND - a bool for if the target was found.
Want to use precompiled headers (Cmake feature since v3.16)?
It's as simple as doing the following (before linking):
target_precompile_headers(httplib::httplib INTERFACE "${HTTPLIB_HEADER_PATH}")
-------------------------------------------------------------------------------
ARCH_INDEPENDENT option of write_basic_package_version_file() requires Cmake v3.14
]]
cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR)
# Get the CPPHTTPLIB_VERSION value and use it as a version
# This gets the string with the CPPHTTPLIB_VERSION value from the header.
# This is so the maintainer doesn't actually need to update this manually.
file(STRINGS httplib.h _raw_version_string REGEX "CPPHTTPLIB_VERSION \"([0-9]+\\.[0-9]+\\.[0-9]+)\"")
# Extracts just the version string itself from the whole string contained in _raw_version_string
# since _raw_version_string would contain the entire line of code where it found the version string
string(REGEX MATCH "([0-9]+\\.?)+" _httplib_version "${_raw_version_string}")
project(httplib
VERSION ${_httplib_version}
LANGUAGES CXX
DESCRIPTION "A C++ header-only HTTP/HTTPS server and client library."
HOMEPAGE_URL "https://github.com/yhirose/cpp-httplib"
)
# Change as needed to set an OpenSSL minimum version.
# This is used in the installed Cmake config file.
set(_HTTPLIB_OPENSSL_MIN_VER "3.0.0")
# Lets you disable C++ exception during CMake configure time.
# The value is used in the install CMake config file.
option(HTTPLIB_NO_EXCEPTIONS "Disable the use of C++ exceptions" OFF)
# Allow for a build to require OpenSSL to pass, instead of just being optional
option(HTTPLIB_REQUIRE_OPENSSL "Requires OpenSSL to be found & linked, or fails build." OFF)
option(HTTPLIB_REQUIRE_WOLFSSL "Requires wolfSSL to be found & linked, or fails build." OFF)
option(HTTPLIB_REQUIRE_MBEDTLS "Requires MbedTLS to be found & linked, or fails build." OFF)
option(HTTPLIB_REQUIRE_ZLIB "Requires ZLIB to be found & linked, or fails build." OFF)
# Allow for a build to casually enable OpenSSL/ZLIB support, but silently continue if not found.
# Make these options so their automatic use can be specifically disabled (as needed)
option(HTTPLIB_USE_OPENSSL_IF_AVAILABLE "Uses OpenSSL (if available) to enable HTTPS support." ON)
option(HTTPLIB_USE_WOLFSSL_IF_AVAILABLE "Uses wolfSSL (if available) to enable HTTPS support." OFF)
option(HTTPLIB_USE_MBEDTLS_IF_AVAILABLE "Uses MbedTLS (if available) to enable HTTPS support." OFF)
option(HTTPLIB_USE_ZLIB_IF_AVAILABLE "Uses ZLIB (if available) to enable Zlib compression support." ON)
# Lets you compile the program as a regular library instead of header-only
option(HTTPLIB_COMPILE "If ON, uses a Python script to split the header into a compilable header & source file (requires Python v3)." OFF)
# Lets you disable the installation (useful when fetched from another CMake project)
option(HTTPLIB_INSTALL "Enables the installation target" ON)
option(HTTPLIB_TEST "Enables testing and builds tests" OFF)
option(HTTPLIB_REQUIRE_BROTLI "Requires Brotli to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_BROTLI_IF_AVAILABLE "Uses Brotli (if available) to enable Brotli decompression support." ON)
option(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES "Disable loading system certs from the Apple Keychain on macOS." OFF)
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
# C++20 modules support requires CMake 3.28 or later
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28")
option(HTTPLIB_BUILD_MODULES "Build httplib modules (requires HTTPLIB_COMPILE to be ON)." OFF)
else()
set(HTTPLIB_BUILD_MODULES OFF CACHE INTERNAL "Build httplib modules disabled (requires CMake 3.28+)" FORCE)
if(DEFINED CACHE{HTTPLIB_BUILD_MODULES} AND HTTPLIB_BUILD_MODULES)
message(WARNING "HTTPLIB_BUILD_MODULES requires CMake 3.28 or later. Current version is ${CMAKE_VERSION}. Modules support has been disabled.")
endif()
endif()
# Incompatibility between TLS libraries
set(TLS_LIBRARIES_USED_TMP 0)
foreach(tls_library OPENSSL WOLFSSL MBEDTLS)
set(TLS_REQUIRED ${HTTPLIB_REQUIRE_${tls_library}})
set(TLS_IF_AVAILABLE ${HTTPLIB_USE_${tls_library}_IF_AVAILABLE})
if(TLS_REQUIRED OR TLS_IF_AVAILABLE)
math(EXPR TLS_LIBRARIES_USED_TMP "${TLS_LIBRARIES_USED_TMP} + 1")
endif()
endforeach()
if(TLS_LIBRARIES_USED_TMP GREATER 1)
message(FATAL_ERROR "TLS libraries are mutually exclusive.")
endif()
# Defaults to static library but respects standard BUILD_SHARED_LIBS if set
include(CMakeDependentOption)
cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only."
"${BUILD_SHARED_LIBS}" HTTPLIB_COMPILE OFF
)
if(HTTPLIB_SHARED)
set(HTTPLIB_LIB_TYPE SHARED)
if(WIN32)
# Necessary for Windows if building shared libs
# See https://stackoverflow.com/a/40743080
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
else()
set(HTTPLIB_LIB_TYPE STATIC)
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
if(CMAKE_SYSTEM_VERSION)
if(${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
message(WARNING "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
endif()
else()
set(CMAKE_SYSTEM_VERSION "10.0.19041.0")
message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.")
endif()
endif()
# Set some variables that are used in-tree and while building based on our options
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES TRUE)
if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
endif()
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
if(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO AND WIN32)
include(CheckCXXSymbolExists)
set(_httplib_cmake_required_definitions ${CMAKE_REQUIRED_DEFINITIONS})
set(_httplib_cmake_required_libraries ${CMAKE_REQUIRED_LIBRARIES})
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_WIN32_WINNT=0x0A00)
list(APPEND CMAKE_REQUIRED_LIBRARIES ws2_32)
check_cxx_symbol_exists(GetAddrInfoExCancel "winsock2.h;ws2tcpip.h" HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
set(CMAKE_REQUIRED_DEFINITIONS ${_httplib_cmake_required_definitions})
set(CMAKE_REQUIRED_LIBRARIES ${_httplib_cmake_required_libraries})
unset(_httplib_cmake_required_definitions)
unset(_httplib_cmake_required_libraries)
if(NOT HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO FALSE)
message(WARNING "GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.")
endif()
endif()
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
# Since Cmake v3.11, Crypto & SSL became optional when not specified as COMPONENTS.
if(HTTPLIB_REQUIRE_OPENSSL)
find_package(OpenSSL ${_HTTPLIB_OPENSSL_MIN_VER} COMPONENTS Crypto SSL REQUIRED)
set(HTTPLIB_IS_USING_OPENSSL TRUE)
elseif(HTTPLIB_USE_OPENSSL_IF_AVAILABLE)
find_package(OpenSSL ${_HTTPLIB_OPENSSL_MIN_VER} COMPONENTS Crypto SSL QUIET)
# Avoid a rare circumstance of not finding all components but the end-user did their
# own call for OpenSSL, which might trick us into thinking we'd otherwise have what we wanted
if (TARGET OpenSSL::SSL AND TARGET OpenSSL::Crypto)
set(HTTPLIB_IS_USING_OPENSSL ${OPENSSL_FOUND})
else()
set(HTTPLIB_IS_USING_OPENSSL FALSE)
endif()
endif()
if(HTTPLIB_REQUIRE_WOLFSSL)
find_package(wolfssl REQUIRED)
set(HTTPLIB_IS_USING_WOLFSSL TRUE)
elseif(HTTPLIB_USE_WOLFSSL_IF_AVAILABLE)
find_package(wolfssl QUIET)
set(HTTPLIB_IS_USING_WOLFSSL ${wolfssl_FOUND})
endif()
if(HTTPLIB_REQUIRE_MBEDTLS)
find_package(MbedTLS REQUIRED)
set(HTTPLIB_IS_USING_MBEDTLS TRUE)
elseif(HTTPLIB_USE_MBEDTLS_IF_AVAILABLE)
find_package(MbedTLS QUIET)
set(HTTPLIB_IS_USING_MBEDTLS ${MbedTLS_FOUND})
endif()
if(HTTPLIB_REQUIRE_ZLIB)
find_package(ZLIB REQUIRED)
set(HTTPLIB_IS_USING_ZLIB TRUE)
elseif(HTTPLIB_USE_ZLIB_IF_AVAILABLE)
find_package(ZLIB QUIET)
# FindZLIB doesn't have a ZLIB_FOUND variable, so check the target.
if(TARGET ZLIB::ZLIB)
set(HTTPLIB_IS_USING_ZLIB TRUE)
endif()
endif()
# Adds our cmake folder to the search path for find_package
# This is so we can use our custom FindBrotli.cmake
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
if(HTTPLIB_REQUIRE_BROTLI)
find_package(Brotli COMPONENTS encoder decoder common REQUIRED)
set(HTTPLIB_IS_USING_BROTLI TRUE)
elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
find_package(Brotli COMPONENTS encoder decoder common QUIET)
set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND})
endif()
# NOTE:
# zstd < 1.5.6 does not provide the CMake imported target `zstd::libzstd`.
# Older versions must be consumed via their pkg-config file.
if(HTTPLIB_REQUIRE_ZSTD)
if (NOT TARGET zstd::libzstd)
find_package(zstd 1.5.6 CONFIG)
if(NOT zstd_FOUND)
find_package(PkgConfig REQUIRED)
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
endif()
endif()
set(HTTPLIB_IS_USING_ZSTD TRUE)
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
if (TARGET zstd::libzstd)
set(HTTPLIB_IS_USING_ZSTD TRUE)
else()
find_package(zstd 1.5.6 CONFIG QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(zstd QUIET IMPORTED_TARGET libzstd)
if(TARGET PkgConfig::zstd)
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
endif()
endif()
endif()
# Both find_package and PkgConf set a XXX_FOUND var
set(HTTPLIB_IS_USING_ZSTD ${zstd_FOUND})
endif()
endif()
# Used for default, common dirs that the end-user can change (if needed)
# like CMAKE_INSTALL_INCLUDEDIR or CMAKE_INSTALL_DATADIR
include(GNUInstallDirs)
include(ExternalProject)
add_library(${PROJECT_NAME} INTERFACE)
target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_11)
if(HTTPLIB_COMPILE)
# Put the split script into the build dir
configure_file(split.py "${CMAKE_CURRENT_BINARY_DIR}/split.py"
COPYONLY
)
# Needs to be in the same dir as the python script
configure_file(httplib.h "${CMAKE_CURRENT_BINARY_DIR}/httplib.h"
COPYONLY
)
target_include_directories(${PROJECT_NAME} INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>)
# Used outside of this if-else
set(_INTERFACE_OR_PUBLIC PUBLIC)
# Brings in the Python3_EXECUTABLE path we can use.
find_package(Python3 REQUIRED)
# Actually split the file
# Keeps the output in the build dir to not pollute the main dir
execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/split.py"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ERROR_VARIABLE _httplib_split_error
)
if(_httplib_split_error)
message(FATAL_ERROR "Failed when trying to split cpp-httplib with the Python script.\n${_httplib_split_error}")
endif()
install(TARGETS ${PROJECT_NAME} EXPORT httplibConfig
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
# If building modules, also generate the module file
if(HTTPLIB_BUILD_MODULES)
# Put the generate_module script into the build dir
configure_file(generate_module.py "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py"
COPYONLY
)
# Generate the module file
execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ERROR_VARIABLE _httplib_module_error
)
if(_httplib_module_error)
message(FATAL_ERROR "Failed when trying to generate cpp-httplib module with the Python script.\n${_httplib_module_error}")
endif()
endif()
install(FILES httplib.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})
# split.py puts output in "out"
set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out")
add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc")
target_sources(${PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
)
install(EXPORT httplibConfig DESTINATION share/httplib/cmake)
# Add C++20 module support if requested
# Include from separate file to prevent parse errors on older CMake versions
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28")
include(cmake/modules.cmake)
endif()
export(TARGETS ${PROJECT_NAME} FILE httplibConfig.cmake)
set_target_properties(${PROJECT_NAME}
PROPERTIES
VERSION ${${PROJECT_NAME}_VERSION}
SOVERSION "${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR}"
OUTPUT_NAME cpp-httplib
)
else()
# This is for header-only.
set(_INTERFACE_OR_PUBLIC INTERFACE)
add_library(${PROJECT_NAME} INTERFACE)
set(_httplib_build_includedir "${CMAKE_CURRENT_SOURCE_DIR}")
endif()
# Lets you address the target with httplib::httplib
# Only useful if building in-tree, versus using it from an installation.
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
#add_subdirectory(example)
#add_subdirectory(test)
# Require C++11, or C++20 if modules are enabled
if(HTTPLIB_BUILD_MODULES)
target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_20)
else()
target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
endif()
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
$<BUILD_INTERFACE:${_httplib_build_includedir}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
# Always require threads
Threads::Threads
# Needed for Windows libs on Mingw, as the pragma comment(lib, "xyz") aren't triggered.
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Windows>:crypt32>
# Needed for API from MacOS Security framework
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
# Needed for non-blocking getaddrinfo on MacOS
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
# Can't put multiple targets in a single generator expression or it bugs out.
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::decoder>
$<$<BOOL:${HTTPLIB_IS_USING_ZLIB}>:ZLIB::ZLIB>
$<$<BOOL:${HTTPLIB_IS_USING_ZSTD}>:zstd::libzstd>
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:OpenSSL::SSL>
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:OpenSSL::Crypto>
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:wolfssl::wolfssl>
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:MbedTLS::mbedtls>
)
# Set the definitions to enable optional features
target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
$<$<BOOL:${HTTPLIB_NO_EXCEPTIONS}>:CPPHTTPLIB_NO_EXCEPTIONS>
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:CPPHTTPLIB_BROTLI_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_ZLIB}>:CPPHTTPLIB_ZLIB_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_ZSTD}>:CPPHTTPLIB_ZSTD_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
$<$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
)
# CMake configuration files installation directory
set(_TARGET_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}")
include(CMakePackageConfigHelpers)
# Configures the meta-file httplibConfig.cmake.in to replace variables with paths/values/etc.
configure_package_config_file("cmake/${PROJECT_NAME}Config.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
INSTALL_DESTINATION "${_TARGET_INSTALL_CMAKEDIR}"
# Passes the includedir install path
PATH_VARS CMAKE_INSTALL_FULL_INCLUDEDIR
)
if(HTTPLIB_COMPILE)
write_basic_package_version_file("${PROJECT_NAME}ConfigVersion.cmake"
# Example: if you find_package(httplib 0.5.4)
# then anything >= 0.5.4 and < 0.6 is accepted
COMPATIBILITY SameMinorVersion
)
else()
write_basic_package_version_file("${PROJECT_NAME}ConfigVersion.cmake"
# Example: if you find_package(httplib 0.5.4)
# then anything >= 0.5.4 and < 0.6 is accepted
COMPATIBILITY SameMinorVersion
# Tells Cmake that it's a header-only lib
# Mildly useful for end-users :)
ARCH_INDEPENDENT
)
endif()
if(HTTPLIB_INSTALL)
# Creates the export httplibTargets.cmake
# This is strictly what holds compilation requirements
# and linkage information (doesn't find deps though).
if(HTTPLIB_BUILD_MODULES)
install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/httplib/modules CXX_MODULES_BMI DESTINATION ${CMAKE_INSTALL_LIBDIR}/httplib/modules)
else()
install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets)
endif()
install(FILES "${_httplib_build_includedir}/httplib.h" TYPE INCLUDE)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
# Install it so it can be used later by the httplibConfig.cmake file.
# Put it in the same dir as our config file instead of a global path so we don't potentially stomp on other packages.
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/FindBrotli.cmake"
DESTINATION ${_TARGET_INSTALL_CMAKEDIR}
)
# NOTE: This path changes depending on if it's on Windows or Linux
install(EXPORT httplibTargets
# Puts the targets into the httplib namespace
# So this makes httplib::httplib linkable after doing find_package(httplib)
NAMESPACE ${PROJECT_NAME}::
DESTINATION ${_TARGET_INSTALL_CMAKEDIR}
)
# Install documentation & license
# ex: /usr/share/doc/httplib/README.md and /usr/share/licenses/httplib/LICENSE
install(FILES "README.md" DESTINATION "${CMAKE_INSTALL_DOCDIR}")
install(FILES "LICENSE" DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/${PROJECT_NAME}")
include(CPack)
endif()
if(HTTPLIB_BUILD_MODULES AND NOT HTTPLIB_COMPILE)
message(FATAL_ERROR "HTTPLIB_BUILD_MODULES requires HTTPLIB_COMPILE to be ON.")
endif()
if(HTTPLIB_TEST)
include(CTest)
add_subdirectory(test)
endif()
+13
View File
@@ -0,0 +1,13 @@
FROM yhirose4dockerhub/ubuntu-builder AS builder
WORKDIR /build
COPY httplib.h .
COPY docker/main.cc .
RUN g++ -std=c++23 -static -o server -O2 -I. main.cc && strip server
FROM scratch
COPY --from=builder /build/server /server
COPY docker/html/index.html /html/index.html
EXPOSE 80
ENTRYPOINT ["/server"]
CMD ["--host", "0.0.0.0", "--port", "80", "--mount", "/:./html"]
+204
View File
@@ -0,0 +1,204 @@
# SSEClient - Server-Sent Events Client
A simple, EventSource-like SSE client for C++11.
## Features
- **Auto-reconnect**: Automatically reconnects on connection loss
- **Last-Event-ID**: Sends last received ID on reconnect for resumption
- **retry field**: Respects server's reconnect interval
- **Event types**: Supports custom event types via `on_event()`
- **Async support**: Run in background thread with `start_async()`
- **C++11 compatible**: No C++14/17/20 features required
## Quick Start
```cpp
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
```
## API Reference
### SSEMessage
```cpp
struct SSEMessage {
std::string event; // Event type (default: "message")
std::string data; // Event payload
std::string id; // Event ID
};
```
### SSEClient
#### Constructor
```cpp
// Basic
SSEClient(Client &client, const std::string &path);
// With custom headers
SSEClient(Client &client, const std::string &path, const Headers &headers);
```
#### Event Handlers
```cpp
// Called for all events (or events without a specific handler)
sse.on_message([](const SSEMessage &msg) { });
// Called for specific event types
sse.on_event("update", [](const SSEMessage &msg) { });
sse.on_event("delete", [](const SSEMessage &msg) { });
// Called when connection is established
sse.on_open([]() { });
// Called on connection errors
sse.on_error([](httplib::Error err) { });
```
#### Configuration
```cpp
// Set reconnect interval (default: 3000ms, minimum: 100ms)
sse.set_reconnect_interval(5000);
// Set max reconnect attempts (default: 0 = unlimited)
sse.set_max_reconnect_attempts(10);
// Update headers at any time (thread-safe)
sse.set_headers({{"Authorization", "Bearer new_token"}});
```
#### Control
```cpp
// Blocking start with auto-reconnect
sse.start();
// Non-blocking start (runs in background thread)
sse.start_async();
// Stop the client (thread-safe)
sse.stop();
```
#### State
```cpp
bool connected = sse.is_connected();
const std::string &id = sse.last_event_id();
```
## Examples
### Basic Usage
```cpp
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << msg.data << std::endl;
});
sse.start();
```
### With Custom Event Types
```cpp
httplib::sse::SSEClient sse(cli, "/events");
sse.on_event("notification", [](const httplib::sse::SSEMessage &msg) {
std::cout << "Notification: " << msg.data << std::endl;
});
sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
std::cout << "Update: " << msg.data << std::endl;
});
sse.start();
```
### Async with Stop
```cpp
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << msg.data << std::endl;
});
sse.start_async(); // Returns immediately
// ... do other work ...
sse.stop(); // Stop when done
```
### With Custom Headers (e.g., Authentication)
```cpp
httplib::Headers headers = {
{"Authorization", "Bearer token123"}
};
httplib::sse::SSEClient sse(cli, "/events", headers);
sse.start();
```
### Refreshing Auth Token on Reconnect
```cpp
httplib::sse::SSEClient sse(cli, "/events",
{{"Authorization", "Bearer " + get_token()}});
// Preemptively refresh token on each successful connection
sse.on_open([&sse]() {
sse.set_headers({{"Authorization", "Bearer " + get_token()}});
});
// Or reactively refresh on auth failure (401 triggers reconnect)
sse.on_error([&sse](httplib::Error) {
sse.set_headers({{"Authorization", "Bearer " + refresh_token()}});
});
sse.start();
```
### Error Handling
```cpp
sse.on_error([](httplib::Error err) {
std::cerr << "Error: " << httplib::to_string(err) << std::endl;
});
sse.set_reconnect_interval(1000);
sse.set_max_reconnect_attempts(5);
sse.start();
```
## SSE Protocol
The client parses SSE format according to the [W3C specification](https://html.spec.whatwg.org/multipage/server-sent-events.html):
```
event: custom-type
id: 123
data: {"message": "hello"}
data: simple message
: this is a comment (ignored)
```
+317
View File
@@ -0,0 +1,317 @@
# cpp-httplib Streaming API
This document describes the streaming extensions for cpp-httplib, providing an iterator-style API for handling HTTP responses incrementally with **true socket-level streaming**.
> **Important Notes**:
>
> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed after the response is fully read. For connection reuse, use `Client::Get()`.
> - **Single iteration only**: The `next()` method can only iterate through the body once.
> - **Result is not thread-safe**: While `stream::Get()` can be called from multiple threads simultaneously, the returned `stream::Result` must be used from a single thread only.
## Overview
The streaming API allows you to process HTTP response bodies chunk by chunk using an iterator-style pattern. Data is read directly from the network socket, enabling low-memory processing of large responses. This is particularly useful for:
- **LLM/AI streaming responses** (e.g., ChatGPT, Claude, Ollama)
- **Server-Sent Events (SSE)**
- **Large file downloads** with progress tracking
- **Reverse proxy implementations**
## Quick Start
```cpp
#include "httplib.h"
int main() {
httplib::Client cli("http://localhost:8080");
// Get streaming response
auto result = httplib::stream::Get(cli, "/stream");
if (result) {
// Process response body in chunks
while (result.next()) {
std::cout.write(result.data(), result.size());
}
}
return 0;
}
```
## API Layers
cpp-httplib provides multiple API layers for different use cases:
```text
┌─────────────────────────────────────────────┐
│ SSEClient │ ← SSE-specific, parsed events
│ - on_message(), on_event() │
│ - Auto-reconnect, Last-Event-ID │
├─────────────────────────────────────────────┤
│ stream::Get() / stream::Result │ ← Iterator-based streaming
│ - while (result.next()) { ... } │
├─────────────────────────────────────────────┤
│ open_stream() / StreamHandle │ ← General-purpose streaming
│ - handle.read(buf, len) │
├─────────────────────────────────────────────┤
│ Client::Get() │ ← Traditional, full buffering
└─────────────────────────────────────────────┘
```
| Use Case | Recommended API |
|----------|----------------|
| SSE with auto-reconnect | `SSEClient` (see [README-sse.md](README-sse.md)) |
| LLM streaming (JSON Lines) | `stream::Get()` |
| Large file download | `stream::Get()` or `open_stream()` |
| Reverse proxy | `open_stream()` |
| Small responses with Keep-Alive | `Client::Get()` |
## API Reference
### Low-Level API: `StreamHandle`
The `StreamHandle` struct provides direct control over streaming responses. It takes ownership of the socket connection and reads data directly from the network.
> **Note:** When using `open_stream()`, the connection is dedicated to streaming and **Keep-Alive is not supported**. For Keep-Alive connections, use `client.Get()` instead.
```cpp
// Open a stream (takes ownership of socket)
httplib::Client cli("http://localhost:8080");
auto handle = cli.open_stream("GET", "/path");
// Check validity
if (handle.is_valid()) {
// Access response headers immediately
int status = handle.response->status;
auto content_type = handle.response->get_header_value("Content-Type");
// Read body incrementally
char buf[4096];
ssize_t n;
while ((n = handle.read(buf, sizeof(buf))) > 0) {
process(buf, n);
}
}
```
#### StreamHandle Members
| Member | Type | Description |
|--------|------|-------------|
| `response` | `std::unique_ptr<Response>` | HTTP response with headers |
| `error` | `Error` | Error code if request failed |
| `is_valid()` | `bool` | Returns true if response is valid |
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket |
| `get_read_error()` | `Error` | Get the last read error |
| `has_read_error()` | `bool` | Check if a read error occurred |
### High-Level API: `stream::Get()` and `stream::Result`
The `httplib.h` header provides a more ergonomic iterator-style API.
```cpp
#include "httplib.h"
httplib::Client cli("http://localhost:8080");
cli.set_follow_location(true);
...
// Simple GET
auto result = httplib::stream::Get(cli, "/path");
// GET with custom headers
httplib::Headers headers = {{"Authorization", "Bearer token"}};
auto result = httplib::stream::Get(cli, "/path", headers);
// Process the response
if (result) {
while (result.next()) {
process(result.data(), result.size());
}
}
// Or read entire body at once
auto result2 = httplib::stream::Get(cli, "/path");
if (result2) {
std::string body = result2.read_all();
}
```
#### stream::Result Members
| Member | Type | Description |
|--------|------|-------------|
| `operator bool()` | `bool` | Returns true if response is valid |
| `is_valid()` | `bool` | Same as `operator bool()` |
| `status()` | `int` | HTTP status code |
| `headers()` | `const Headers&` | Response headers |
| `get_header_value(key, def)` | `std::string` | Get header value (with optional default) |
| `has_header(key)` | `bool` | Check if header exists |
| `next()` | `bool` | Read next chunk, returns false when done |
| `data()` | `const char*` | Pointer to current chunk data |
| `size()` | `size_t` | Size of current chunk |
| `read_all()` | `std::string` | Read entire remaining body into string |
| `error()` | `Error` | Get the connection/request error |
| `read_error()` | `Error` | Get the last read error |
| `has_read_error()` | `bool` | Check if a read error occurred |
## Usage Examples
### Example 1: SSE (Server-Sent Events) Client
```cpp
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:1234");
auto result = httplib::stream::Get(cli, "/events");
if (!result) { return 1; }
while (result.next()) {
std::cout.write(result.data(), result.size());
std::cout.flush();
}
return 0;
}
```
For a complete SSE client with auto-reconnection and event parsing, see `example/ssecli-stream.cc`.
### Example 2: LLM Streaming Response
```cpp
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:11434"); // Ollama
auto result = httplib::stream::Get(cli, "/api/generate");
if (result && result.status() == 200) {
while (result.next()) {
std::cout.write(result.data(), result.size());
std::cout.flush();
}
}
// Check for connection errors
if (result.read_error() != httplib::Error::Success) {
std::cerr << "Connection lost\n";
}
return 0;
}
```
### Example 3: Large File Download with Progress
```cpp
#include "httplib.h"
#include <fstream>
#include <iostream>
int main() {
httplib::Client cli("http://example.com");
auto result = httplib::stream::Get(cli, "/large-file.zip");
if (!result || result.status() != 200) {
std::cerr << "Download failed\n";
return 1;
}
std::ofstream file("download.zip", std::ios::binary);
size_t total = 0;
while (result.next()) {
file.write(result.data(), result.size());
total += result.size();
std::cout << "\rDownloaded: " << (total / 1024) << " KB" << std::flush;
}
std::cout << "\nComplete!\n";
return 0;
}
```
### Example 4: Reverse Proxy Streaming
```cpp
#include "httplib.h"
httplib::Server svr;
svr.Get("/proxy/(.*)", [](const httplib::Request& req, httplib::Response& res) {
httplib::Client upstream("http://backend:8080");
auto handle = upstream.open_stream("/" + req.matches[1].str());
if (!handle.is_valid()) {
res.status = 502;
return;
}
res.status = handle.response->status;
res.set_chunked_content_provider(
handle.response->get_header_value("Content-Type"),
[handle = std::move(handle)](size_t, httplib::DataSink& sink) mutable {
char buf[8192];
auto n = handle.read(buf, sizeof(buf));
if (n > 0) {
sink.write(buf, static_cast<size_t>(n));
return true;
}
sink.done();
return true;
}
);
});
svr.listen("0.0.0.0", 3000);
```
## Comparison with Existing APIs
| Feature | `Client::Get()` | `open_stream()` | `stream::Get()` |
|---------|----------------|-----------------|----------------|
| Headers available | After complete | Immediately | Immediately |
| Body reading | All at once | Direct from socket | Iterator-based |
| Memory usage | Full body in RAM | Minimal (controlled) | Minimal (controlled) |
| Keep-Alive support | ✅ Yes | ❌ No | ❌ No |
| Compression | Auto-handled | Auto-handled | Auto-handled |
| Best for | Small responses, Keep-Alive | Low-level streaming | Easy streaming |
## Features
- **True socket-level streaming**: Data is read directly from the network socket
- **Low memory footprint**: Only the current chunk is held in memory
- **Compression support**: Automatic decompression for gzip, brotli, and zstd
- **Chunked transfer**: Full support for chunked transfer encoding
- **SSL/TLS support**: Works with HTTPS connections
## Important Notes
### Keep-Alive Behavior
The streaming API (`stream::Get()` / `open_stream()`) takes ownership of the socket connection for the duration of the stream. This means:
- **Keep-Alive is not supported** for streaming connections
- The socket is closed when `StreamHandle` is destroyed
- For Keep-Alive scenarios, use the standard `client.Get()` API instead
```cpp
// Use for streaming (no Keep-Alive)
auto result = httplib::stream::Get(cli, "/large-stream");
while (result.next()) { /* ... */ }
// Use for Keep-Alive connections
auto res = cli.Get("/api/data"); // Connection can be reused
```
## Related
- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
- [example/ssecli-stream.cc](./example/ssecli-stream.cc) - SSE client with auto-reconnection
+523
View File
@@ -0,0 +1,523 @@
# WebSocket - RFC 6455 WebSocket Support
A simple, blocking WebSocket implementation for C++11.
> [!IMPORTANT]
> 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 (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
### Server
```cpp
httplib::Server svr;
svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.read(msg)) {
ws.send("echo: " + msg);
}
});
svr.listen("localhost", 8080);
```
### Client
```cpp
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
if (ws.connect()) {
ws.send("hello");
std::string msg;
if (ws.read(msg)) {
std::cout << msg << std::endl; // "echo: hello"
}
ws.close();
}
```
## API Reference
### ReadResult
```cpp
enum ReadResult : int {
Fail = 0, // Connection closed or error
Text = 1, // UTF-8 text message
Binary = 2, // Binary message
Timeout = 3, // Read timeout elapsed; connection still open
};
```
Returned by `read()`. Since `Fail` is `0`, the result works naturally in boolean contexts — `while (ws.read(msg))` continues until the connection closes. When you need to distinguish text from binary, check the return value directly.
`Timeout` is only returned for a read timeout you set yourself with `set_read_timeout()`. It means the timeout elapsed on a message boundary: nothing was consumed and the connection is still open, so you can send on it and read again. The compile-time defaults (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`, 300 seconds on the server; a client waits forever) are a backstop against a peer that has gone quiet, not a request for control: when one of them elapses, `read()` returns `Fail` and closes the connection, so code that never calls `set_read_timeout()` can keep using `while (ws.read(msg))`.
**`msg` is left untouched on `Timeout`.** Because `Timeout` is non-zero, `while (ws.read(msg))` keeps looping — with the *previous* message still in `msg`. Once you set a read timeout, test the result instead:
```cpp
ws.set_read_timeout(std::chrono::milliseconds(100));
std::string msg;
while (ws.is_open()) {
auto r = ws.read(msg);
if (r == httplib::ws::Timeout) { continue; } // nothing yet; send if you like
if (r == httplib::ws::Fail) { break; }
handle(msg);
}
```
### CloseStatus
```cpp
enum class CloseStatus : uint16_t {
Normal = 1000,
GoingAway = 1001,
ProtocolError = 1002,
UnsupportedData = 1003,
NoStatus = 1005,
Abnormal = 1006,
InvalidPayload = 1007,
PolicyViolation = 1008,
MessageTooBig = 1009,
MandatoryExtension = 1010,
InternalError = 1011,
};
```
### Server Registration
```cpp
// Basic handler
Server &WebSocket(const std::string &pattern, WebSocketHandler handler);
// With subprotocol negotiation
Server &WebSocket(const std::string &pattern, WebSocketHandler handler,
SubProtocolSelector sub_protocol_selector);
```
**Type aliases:**
```cpp
using WebSocketHandler =
std::function<void(const Request &, ws::WebSocket &)>;
using SubProtocolSelector =
std::function<std::string(const std::vector<std::string> &protocols)>;
```
The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols. A returned value that the client did not propose is ignored.
### WebSocket (Server-side)
Passed to the handler registered with `Server::WebSocket()`. The handler runs in a dedicated thread per connection.
```cpp
// Read next message (blocks until received, returns Fail/Text/Binary)
ReadResult read(std::string &msg);
// Send messages
bool send(const std::string &data); // Text
bool send(const char *data, size_t len); // Binary
// Close the connection
void close(CloseStatus status = CloseStatus::Normal,
const std::string &reason = "");
// Access the original HTTP upgrade request
const Request &request() const;
// Check if the connection is still open
bool is_open() const;
```
### WebSocketClient
```cpp
// Constructor - accepts ws:// or wss:// URL
explicit WebSocketClient(const std::string &scheme_host_port_path,
const Headers &headers = {});
// Constructor with a client certificate for mutual TLS (wss:// only,
// requires CPPHTTPLIB_OPENSSL_SUPPORT). The certificate is ignored for
// ws:// URLs.
struct PemMemory {
const char *cert_pem;
size_t cert_pem_len;
const char *key_pem;
size_t key_pem_len;
const char *private_key_password;
};
explicit WebSocketClient(const std::string &scheme_host_port_path,
const PemMemory &pem, const Headers &headers = {});
// Check if the URL was parsed successfully
bool is_valid() const;
// Connect (performs HTTP upgrade handshake). The returned Result is truthy
// only when the handshake fully succeeded; on failure it describes what went
// wrong:
// res.error() httplib::Error identifying the failing layer
// res.status() HTTP status of the upgrade response (-1 if none)
// res.headers() headers of the upgrade response
// res.ssl_error() TLS error detail (wss://, SSL builds only)
// res.ssl_backend_error() backend-specific TLS error code (SSL builds only)
Result connect();
// Get the subprotocol selected by the server (empty if none)
const std::string &subprotocol() const;
// Read/Send/Close (same as server-side WebSocket)
ReadResult read(std::string &msg);
bool send(const std::string &data);
bool send(const char *data, size_t len);
void close(CloseStatus status = CloseStatus::Normal,
const std::string &reason = "");
bool is_open() const;
// Timeouts
void set_read_timeout(time_t sec, time_t usec = 0);
void set_write_timeout(time_t sec, time_t usec = 0);
void set_connection_timeout(time_t sec, time_t usec = 0);
template <class Rep, class Period>
void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
template <class Rep, class Period>
void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
template <class Rep, class Period>
void set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
// SSL configuration (wss:// only, requires CPPHTTPLIB_OPENSSL_SUPPORT)
void set_ca_cert_path(const std::string &ca_cert_file_path,
const std::string &ca_cert_dir_path = std::string());
void set_ca_cert_store(tls::ca_store_t store);
void enable_server_certificate_verification(bool enabled);
void enable_server_hostname_verification(bool enabled);
```
## Examples
### Echo Server with Connection Logging
```cpp
httplib::Server svr;
svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::cout << "Connected from " << req.remote_addr << std::endl;
std::string msg;
while (ws.read(msg)) {
ws.send("echo: " + msg);
}
std::cout << "Disconnected" << std::endl;
});
svr.listen("localhost", 8080);
```
### Client: Continuous Read Loop
```cpp
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
if (ws.connect()) {
ws.send("hello");
ws.send("world");
std::string msg;
while (ws.read(msg)) { // blocks until a message arrives
std::cout << msg << std::endl; // "echo: hello", "echo: world"
}
// read() returns false when the server closes the connection
}
```
### Inspecting Connection Failures
`connect()` returns a `Result` that tells you why a connection attempt failed.
`error()` distinguishes network problems (`Connection`, `ConnectionTimeout`),
TLS problems (`SSLConnection`, `SSLServerVerification`,
`SSLServerHostnameVerification`), and upgrade rejections
(`WebSocketHandshake`). When the server answered with something other than
`101 Switching Protocols`, `status()` and `headers()` carry that response:
```cpp
auto res = ws.connect();
if (!res) {
std::cerr << "connect failed: " << httplib::to_string(res.error()) << std::endl;
if (res.status() != -1) {
// The server responded but refused the upgrade (e.g. 401, 404)
std::cerr << "HTTP status: " << res.status() << std::endl;
}
}
```
### Text and Binary Messages
Check the `ReadResult` return value to distinguish between text and binary:
```cpp
// Server
svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
httplib::ws::ReadResult ret;
while ((ret = ws.read(msg))) {
if (ret == httplib::ws::Text) {
ws.send("echo: " + msg);
} else {
ws.send(msg.data(), msg.size()); // Binary echo
}
}
});
// Client
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
if (ws.connect()) {
// Send binary data
const char binary[] = {0x00, 0x01, 0x02, 0x03};
ws.send(binary, sizeof(binary));
// Receive and check the type
std::string msg;
if (ws.read(msg) == httplib::ws::Binary) {
// Process binary data in msg
}
ws.close();
}
```
### SSL Client
```cpp
httplib::ws::WebSocketClient ws("wss://echo.example.com/ws");
if (ws.connect()) {
ws.send("hello over TLS");
std::string msg;
if (ws.read(msg)) {
std::cout << msg << std::endl;
}
ws.close();
}
```
### Close with Status
```cpp
// Client-side: close with a specific status code and reason
ws.close(httplib::ws::CloseStatus::GoingAway, "shutting down");
// Server-side: close with a policy violation status
ws.close(httplib::ws::CloseStatus::PolicyViolation, "forbidden");
```
### Accessing the Upgrade Request
```cpp
svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
// Access headers from the original HTTP upgrade request
auto auth = req.get_header_value("Authorization");
if (auth.empty()) {
ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized");
return;
}
std::string msg;
while (ws.read(msg)) {
ws.send("echo: " + msg);
}
});
```
The check above runs after the handshake, so the client sees a successful upgrade followed by a close frame. To refuse the upgrade itself with an HTTP status, use a pre-routing or pre-request handler. Both run before the `101 Switching Protocols` response, and `req.matched_route` is available in the pre-request handler:
```cpp
svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &res) {
if (req.matched_route == "/ws" && req.get_header_value("Authorization").empty()) {
res.status = httplib::StatusCode::Unauthorized_401;
return httplib::Server::HandlerResponse::Handled; // not upgraded
}
return httplib::Server::HandlerResponse::Unhandled;
});
```
### Custom Headers and Timeouts
```cpp
httplib::Headers headers = {
{"Authorization", "Bearer token123"}
};
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws", headers);
ws.set_connection_timeout(5, 0); // 5 seconds
ws.set_read_timeout(30, 0); // 30 seconds
ws.set_write_timeout(10, 0); // 10 seconds
// std::chrono is also supported
ws.set_connection_timeout(std::chrono::seconds(5));
ws.set_read_timeout(std::chrono::seconds(30));
ws.set_write_timeout(std::chrono::seconds(10));
if (ws.connect()) {
std::string msg;
while (ws.read(msg)) {
std::cout << msg << std::endl;
}
}
```
### Subprotocol Negotiation
The server can negotiate a subprotocol with the client using `Sec-WebSocket-Protocol`. This is required for protocols like GraphQL over WebSocket (`graphql-ws`) and MQTT.
```cpp
// Server: register a handler with a subprotocol selector
svr.WebSocket(
"/ws",
[](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.read(msg)) {
ws.send("echo: " + msg);
}
},
[](const std::vector<std::string> &protocols) -> std::string {
// The client proposed a list of subprotocols; pick one
for (const auto &p : protocols) {
if (p == "graphql-ws" || p == "graphql-transport-ws") {
return p;
}
}
return ""; // Decline all
});
// Client: propose subprotocols via Sec-WebSocket-Protocol header
httplib::Headers headers = {
{"Sec-WebSocket-Protocol", "graphql-ws, graphql-transport-ws"}
};
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws", headers);
if (ws.connect()) {
// Check which subprotocol the server selected
std::cout << "Subprotocol: " << ws.subprotocol() << std::endl;
// => "graphql-ws"
ws.close();
}
```
### SSL Client with Certificate Configuration
```cpp
httplib::ws::WebSocketClient ws("wss://example.com/ws");
ws.set_ca_cert_path("/path/to/ca-bundle.crt");
ws.enable_server_certificate_verification(true);
ws.enable_server_hostname_verification(true); // default; false skips the identity check
if (ws.connect()) {
ws.send("secure message");
ws.close();
}
```
## Configuration
| Macro | Default | Description |
|---------------------------------------------|-------------------|----------------------------------------------------------|
| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message |
| `CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND` | `0` | Client read timeout (seconds); `0` waits forever |
| `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` | `300` | Server read timeout (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
You can override the ping interval at runtime instead of changing the compile-time macro. Set it to `0` to disable automatic pings entirely.
```cpp
// Server side
httplib::Server svr;
svr.set_websocket_ping_interval(10); // 10 seconds
// Or using std::chrono
svr.set_websocket_ping_interval(std::chrono::seconds(10));
// Client side
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
ws.set_websocket_ping_interval(10); // 10 seconds
// Disable automatic pings
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. On the server side a dead connection still does not linger: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no such backstop — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side it is also the knob for noticing one *faster* than the 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.
The default thread pool uses dynamic scaling: it maintains a base thread count of `CPPHTTPLIB_THREAD_POOL_COUNT` (8 or `std::thread::hardware_concurrency() - 1`, whichever is greater) and can scale up to 4x that count under load (`CPPHTTPLIB_THREAD_POOL_MAX_COUNT`). When all base threads are busy, temporary threads are spawned automatically up to the maximum. These dynamic threads exit after an idle timeout (`CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT`, default 3 seconds).
This dynamic scaling helps accommodate WebSocket connections alongside HTTP requests. However, if you expect many simultaneous WebSocket connections, you should configure the thread pool accordingly:
```cpp
httplib::Server svr;
svr.new_task_queue = [] {
return new httplib::ThreadPool(/*base_threads=*/8, /*max_threads=*/128);
};
```
Choose sizes that account for both your expected HTTP load and the maximum number of simultaneous WebSocket connections.
### Calling from Multiple Threads
A single `WebSocket` (server-side) or `WebSocketClient` handle is shared by three potential callers: the thread running your handler (or holding the client), the heartbeat thread, and, if your code does its own thing, a separate thread calling `send()`/`close()` while another thread is blocked in `read()`.
**Supported**: calling `read()` from one thread while calling `send()`/`close()` from another. This is the common pattern for a client that reads incoming messages in a loop on one thread and sends from elsewhere (e.g. a UI thread). A message that is in flight when `close()` is called still arrives intact; `close()` sends the Close frame and returns, leaving the connection's read side to the thread that owns it, so it does not block waiting for the peer's Close reply in that case. The heartbeat thread's automatic pings use the same `send()` path internally, so they are safe to run concurrently with your `read()` loop too — for `wss://` this requires every TLS call on a connection to be serialized internally, which cpp-httplib does for you.
**Not supported**: calling `read()` from two threads at the same time on the same handle. The calls are serialized rather than left to corrupt each other, but which thread receives which message is unspecified, so there is nothing useful to build on it.
## Protocol
The implementation follows [RFC 6455](https://tools.ietf.org/html/rfc6455):
- Handshake via HTTP Upgrade with `Sec-WebSocket-Key` / `Sec-WebSocket-Accept`
- Subprotocol negotiation via `Sec-WebSocket-Protocol`
- Frame masking (client-to-server)
- Control frames: Close, Ping, Pong
- Message fragmentation and reassembly
- Close handshake with status codes
## Browser Test
Run the echo server example and open `http://localhost:8080` in a browser:
```bash
cd example && make wsecho && ./wsecho
```
+1465 -190
View File
File diff suppressed because it is too large Load Diff
-9
View File
@@ -1,9 +0,0 @@
version: 1.0.{build}
image: Visual Studio 2017
build_script:
- cmd: >-
cd test
msbuild.exe test.sln /verbosity:minimal /t:Build /p:Configuration=Debug;Platform=Win32
test_script:
- cmd: Debug\test.exe
+51
View File
@@ -0,0 +1,51 @@
CXXFLAGS = -O2 -I..
CPPHTTPLIB_CXXFLAGS = -std=c++11
CROW_CXXFLAGS = -std=c++17
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=$$!; $(WAIT); $(BENCH); kill $${PID}
@echo ""
monitor: server
@./server & export PID=$$!; $(MONITOR); kill $${PID}
run : server
@./server
server : cpp-httplib/main.cpp ../httplib.h
@g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib/main.cpp
# crow
bench-crow: server-crow
@echo "-------------\n Crow v1.3.1\n-------------\n"
@./server-crow & export PID=$$!; $(WAIT); $(BENCH); kill $${PID}
@echo ""
monitor-crow: server-crow
@./server-crow & export PID=$$!; $(MONITOR); kill $${PID}
run-crow : server-crow
@./server-crow
server-crow : crow/main.cpp crow/crow_all.h
@g++ -o $@ $(CXXFLAGS) $(CROW_CXXFLAGS) crow/main.cpp
# misc
build: server server-crow
bench-all: bench-crow bench
issue:
bombardier -c 10 -d 30s localhost:8080
clean:
rm -rf server*
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env bash
#
# A/B throughput comparison between two git refs.
#
# Usage: ./ab.sh [--base REF] [--head REF] [--rounds N] [--duration S]
# [--connections N] [--threads N] [--path PATH] [--tls]
# [--large-mib N] [--timeout S]
#
# --path selects the workload. The harness serves:
# / small body via set_content(); the response line, the
# headers and the body already share a single write(), so
# this is the least sensitive case
# /large large body via set_content()
# /static/small.js 1 KiB file from a mount point, where the headers and the
# body are two separate writes
# /static/large.bin same, with the body large enough to dominate
#
# --large-mib sizes the two large workloads (default 1).
#
# --tls runs the same workload over HTTPS, which writes through
# SSLSocketStream instead of SocketStream.
#
# --timeout is bombardier's per-request timeout. Its 2s default aborts large
# TLS responses, and the run then fails on the non-2xx check.
#
# Absolute numbers from a single run are meaningless: on a quiet 8-core laptop
# the same binary varies by +/-20% run to run, and shared CI runners are worse.
# So both refs are built and then measured alternately in the same session, and
# only the ratio of the medians is reported.
#
# Requires: bombardier, python3, g++ (or $CXX), git.
set -euo pipefail
BASE_REF="master"
HEAD_REF="HEAD"
ROUNDS=5
DURATION="5s"
CONNECTIONS=10
THREADS=""
PORT=8080
REQ_PATH="/"
TLS=0
LARGE_MIB=1
TIMEOUT="30s"
while [ $# -gt 0 ]; do
case "$1" in
--base) BASE_REF="$2"; shift 2 ;;
--head) HEAD_REF="$2"; shift 2 ;;
--rounds) ROUNDS="$2"; shift 2 ;;
--duration) DURATION="$2"; shift 2 ;;
--connections) CONNECTIONS="$2"; shift 2 ;;
--threads) THREADS="$2"; shift 2 ;;
--path) REQ_PATH="$2"; shift 2 ;;
--large-mib) LARGE_MIB="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--tls) TLS=1; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
command -v bombardier >/dev/null || { echo "Error: bombardier not found" >&2; exit 1; }
command -v python3 >/dev/null || { echo "Error: python3 not found" >&2; exit 1; }
REPO_ROOT=$(git rev-parse --show-toplevel)
CXX=${CXX:-g++}
# Default the thread pool to the core count. The committed benchmark Makefile
# hardcodes 16, which heavily oversubscribes a 2-4 vCPU CI runner and inflates
# the variance we are trying to see through.
if [ -z "$THREADS" ]; then
THREADS=$(python3 -c 'import os; print(os.cpu_count() or 4)')
fi
WORKDIR=$(mktemp -d)
cleanup() {
pkill -f "$WORKDIR/.*/server-ab" 2>/dev/null || true
git -C "$REPO_ROOT" worktree remove --force "$WORKDIR/base" 2>/dev/null || true
git -C "$REPO_ROOT" worktree remove --force "$WORKDIR/head" 2>/dev/null || true
rm -rf "$WORKDIR"
}
trap cleanup EXIT
BASE_SHA=$(git -C "$REPO_ROOT" rev-parse --short "$BASE_REF")
HEAD_SHA=$(git -C "$REPO_ROOT" rev-parse --short "$HEAD_REF")
echo "==> base: $BASE_REF ($BASE_SHA)"
echo "==> head: $HEAD_REF ($HEAD_SHA)"
echo "==> rounds=$ROUNDS duration=$DURATION connections=$CONNECTIONS threads=$THREADS"
echo "==> path=$REQ_PATH tls=$TLS large=${LARGE_MIB}MiB"
echo ""
if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
echo "Note: base and head are the same commit; this measures harness noise."
echo ""
fi
# --- Toolchain bits that depend on --tls ---
SCHEME="http"
INSECURE=""
TLS_CXXFLAGS=""
TLS_LDFLAGS=""
TLS_ARGS=""
if [ "$TLS" = "1" ]; then
SCHEME="https"
INSECURE="-k"
TLS_CXXFLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT"
TLS_LDFLAGS="-lssl -lcrypto"
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists openssl; then
TLS_CXXFLAGS="$TLS_CXXFLAGS $(pkg-config --cflags openssl)"
TLS_LDFLAGS="$(pkg-config --libs openssl)"
elif command -v brew >/dev/null 2>&1 && brew --prefix openssl >/dev/null 2>&1; then
OPENSSL_PREFIX=$(brew --prefix openssl)
TLS_CXXFLAGS="$TLS_CXXFLAGS -I$OPENSSL_PREFIX/include"
TLS_LDFLAGS="-L$OPENSSL_PREFIX/lib -lssl -lcrypto"
fi
if [ "$(uname -s)" = "Darwin" ]; then
TLS_LDFLAGS="$TLS_LDFLAGS -framework CoreFoundation -framework Security"
fi
TLS_ARGS="--cert $REPO_ROOT/test/cert.pem --key $REPO_ROOT/test/key.pem"
for f in "$REPO_ROOT/test/cert.pem" "$REPO_ROOT/test/key.pem"; do
[ -f "$f" ] || { echo "Error: $f not found" >&2; exit 1; }
done
fi
# --- Build both refs ---
# The harness source always comes from the invoking worktree, so both refs run
# an identical workload and a ref that predates a harness change stays
# measurable. Only httplib.h varies, through -I.
HARNESS="$REPO_ROOT/benchmark/cpp-httplib/main.cpp"
[ -f "$HARNESS" ] || { echo "Error: $HARNESS not found" >&2; exit 1; }
build() {
local name=$1 ref=$2
git -C "$REPO_ROOT" worktree add --detach --quiet "$WORKDIR/$name" "$ref"
"$CXX" -o "$WORKDIR/$name/server-ab" -O2 -std=c++11 \
-I"$WORKDIR/$name" \
-DCPPHTTPLIB_THREAD_POOL_COUNT="$THREADS" \
$TLS_CXXFLAGS \
"$HARNESS" -lpthread $TLS_LDFLAGS
}
echo "==> Building..."
build base "$BASE_REF"
build head "$HEAD_REF"
# --- Measure one ref once, echo rps ---
measure() {
local name=$1
local json rc
"$WORKDIR/$name/server-ab" --port "$PORT" --dir "$WORKDIR/$name-www" \
--large-mib "$LARGE_MIB" $TLS_ARGS >/dev/null 2>&1 &
local pid=$!
# Wait for the listener (no dependency on nc)
local i
for i in $(seq 1 200); do
if (exec 3<>/dev/tcp/127.0.0.1/$PORT) 2>/dev/null; then exec 3>&- 3<&-; break; fi
sleep 0.05
done
set +e
json=$(bombardier -c "$CONNECTIONS" -d "$DURATION" -t "$TIMEOUT" -o json -p r $INSECURE \
"$SCHEME://127.0.0.1:$PORT$REQ_PATH" 2>/dev/null)
rc=$?
set -e
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
# Wait for the port to be released before the next run
for i in $(seq 1 200); do
if ! (exec 3<>/dev/tcp/127.0.0.1/$PORT) 2>/dev/null; then break; fi
exec 3>&- 3<&-
sleep 0.05
done
if [ $rc -ne 0 ] || [ -z "$json" ]; then
echo "Error: bombardier failed for $name" >&2
exit 1
fi
python3 -c '
import json, sys
r = json.load(sys.stdin)["result"]
total = sum(r[k] for k in ("req1xx","req2xx","req3xx","req4xx","req5xx","others"))
bad = total - r["req2xx"]
if bad:
sys.stderr.write("Error: %d non-2xx/error responses\n" % bad)
sys.exit(1)
print("%.1f" % (total / r["timeTakenSeconds"]))
' <<<"$json"
}
# --- Alternate, flipping the order each round to cancel ordering bias ---
BASE_RESULTS=()
HEAD_RESULTS=()
echo ""
echo "==> Measuring..."
for ((r = 1; r <= ROUNDS; r++)); do
if (( r % 2 == 1 )); then order=("base" "head"); else order=("head" "base"); fi
line=" round $r:"
for name in "${order[@]}"; do
rps=$(measure "$name")
if [ "$name" = "base" ]; then BASE_RESULTS+=("$rps"); else HEAD_RESULTS+=("$rps"); fi
line="$line $name=$rps"
done
echo "$line"
done
# --- Report ---
SUMMARY=$(python3 -c '
import statistics, sys
from itertools import combinations
base = [float(x) for x in sys.argv[1].split()]
head = [float(x) for x in sys.argv[2].split()]
bm, hm = statistics.median(base), statistics.median(head)
def spread(v):
return (max(v) - min(v)) / statistics.median(v) * 100
def u_stat(a, b):
"""Mann-Whitney U: number of (a, b) pairs where a > b, ties count a half."""
return sum((x > y) + 0.5 * (x == y) for x in a for y in b)
def exact_p(a, b):
"""Two-sided permutation p-value. A single slow round cannot swing this
the way a min/max spread check can."""
n1, n2 = len(a), len(b)
pooled = a + b
observed = abs(u_stat(a, b) - n1 * n2 / 2)
total = extreme = 0
for idx in combinations(range(n1 + n2), n1):
s = set(idx)
ga = [pooled[i] for i in idx]
gb = [pooled[i] for i in range(n1 + n2) if i not in s]
total += 1
if abs(u_stat(ga, gb) - n1 * n2 / 2) >= observed:
extreme += 1
return extreme / total
print("| | median req/s | min | max | spread |")
print("|---|---|---|---|---|")
print("| base | %.0f | %.0f | %.0f | %.1f%% |" % (bm, min(base), max(base), spread(base)))
print("| head | %.0f | %.0f | %.0f | %.1f%% |" % (hm, min(head), max(head), spread(head)))
print("")
print("**ratio: %.3fx** (%+.1f%%)" % (hm / bm, (hm / bm - 1) * 100))
print("")
if len(base) + len(head) > 20:
print("> %d rounds: skipping the permutation test (too many combinations)."
% len(base))
else:
p = exact_p(base, head)
if p <= 0.05:
print("> Separation is consistent across rounds (permutation p = %.3f)." % p)
else:
print("> Not separated from noise (permutation p = %.3f). Inconclusive;" % p)
print("> raise --rounds or --duration, or run on a quieter machine.")
min_p = exact_p(list(range(len(base))),
list(range(len(base), len(base) + len(head))))
if min_p > 0.05:
print(">")
print("> With %d rounds even perfect separation only reaches p = %.3f,"
% (len(base), min_p))
print("> so this test can never call a win. Use --rounds 4 or more.")
' "${BASE_RESULTS[*]}" "${HEAD_RESULTS[*]}")
echo ""
echo "$SUMMARY"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
{
echo "## Benchmark A/B"
echo ""
echo "- base: \`$BASE_REF\` ($BASE_SHA)"
echo "- head: \`$HEAD_REF\` ($HEAD_SHA)"
echo "- rounds=$ROUNDS duration=$DURATION connections=$CONNECTIONS threads=$THREADS"
echo ""
echo "$SUMMARY"
} >> "$GITHUB_STEP_SUMMARY"
fi
+139
View File
@@ -0,0 +1,139 @@
#include "httplib.h"
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#ifndef _WIN32
#include <sys/stat.h>
#include <sys/types.h>
#endif
using namespace httplib;
namespace {
// The workloads differ in which part of the write path they exercise:
//
// / small body set through set_content(); the response line,
// the headers and the body already share a single write()
// /large large body set through set_content(); the body is copied
// into the header buffer before that single write().
// --large-mib sets its size (and large.bin's).
// /static/small.js small file served from a mount point, where the headers
// and the body are two separate writes
// /static/large.bin same, with the body large enough to dominate
//
// Bodies are generated at startup so the repository carries no fixtures.
const size_t SMALL_SIZE = 1024;
const size_t LARGE_SIZE_DEFAULT_MIB = 1;
std::string filler(size_t n) {
std::string s;
s.reserve(n);
while (s.size() < n) {
s += "0123456789abcdef";
}
s.resize(n);
return s;
}
bool write_file(const std::string &path, const std::string &content) {
std::ofstream f(path.c_str(), std::ios::binary);
f.write(content.data(), static_cast<std::streamsize>(content.size()));
return f.good();
}
std::string default_dir() {
const char *tmp = std::getenv("TMPDIR");
std::string base = tmp && *tmp ? tmp : "/tmp";
if (!base.empty() && base[base.size() - 1] == '/') {
base.erase(base.size() - 1);
}
return base + "/cpp-httplib-bench";
}
bool make_dir(const std::string &path) {
#ifdef _WIN32
return _mkdir(path.c_str()) == 0 || errno == EEXIST;
#else
return ::mkdir(path.c_str(), 0755) == 0 || errno == EEXIST;
#endif
}
void setup(Server &svr, const std::string &large, const std::string &dir) {
svr.Get("/", [](const Request &, Response &res) {
res.set_content("Hello World!", "text/plain");
});
svr.Get("/large", [&large](const Request &, Response &res) {
res.set_content(large, "application/octet-stream");
});
svr.set_mount_point("/static", dir);
}
} // namespace
int main(int argc, char *argv[]) {
int port = 8080;
std::string dir = default_dir();
std::string cert;
std::string key;
size_t large_mib = LARGE_SIZE_DEFAULT_MIB;
for (int i = 1; i < argc; i++) {
auto last = i + 1 < argc;
if (!std::strcmp(argv[i], "--port") && last) {
port = std::atoi(argv[++i]);
} else if (!std::strcmp(argv[i], "--large-mib") && last) {
large_mib = static_cast<size_t>(std::atoi(argv[++i]));
} else if (!std::strcmp(argv[i], "--dir") && last) {
dir = argv[++i];
} else if (!std::strcmp(argv[i], "--cert") && last) {
cert = argv[++i];
} else if (!std::strcmp(argv[i], "--key") && last) {
key = argv[++i];
} else {
std::fprintf(stderr,
"usage: %s [--port N] [--dir PATH] [--large-mib N]"
" [--cert PATH --key PATH]\n",
argv[0]);
return 2;
}
}
if (!make_dir(dir)) {
std::fprintf(stderr, "cannot create %s\n", dir.c_str());
return 1;
}
auto large = filler(large_mib * 1024 * 1024);
if (!write_file(dir + "/small.js", filler(SMALL_SIZE)) ||
!write_file(dir + "/large.bin", large)) {
std::fprintf(stderr, "cannot write fixtures under %s\n", dir.c_str());
return 1;
}
if (!cert.empty()) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
SSLServer svr(cert.c_str(), key.c_str());
if (!svr.is_valid()) {
std::fprintf(stderr, "cannot load %s / %s\n", cert.c_str(), key.c_str());
return 1;
}
setup(svr, large, dir);
svr.listen("0.0.0.0", port);
return 0;
#else
std::fprintf(stderr, "built without CPPHTTPLIB_OPENSSL_SUPPORT\n");
return 1;
#endif
}
Server svr;
setup(svr, large, dir);
svr.listen("0.0.0.0", port);
}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
#include "crow_all.h"
class CustomLogger : public crow::ILogHandler {
public:
void log(const std::string &, crow::LogLevel) {}
};
int main() {
CustomLogger logger;
crow::logger::setHandler(&logger);
crow::SimpleApp app;
CROW_ROUTE(app, "/")([]() { return "Hello world!"; });
app.port(8080).multithreaded().run();
}
+173
View File
@@ -0,0 +1,173 @@
# A simple FindBrotli package for Cmake's find_package function.
# Note: This find package doesn't have version support, as the version file doesn't seem to be installed on most systems.
#
# If you want to find the static packages instead of shared (the default), define BROTLI_USE_STATIC_LIBS as TRUE.
# The targets will have the same names, but it will use the static libs.
#
# Valid find_package COMPONENTS names: "decoder", "encoder", and "common"
# Note that if you're requiring "decoder" or "encoder", then "common" will be automatically added as required.
#
# Defines the libraries (if found): Brotli::decoder, Brotli::encoder, Brotli::common
# and the includes path variable: Brotli_INCLUDE_DIR
#
# If it's failing to find the libraries, try setting BROTLI_ROOT_DIR to the folder containing your library & include dir.
# If they asked for a specific version, warn/fail since we don't support it.
# TODO: if they start distributing the version somewhere, implement finding it.
# See https://github.com/google/brotli/issues/773#issuecomment-579133187
if(Brotli_FIND_VERSION)
set(_brotli_version_error_msg "FindBrotli.cmake doesn't have version support!")
# If the package is required, throw a fatal error
# Otherwise, if not running quietly, we throw a warning
if(Brotli_FIND_REQUIRED)
message(FATAL_ERROR "${_brotli_version_error_msg}")
elseif(NOT Brotli_FIND_QUIETLY)
message(WARNING "${_brotli_version_error_msg}")
endif()
endif()
# Since both decoder & encoder require the common lib, force its requirement..
# if the user is requiring either of those other libs.
if(Brotli_FIND_REQUIRED_decoder OR Brotli_FIND_REQUIRED_encoder)
set(Brotli_FIND_REQUIRED_common TRUE)
endif()
# Support preference of static libs by adjusting CMAKE_FIND_LIBRARY_SUFFIXES
# Credit to FindOpenSSL.cmake for this
if(BROTLI_USE_STATIC_LIBS)
set(_brotli_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
if(WIN32)
set(CMAKE_FIND_LIBRARY_SUFFIXES .lib .a ${CMAKE_FIND_LIBRARY_SUFFIXES})
else()
set(CMAKE_FIND_LIBRARY_SUFFIXES .a)
endif()
endif()
# Make PkgConfig optional, since some users (mainly Windows) don't have it.
# But it's a lot more clean than manually using find_library.
find_package(PkgConfig QUIET)
# Only used if the PkgConfig libraries aren't used.
find_path(Brotli_INCLUDE_DIR
NAMES
"brotli/decode.h"
"brotli/encode.h"
HINTS
${BROTLI_ROOT_DIR}
PATH_SUFFIXES
"include"
"includes"
DOC "The path to Brotli's include directory."
)
# Hides this var from the GUI
mark_as_advanced(Brotli_INCLUDE_DIR)
# Just used for PkgConfig stuff in the loop below
set(_brotli_stat_str "")
if(BROTLI_USE_STATIC_LIBS)
set(_brotli_stat_str "_STATIC")
endif()
# Each string here is "ComponentName;LiteralName" (the semi-colon is a delimiter)
foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
# Split the component name and literal library name from the listvar
list(GET _listvar 0 _component_name)
list(GET _listvar 1 _libname)
# NOTE: We can't rely on PkgConf for static libs since the upstream static lib support is broken
# See https://github.com/google/brotli/issues/795
# TODO: whenever their issue is fixed upstream, remove this "AND NOT BROTLI_USE_STATIC_LIBS" check
if(PKG_CONFIG_FOUND AND NOT BROTLI_USE_STATIC_LIBS)
# These need to be GLOBAL for MinGW when making ALIAS libraries against them.
# Have to postfix _STATIC on the name to tell PkgConfig to find the static libs.
pkg_check_modules(Brotli_${_component_name}${_brotli_stat_str} QUIET GLOBAL IMPORTED_TARGET libbrotli${_libname})
endif()
# Check if the target was already found by Pkgconf
if(TARGET PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
# ALIAS since we don't want the PkgConfig namespace on the Cmake library (for end-users)
if (NOT TARGET Brotli::${_component_name})
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
endif()
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND TRUE)
if(Brotli_FIND_REQUIRED_${_component_name})
# If the lib is required, we can add its literal path as a required var for FindPackageHandleStandardArgs
# Since it won't accept the PkgConfig targets
if(BROTLI_USE_STATIC_LIBS)
list(APPEND _brotli_req_vars "Brotli_${_component_name}_STATIC_LIBRARIES")
else()
list(APPEND _brotli_req_vars "Brotli_${_component_name}_LINK_LIBRARIES")
endif()
endif()
# Skip searching for the libs with find_library since it was already found by Pkgconf
continue()
endif()
if(Brotli_FIND_REQUIRED_${_component_name})
# If it's required, we can set the name used in find_library as a required var for FindPackageHandleStandardArgs
list(APPEND _brotli_req_vars "Brotli_${_component_name}")
endif()
list(APPEND _brotli_lib_names
"brotli${_libname}"
"libbrotli${_libname}"
)
if(BROTLI_USE_STATIC_LIBS)
# Postfix "-static" to the libnames since we're looking for static libs
list(TRANSFORM _brotli_lib_names APPEND "-static")
endif()
find_library(Brotli_${_component_name}
NAMES ${_brotli_lib_names}
HINTS ${BROTLI_ROOT_DIR}
PATH_SUFFIXES
"lib"
"lib64"
"libs"
"libs64"
"lib/x86_64-linux-gnu"
)
# Hide the library variable from the Cmake GUI
mark_as_advanced(Brotli_${_component_name})
# Unset since otherwise it'll stick around for the next loop and break things
unset(_brotli_lib_names)
# Check if find_library found the library
if(Brotli_${_component_name})
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND TRUE)
if (NOT TARGET Brotli::${_component_name})
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
endif()
# Attach the literal library and include dir to the IMPORTED target for the end-user
set_target_properties("Brotli::${_component_name}" PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${Brotli_INCLUDE_DIR}"
IMPORTED_LOCATION "${Brotli_${_component_name}}"
)
else()
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND FALSE)
endif()
endforeach()
include(FindPackageHandleStandardArgs)
# Sets Brotli_FOUND, and fails the find_package(Brotli) call if it was REQUIRED but missing libs.
find_package_handle_standard_args(Brotli
FOUND_VAR
Brotli_FOUND
REQUIRED_VARS
Brotli_INCLUDE_DIR
${_brotli_req_vars}
HANDLE_COMPONENTS
)
# Restore the original find library ordering
if(BROTLI_USE_STATIC_LIBS)
set(CMAKE_FIND_LIBRARY_SUFFIXES ${_brotli_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES})
endif()
+119
View File
@@ -0,0 +1,119 @@
# Generates a macro to auto-configure everything
@PACKAGE_INIT@
# Setting these here so they're accessible after install.
# Might be useful for some users to check which settings were used.
set(HTTPLIB_IS_USING_OPENSSL @HTTPLIB_IS_USING_OPENSSL@)
set(HTTPLIB_IS_USING_WOLFSSL @HTTPLIB_IS_USING_WOLFSSL@)
set(HTTPLIB_IS_USING_MBEDTLS @HTTPLIB_IS_USING_MBEDTLS@)
set(HTTPLIB_IS_USING_ZLIB @HTTPLIB_IS_USING_ZLIB@)
set(HTTPLIB_IS_COMPILED @HTTPLIB_COMPILE@)
set(HTTPLIB_IS_USING_BROTLI @HTTPLIB_IS_USING_BROTLI@)
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO @HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO@)
set(HTTPLIB_VERSION @PROJECT_VERSION@)
include(CMakeFindDependencyMacro)
# We add find_dependency calls here to not make the end-user have to call them.
find_dependency(Threads)
if(@HTTPLIB_IS_USING_OPENSSL@)
# OpenSSL COMPONENTS were added in Cmake v3.11
if(CMAKE_VERSION VERSION_LESS "3.11")
find_dependency(OpenSSL @_HTTPLIB_OPENSSL_MIN_VER@)
else()
# Once the COMPONENTS were added, they were made optional when not specified.
# Since we use both, we need to search for both.
find_dependency(OpenSSL @_HTTPLIB_OPENSSL_MIN_VER@ COMPONENTS Crypto SSL)
endif()
set(httplib_OpenSSL_FOUND ${OpenSSL_FOUND})
endif()
if(@HTTPLIB_IS_USING_ZLIB@)
find_dependency(ZLIB)
set(httplib_ZLIB_FOUND ${ZLIB_FOUND})
endif()
if(@HTTPLIB_IS_USING_WOLFSSL@)
find_dependency(wolfssl)
set(httplib_wolfssl_FOUND ${wolfssl_FOUND})
endif()
if(@HTTPLIB_IS_USING_MBEDTLS@)
find_dependency(MbedTLS)
set(httplib_MbedTLS_FOUND ${MbedTLS_FOUND})
endif()
if(@HTTPLIB_IS_USING_BROTLI@)
# Needed so we can use our own FindBrotli.cmake in this file.
# Note that the FindBrotli.cmake file is installed in the same dir as this file.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")
set(BROTLI_USE_STATIC_LIBS @BROTLI_USE_STATIC_LIBS@)
find_dependency(Brotli COMPONENTS common encoder decoder)
set(httplib_Brotli_FOUND ${Brotli_FOUND})
endif()
if(@HTTPLIB_IS_USING_ZSTD@)
set(httplib_fd_zstd_quiet_arg)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_QUIETLY)
set(httplib_fd_zstd_quiet_arg QUIET)
endif()
set(httplib_fd_zstd_required_arg)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
set(httplib_fd_zstd_required_arg REQUIRED)
endif()
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)
pkg_check_modules(zstd ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg} IMPORTED_TARGET libzstd)
if(TARGET PkgConfig::zstd)
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
endif()
endif()
endif()
set(httplib_zstd_FOUND ${zstd_FOUND})
endif()
# Mildly useful for end-users
# Not really recommended to be used though
set_and_check(HTTPLIB_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_FULL_INCLUDEDIR@")
# Lets the end-user find the header path with the header appended
# This is helpful if you're using Cmake's pre-compiled header feature
set_and_check(HTTPLIB_HEADER_PATH "@PACKAGE_CMAKE_INSTALL_FULL_INCLUDEDIR@/httplib.h")
check_required_components(httplib)
# Brings in the target library, but only if all required components are found
if(NOT DEFINED httplib_FOUND OR httplib_FOUND)
include("${CMAKE_CURRENT_LIST_DIR}/httplibTargets.cmake")
endif()
# Outputs a "found httplib /usr/include/httplib.h" message when using find_package(httplib)
include(FindPackageMessage)
if(TARGET httplib::httplib)
set(HTTPLIB_FOUND TRUE)
# Since the compiled version has a lib, show that in the message
if(@HTTPLIB_COMPILE@)
# The list of configurations is most likely just 1 unless they installed a debug & release
get_target_property(_httplib_configs httplib::httplib "IMPORTED_CONFIGURATIONS")
# Need to loop since the "IMPORTED_LOCATION" property isn't want we want.
# Instead, we need to find the IMPORTED_LOCATION_RELEASE or IMPORTED_LOCATION_DEBUG which has the lib path.
foreach(_httplib_conf "${_httplib_configs}")
# Grab the path to the lib and sets it to HTTPLIB_LIBRARY
get_target_property(HTTPLIB_LIBRARY httplib::httplib "IMPORTED_LOCATION_${_httplib_conf}")
# Check if we found it
if(HTTPLIB_LIBRARY)
break()
endif()
endforeach()
unset(_httplib_configs)
unset(_httplib_conf)
find_package_message(httplib "Found httplib: ${HTTPLIB_LIBRARY} (found version \"${HTTPLIB_VERSION}\")" "[${HTTPLIB_LIBRARY}][${HTTPLIB_HEADER_PATH}]")
else()
find_package_message(httplib "Found httplib: ${HTTPLIB_HEADER_PATH} (found version \"${HTTPLIB_VERSION}\")" "[${HTTPLIB_HEADER_PATH}]")
endif()
endif()
+19
View File
@@ -0,0 +1,19 @@
# This file contains C++20 module support requiring CMake 3.28+
# Included conditionally to prevent parse errors on older CMake versions
if(HTTPLIB_BUILD_MODULES)
if(POLICY CMP0155)
cmake_policy(SET CMP0155 NEW)
endif()
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
target_sources(${PROJECT_NAME}
PUBLIC
FILE_SET CXX_MODULES
BASE_DIRS
"${_httplib_build_includedir}"
FILES
"${_httplib_build_includedir}/httplib.cppm"
)
endif()
+7
View File
@@ -0,0 +1,7 @@
services:
http:
build: .
ports:
- "8080:80"
volumes:
- ./docker/html:/html
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Welcome to cpp-httplib!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to cpp-httplib!</h1>
<p>If you see this page, the cpp-httplib web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="https://github.com/yhirose/cpp-httplib">github.com/yhirose/cpp-httplib</a>.<br/>
<p><em>Thank you for using cpp-httplib.</em></p>
</body>
</html>
+323
View File
@@ -0,0 +1,323 @@
//
// main.cc
//
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
#include <atomic>
#include <chrono>
#include <ctime>
#include <format>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <sstream>
#include <httplib.h>
using namespace httplib;
const auto SERVER_NAME =
std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION);
Server svr;
void signal_handler(int signal) {
if (signal == SIGINT || signal == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
std::string get_time_format() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%d/%b/%Y:%H:%M:%S %z");
return ss.str();
}
std::string get_error_time_format() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
return ss.str();
}
// Escape a value for a log line the way NGINX does: '"', '\\', control
// bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
// (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
// writing them verbatim would let a client forge extra log lines.
std::string escape_log(const std::string &s) {
std::string out;
out.reserve(s.size());
for (unsigned char c : s) {
if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
out += std::format("\\x{:02X}", c);
} else {
out += static_cast<char>(c);
}
}
return out;
}
// NGINX Combined log format:
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
// "$http_referer" "$http_user_agent"
void nginx_access_logger(const Request &req, const Response &res) {
std::string remote_user =
"-"; // cpp-httplib doesn't have built-in auth user tracking
auto time_local = get_time_format();
// $request is the original request line, so log the raw target rather than
// the percent-decoded req.path.
auto request = std::format("{} {} {}", req.method, req.target, req.version);
auto status = res.status;
auto body_bytes_sent = res.body.size();
auto http_referer = req.get_header_value("Referer");
if (http_referer.empty()) http_referer = "-";
auto http_user_agent = req.get_header_value("User-Agent");
if (http_user_agent.empty()) http_user_agent = "-";
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
req.remote_addr, remote_user, time_local,
escape_log(request), status, body_bytes_sent,
escape_log(http_referer), escape_log(http_user_agent))
<< std::endl;
}
// NGINX Error log format:
// YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request",
// host: "host"
void nginx_error_logger(const Error &err, const Request *req) {
auto time_local = get_error_time_format();
std::string level = "error";
if (req) {
auto request =
std::format("{} {} {}", req->method, req->target, req->version);
auto host = req->get_header_value("Host");
if (host.empty()) host = "-";
std::cerr << std::format("{} [{}] {}, client: {}, request: "
"\"{}\", host: \"{}\"",
time_local, level, to_string(err),
req->remote_addr, escape_log(request),
escape_log(host))
<< std::endl;
} else {
// If no request context, just log the error
std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err))
<< std::endl;
}
}
void print_usage(const char *program_name) {
std::cout << "Usage: " << program_name << " [OPTIONS]" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --host <hostname> Server hostname (default: localhost)"
<< std::endl;
std::cout << " --port <port> Server port (default: 8080)"
<< std::endl;
std::cout << " --mount <mount:path> Mount point and document root"
<< std::endl;
std::cout << " Format: mount_point:document_root"
<< std::endl;
std::cout << " (default: /:./html)" << std::endl;
std::cout << " --trusted-proxy <ip> Add trusted proxy IP address"
<< std::endl;
std::cout << " (can be specified multiple times)"
<< std::endl;
std::cout << " --version Show version information"
<< std::endl;
std::cout << " --help Show this help message" << std::endl;
std::cout << std::endl;
std::cout << "Examples:" << std::endl;
std::cout << " " << program_name
<< " --host localhost --port 8080 --mount /:./html" << std::endl;
std::cout << " " << program_name
<< " --host 0.0.0.0 --port 3000 --mount /api:./api" << std::endl;
std::cout << " " << program_name
<< " --trusted-proxy 192.168.1.100 --trusted-proxy 10.0.0.1"
<< std::endl;
}
struct ServerConfig {
std::string hostname = "localhost";
int port = 8080;
std::string mount_point = "/";
std::string document_root = "./html";
std::vector<std::string> trusted_proxies;
};
enum class ParseResult { SUCCESS, HELP_REQUESTED, VERSION_REQUESTED, ERROR };
ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
print_usage(argv[0]);
return ParseResult::HELP_REQUESTED;
} else if (strcmp(argv[i], "--host") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --host requires a hostname argument" << std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.hostname = argv[++i];
} else if (strcmp(argv[i], "--port") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --port requires a port number argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.port = std::atoi(argv[++i]);
if (config.port <= 0 || config.port > 65535) {
std::cerr << "Error: Invalid port number. Must be between 1 and 65535"
<< std::endl;
return ParseResult::ERROR;
}
} else if (strcmp(argv[i], "--mount") == 0) {
if (i + 1 >= argc) {
std::cerr
<< "Error: --mount requires mount_point:document_root argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
std::string mount_arg = argv[++i];
auto colon_pos = mount_arg.find(':');
if (colon_pos == std::string::npos) {
std::cerr << "Error: --mount argument must be in format "
"mount_point:document_root"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.mount_point = mount_arg.substr(0, colon_pos);
config.document_root = mount_arg.substr(colon_pos + 1);
if (config.mount_point.empty() || config.document_root.empty()) {
std::cerr
<< "Error: Both mount_point and document_root must be non-empty"
<< std::endl;
return ParseResult::ERROR;
}
} else if (strcmp(argv[i], "--version") == 0) {
std::cout << CPPHTTPLIB_VERSION << std::endl;
return ParseResult::VERSION_REQUESTED;
} else if (strcmp(argv[i], "--trusted-proxy") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --trusted-proxy requires an IP address argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.trusted_proxies.push_back(argv[++i]);
} else {
std::cerr << "Error: Unknown option '" << argv[i] << "'" << std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
}
return ParseResult::SUCCESS;
}
bool setup_server(Server &svr, const ServerConfig &config) {
svr.set_logger(nginx_access_logger);
svr.set_error_logger(nginx_error_logger);
// Set trusted proxies if specified
if (!config.trusted_proxies.empty()) {
svr.set_trusted_proxies(config.trusted_proxies);
}
auto ret = svr.set_mount_point(config.mount_point, config.document_root);
if (!ret) {
std::cerr
<< std::format(
"Error: Cannot mount '{}' to '{}'. Directory may not exist.",
config.mount_point, config.document_root)
<< std::endl;
return false;
}
svr.set_file_extension_and_mimetype_mapping("html", "text/html");
svr.set_file_extension_and_mimetype_mapping("htm", "text/html");
svr.set_file_extension_and_mimetype_mapping("css", "text/css");
svr.set_file_extension_and_mimetype_mapping("js", "text/javascript");
svr.set_file_extension_and_mimetype_mapping("json", "application/json");
svr.set_file_extension_and_mimetype_mapping("xml", "application/xml");
svr.set_file_extension_and_mimetype_mapping("png", "image/png");
svr.set_file_extension_and_mimetype_mapping("jpg", "image/jpeg");
svr.set_file_extension_and_mimetype_mapping("jpeg", "image/jpeg");
svr.set_file_extension_and_mimetype_mapping("gif", "image/gif");
svr.set_file_extension_and_mimetype_mapping("svg", "image/svg+xml");
svr.set_file_extension_and_mimetype_mapping("ico", "image/x-icon");
svr.set_file_extension_and_mimetype_mapping("pdf", "application/pdf");
svr.set_file_extension_and_mimetype_mapping("zip", "application/zip");
svr.set_file_extension_and_mimetype_mapping("txt", "text/plain");
svr.set_error_handler([](const Request & /*req*/, Response &res) {
if (res.status == 404) {
res.set_content(
std::format(
"<html><head><title>404 Not Found</title></head>"
"<body><h1>404 Not Found</h1>"
"<p>The requested resource was not found on this server.</p>"
"<hr><p>{}</p></body></html>",
SERVER_NAME),
"text/html");
}
});
svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
res.set_header("Server", SERVER_NAME);
return Server::HandlerResponse::Unhandled;
});
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
return true;
}
int main(int argc, char *argv[]) {
ServerConfig config;
auto result = parse_command_line(argc, argv, config);
switch (result) {
case ParseResult::HELP_REQUESTED:
case ParseResult::VERSION_REQUESTED: return 0;
case ParseResult::ERROR: return 1;
case ParseResult::SUCCESS: break;
}
if (!setup_server(svr, config)) { return 1; }
std::cout << "Serving HTTP on " << config.hostname << ":" << config.port
<< std::endl;
std::cout << "Mount point: " << config.mount_point << " -> "
<< config.document_root << std::endl;
if (!config.trusted_proxies.empty()) {
std::cout << "Trusted proxies: ";
for (size_t i = 0; i < config.trusted_proxies.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << config.trusted_proxies[i];
}
std::cout << std::endl;
}
std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl;
auto ret = svr.listen(config.hostname, config.port);
std::cout << "Server has been shut down." << std::endl;
return ret ? 0 : 1;
}
+30
View File
@@ -0,0 +1,30 @@
[system]
theme = "monotone"
langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.60.1"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
[[nav]]
label = "Tour"
path = "tour/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>'
[[nav]]
label = "Cookbook"
path = "cookbook/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
[[nav]]
label = "LLM App"
path = "llm-app/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
[[nav]]
label = "GitHub"
url = "https://github.com/yhirose/cpp-httplib"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>'
@@ -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).
+36
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).
@@ -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).
@@ -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.
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -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).
@@ -0,0 +1,52 @@
---
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).
> For WebSocket client timeouts, see [W06. Set Timeouts](../w06-websocket-timeouts).
@@ -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.
@@ -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.
@@ -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.
+87
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_`.
@@ -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).
@@ -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).
@@ -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.
@@ -0,0 +1,87 @@
---
title: "E01. Implement an SSE Server"
order: 48
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).
@@ -0,0 +1,84 @@
---
title: "E02. Use Named Events in SSE"
order: 49
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.
@@ -0,0 +1,85 @@
---
title: "E03. Handle SSE Reconnection"
order: 50
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.
@@ -0,0 +1,99 @@
---
title: "E04. Receive SSE on the Client"
order: 51
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).
+101
View File
@@ -0,0 +1,101 @@
---
title: "Cookbook"
order: 0
status: "draft"
---
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
- [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
- [C05. Use Basic authentication](c05-basic-auth)
- [C06. Call an API with a Bearer token](c06-bearer-token)
### File Upload
- [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
- [C10. Receive a response as a stream](c10-stream-response)
- [C11. Use the progress callback](c11-progress-callback)
### Connection & Performance
- [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
- [C17. Handle error codes](c17-error-codes)
- [C18. Handle SSL errors](c18-ssl-errors)
- [C19. Set up client logging](c19-client-logger)
## Server
### Basics
- [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
- [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
- [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
- [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
- [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)
### Protocol Extensions
- [S23. Handle custom HTTP methods](s23-custom-methods)
## TLS / Security
- [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
- [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
- [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)
- [W05. Configure TLS for wss:// connections](w05-websocket-tls)
- [W06. Set timeouts](w06-websocket-timeouts)
@@ -0,0 +1,68 @@
---
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()`. For methods outside the built-in set, such as WebDAV's `PROPFIND`, use `CustomRoute()`.
## 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).
> For methods outside the built-in set, such as WebDAV's `PROPFIND`, see [S23. Handle custom HTTP methods](../s23-custom-methods).
@@ -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).
@@ -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.
@@ -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).
@@ -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).
@@ -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.
@@ -0,0 +1,103 @@
---
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.
## Count the parts yourself
There is a cap on the number of parts, `CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (1024 by default), but it only applies to the buffered path, where every part is accumulated into `req.form`. The `ContentReader` keeps nothing on the library side, so the cap does not apply here.
If you want an upper bound, count the parts yourself and return `false` from the header callback. The parser stops right there.
```cpp
svr.Post("/upload",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
size_t count = 0;
auto ok = content_reader(
[&](const httplib::FormData &file) {
if (++count > 100) { return false; } // stop here
return true;
},
[&](const char *data, size_t len) {
return true;
});
if (!ok) {
res.status = httplib::StatusCode::BadRequest_400;
return;
}
res.set_content("ok", "text/plain");
});
```
When `content_reader` returns `false`, set the response status yourself. The rest of the body is left unread and the connection is closed, so a client that is still sending sees the connection drop.
> **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).
@@ -0,0 +1,78 @@
---
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) {
// ...
});
});
```
## Static files need to be opted in
Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:
```cpp
svr.set_static_file_compression(true);
```
Only files within a size range are compressed, and both ends of it can be moved:
```cpp
svr.set_static_file_compression_min_length(512);
svr.set_static_file_compression_max_length(1024 * 1024);
```
The lower bound defaults to 1400 bytes. A response that already fits in a single 1500-byte MTU is not delivered any faster for being smaller, and a file of a few bytes comes back larger than it went in, because gzip's header and trailer outweigh what deflate saves.
The upper bound defaults to 4MB and exists for a different reason: the file is compressed on every request, and the compressed bytes stay in memory until the response has been written, so the peak cost scales with the number of requests in flight. It bounds what a single request can cost, and says nothing about how well large files compress, so raising it is reasonable when the files are known and the traffic is not.
Either bound takes `0` to turn it off, and each has a compile-time default (`CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH`, `CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH`).
A compressed response keeps its `Content-Length`, so `HEAD` reports the same size a `GET` would. Two details to know: Range requests are answered from the uncompressed representation, and the `ETag` carries the coding it belongs to, as in `W/"...-gzip"`.
Content providers registered with `set_content_provider()` are not covered. Running one through a compressor holds each write back until the internal buffer fills, which stalls providers that build their body a piece at a time. To compress a generated body, use `set_chunked_content_provider()`.
> **Note:** The size range covers static files only. A body passed to `set_content()` is compressed whenever the client accepts it and the MIME type is compressible, however small it is, so a response of a few bytes ends up larger than it started. Decide in the handler if you want to avoid that.
> For the client-side counterpart, see [C15. Enable compression](../c15-compression).
@@ -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).
@@ -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.
@@ -0,0 +1,51 @@
---
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.
The pre-request handler also runs for routes registered with `svr.WebSocket()`. It is called before the `101 Switching Protocols` response, so returning `Handled` sends your HTTP response (such as a 403) and the connection is never upgraded.
## 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).
@@ -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.
@@ -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).
@@ -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.
@@ -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.
@@ -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."
@@ -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).
@@ -0,0 +1,82 @@
---
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, for example when you don't have permission to bind to the port. Always check it.
```cpp
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "bind failed" << std::endl;
return 1;
}
```
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
## Detect a port that's already in use
With the default settings, you can actually bind to a port another server is already using. That's because cpp-httplib sets `SO_REUSEPORT` (Linux, macOS) or `SO_REUSEADDR` (Windows) on the server socket. A restarted server can bind again right away. The flip side is that a second server on the same port starts without an error, and connections get split between the two.
To make `bind_to_port()` fail on a port in use, replace the socket options with `set_socket_options()`.
```cpp
svr.set_socket_options([](socket_t sock) {
#ifdef _WIN32
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
#else
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
#endif
});
if (!svr.bind_to_port("0.0.0.0", 8080)) {
std::cerr << "port already in use" << std::endl;
return 1;
}
```
`set_socket_options()` replaces the defaults entirely. Setting `SO_REUSEADDR` on Linux and macOS keeps the "restarted server can bind again right away" behavior.
> **Note:** `SO_REUSEADDR` alone isn't enough on Windows. Two sockets that both set it can bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
> **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()`.
@@ -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.
@@ -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.
@@ -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)`).
@@ -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.
@@ -0,0 +1,59 @@
---
title: "S23. Handle custom HTTP methods"
order: 42
status: "draft"
---
The server rejects HTTP methods it does not know with `400 Bad Request`. To accept an extension method, such as the WebDAV methods of RFC 4918 (`PROPFIND`, `PROPPATCH`, `MKCOL` and friends) or UPnP's `SUBSCRIBE`, register a handler with `CustomRoute()`. Registering the handler is what makes the server accept the method.
## Basic usage
```cpp
svr.CustomRoute("PROPFIND", "/dav/:id",
[](const httplib::Request &req, httplib::Response &res) {
// The request body is available as usual
auto id = req.path_params.at("id");
res.status = httplib::StatusCode::MultiStatus_207;
res.set_content(build_multistatus(req.body), "application/xml");
});
```
Patterns work the same way as they do for `Get()`. Regular expressions and path parameters are both available.
## Advertise your methods with OPTIONS
A WebDAV client asks the server about its capabilities with `OPTIONS` before doing anything else. cpp-httplib generates neither the `DAV:` header nor `Allow`, so return them yourself. Forget this and clients will turn you away even though your `PROPFIND` works.
```cpp
svr.Options("/dav/.*", [](const httplib::Request &req, httplib::Response &res) {
res.set_header("DAV", "1");
res.set_header("Allow", "OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL");
});
```
## Read the body as a stream
There is a content reader overload, just like the one on `Post()`. Use it when you would rather not hold a large XML document in memory all at once.
```cpp
svr.CustomRoute("REPORT", "/dav/.*",
[](const httplib::Request &req, httplib::Response &res,
const httplib::ContentReader &content_reader) {
content_reader([&](const char *data, size_t data_length) {
// Process it a chunk at a time
return true;
});
res.status = httplib::StatusCode::MultiStatus_207;
});
```
## Things to keep in mind
- The method name has to be a valid HTTP method token (RFC 9110), and it must be registered before you call `listen()`
- `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH` and `PRI` cannot be registered here. Use the dedicated methods for those
- A rejected registration makes `is_valid()` return `false` and `listen()` fail, so the server never starts holding a handler that would never run
- Static file serving and WebSocket upgrades stay `GET`/`HEAD` only
> **Note:** cpp-httplib takes you as far as routing the method. If you want to call it WebDAV, generating the `207 Multi-Status` XML, interpreting the `Depth` header and managing locks are all yours to implement. The protocol itself lives outside the library.
> For the basics of registering handlers, see [S01. Register GET / POST / PUT / DELETE handlers](../s01-handlers).
@@ -0,0 +1,49 @@
---
title: "T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL"
order: 43
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`.
@@ -0,0 +1,55 @@
---
title: "T02. Control SSL Certificate Verification"
order: 44
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).
> For TLS configuration on a WebSocket client (`wss://`), see [W05. Configure TLS for wss:// Connections](../w05-websocket-tls).
@@ -0,0 +1,78 @@
---
title: "T03. Start an SSL/TLS Server"
order: 45
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).
+86
View File
@@ -0,0 +1,86 @@
---
title: "T04. Configure mTLS"
order: 46
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.
The client side has the same `PemMemory` struct too, letting you set the client certificate from PEM in memory.
```cpp
httplib::SSLClient::PemMemory pem{};
pem.cert_pem = client_cert.data();
pem.cert_pem_len = client_cert.size();
pem.key_pem = client_key.data();
pem.key_pem_len = client_key.size();
httplib::SSLClient cli("api.example.com", 443, pem);
auto res = cli.Get("/");
```
> For mTLS with a WebSocket client (`wss://`), see [W05. Configure TLS for wss:// Connections](../w05-websocket-tls).
## 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.
@@ -0,0 +1,88 @@
---
title: "T05. Access the Peer Certificate on the Server Side"
order: 47
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).
@@ -0,0 +1,89 @@
---
title: "W01. Implement a WebSocket Echo Server and Client"
order: 52
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
- `ReadResult::Timeout`: a read timeout you set with `set_read_timeout()` elapsed with nothing received; the connection is still open. The compile-time default timeout closes the connection and is reported as `Fail` instead — see [W06. Set Timeouts](../w06-websocket-timeouts)
## 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. For CA and client certificate configuration, see [W05. Configure TLS for wss:// Connections](../w05-websocket-tls).
@@ -0,0 +1,80 @@
---
title: "W02. Set a WebSocket Heartbeat"
order: 53
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 | 30–60s |
| Strict proxies (e.g. AWS ALB) | 15–30s |
| 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.
On the server side, even with `0`, a dead connection won't linger forever: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no backstop of its own — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side, it is also how you notice one **faster** than that 5-minute fallback.
> For handling a closed connection, see [W03. Handle connection close](../w03-websocket-close).
@@ -0,0 +1,91 @@
---
title: "W03. Handle Connection Close"
order: 54
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.
@@ -0,0 +1,88 @@
---
title: "W04. Send and Receive Binary Frames"
order: 55
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;
case httplib::ws::ReadResult::Timeout:
// read timeout elapsed; the connection is still open
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.
@@ -0,0 +1,49 @@
---
title: "W05. Configure TLS for wss:// Connections"
order: 56
status: "draft"
---
Client-side TLS configuration for `wss://` (WebSocket over TLS) connections uses almost the same API as `SSLClient`. `ws::WebSocketClient` handles both `ws://` and `wss://` through the same class, so there's no separate class to switch to the way `SSLClient` requires.
```cpp
httplib::ws::WebSocketClient ws1("ws://localhost:8080/ws"); // plaintext
httplib::ws::WebSocketClient ws2("wss://localhost:8443/ws"); // TLS
```
## Verifying the server certificate
Use `set_ca_cert_path()` to point at your own CA certificate. The signature matches `SSLClient`: the first argument is the CA certificate file, the second is an optional CA directory.
```cpp
httplib::ws::WebSocketClient ws("wss://internal.example.com/ws");
ws.set_ca_cert_path("/etc/ssl/certs/internal-ca.pem");
if (ws.connect()) {
ws.send("hello");
}
```
To disable certificate verification entirely, use `enable_server_certificate_verification(false)`. For details on that behavior, see [T02. Control SSL Certificate Verification](../t02-cert-verification).
## Presenting a client certificate (mTLS)
`ws::WebSocketClient` has a constructor overload that takes a `PemMemory` struct, letting `wss://` connections present a client certificate.
```cpp
httplib::ws::WebSocketClient::PemMemory pem{};
pem.cert_pem = client_cert.data();
pem.cert_pem_len = client_cert.size();
pem.key_pem = client_key.data();
pem.key_pem_len = client_key.size();
httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem);
if (ws.connect()) {
ws.send("hello");
}
```
Passing `PemMemory` to a `ws://` (non-TLS) URL is silently ignored. There's no constructor that reads the cert files directly, so unlike `SSLClient` you always load the PEM into memory yourself before passing it in.
For the full mTLS picture, including server-side setup and use cases, see [T04. Configure mTLS](../t04-mtls).
@@ -0,0 +1,86 @@
---
title: "W06. Set Timeouts"
order: 57
status: "draft"
---
`ws::WebSocketClient` has the same three kinds of timeouts as `Client`, with the same meaning.
| Kind | API | Default |
| --- | --- | --- |
| Connection | `set_connection_timeout` | 300s |
| Read | `set_read_timeout` | none — waits forever (`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
| Write | `set_write_timeout` | 5s |
## Basic usage
```cpp
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
ws.set_connection_timeout(5, 0); // 5 seconds
ws.set_read_timeout(30, 0); // 30 seconds
ws.set_write_timeout(10, 0); // 10 seconds
if (ws.connect()) {
ws.send("hello");
}
```
Set the connection and write timeouts before calling `connect()`. The read timeout can be changed at any time — setting it on an open connection takes effect on the next `read()`.
## Use `std::chrono`
Just like `Client`, there's an overload that takes a `std::chrono` duration directly.
```cpp
using namespace std::chrono_literals;
ws.set_connection_timeout(5s);
ws.set_read_timeout(30s);
ws.set_write_timeout(10s);
```
## What the read timeout means
`set_read_timeout()` applies to a single `read()` call. If no message arrives within that time, `read()` returns `ReadResult::Timeout`: **the connection is still open** and nothing was consumed, so you can send on it and read again. That is what separates it from `ReadResult::Fail`, which means the connection is gone.
This is what lets one thread own a connection in both directions:
```cpp
using namespace std::chrono_literals;
ws.set_read_timeout(100ms);
std::string msg;
while (ws.is_open()) {
auto r = ws.read(msg);
if (r == httplib::ws::Timeout) {
flush_outgoing(ws); // nothing arrived — send whatever is queued
continue;
}
if (r == httplib::ws::Fail) { break; }
handle(msg);
}
```
Without a read timeout, `read()` blocks until a message arrives, so the thread holding the connection never gets to its writes.
Two things to know about `Timeout`:
- It leaves `msg` untouched, and it is non-zero. So `while (ws.read(msg))` is not usable once a read timeout is set — the loop would keep running with the *previous* message still in `msg`.
- It is only reported on a message boundary. If the timeout elapses partway through a fragmented message, that message cannot be resumed and `read()` returns `Fail`.
For connections where long idle periods are normal — waiting on notifications, for example — either leave the read timeout unset, or treat `Timeout` as the no-op it is and keep looping.
## On the server side
A handler's `ws::WebSocket` has `set_read_timeout()` too, and the pattern above is how a handler relays between connections instead of parking in `read()`.
The server default is 300s (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`) rather than "forever": it is a backstop that reclaims a worker from a peer that has gone silent, since a WebSocket handler holds its worker for the life of the connection.
Because it is a backstop rather than something the handler asked for, it does not surface as `Timeout`. When it elapses, `read()` returns `Fail` and closes the connection, so a handler written as `while (ws.read(msg))` ends the way it always has. Only a timeout the handler set itself with `set_read_timeout()` comes back as `Timeout`.
> Unresponsive-peer detection via Ping/Pong is a separate mechanism. See [W02. Set a WebSocket Heartbeat](../w02-websocket-ping) for details.
## How this differs from `Client`
For `Client`'s timeout configuration, see [C12. Set Timeouts](../c12-timeouts). The behavior and API are nearly identical, but `WebSocketClient` has no equivalent to `set_max_timeout()` for capping the whole request — once connected, the connection stays open for as long as you keep calling `read()`.
+25
View File
@@ -0,0 +1,25 @@
---
title: "cpp-httplib"
order: 0
---
[cpp-httplib](https://github.com/yhirose/cpp-httplib) is an HTTP/HTTPS library for C++. Just copy a single header file, [`httplib.h`](https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h), and you're ready to go.
When you need a quick HTTP server or client in C++, you want something that just works. That's exactly why I built cpp-httplib. You can start writing both servers and clients in just a few lines of code.
The API uses a lambda-based design that feels natural. It runs anywhere you have a C++11 or later compiler. Windows, macOS, Linux — use whatever environment you already have.
HTTPS works too. Just link OpenSSL or mbedTLS, and both server and client gain TLS support. Content-Encoding (gzip, Brotli, etc.), file uploads, and other features you actually need in real-world development are all included. WebSocket is also supported.
Under the hood, it uses blocking I/O with a thread pool. It's not built for handling massive numbers of simultaneous connections. But for API servers, embedded HTTP in tools, mock servers for testing, and many other use cases, it delivers solid performance.
"Solve today's problem, today." That's the kind of simplicity cpp-httplib aims for.
## Documentation
- [A Tour of cpp-httplib](tour/) — A step-by-step tutorial covering the basics. Start here if you're new
- [Building a Desktop LLM App](llm-app/) — A hands-on guide to building a desktop app with llama.cpp, step by step
## Stay Tuned
- [Cookbook](cookbook/) — A collection of recipes organized by topic. Jump to whatever you need
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+236
View File
@@ -0,0 +1,236 @@
---
title: "1. Setting Up the Project Environment"
order: 1
---
Let's incrementally build a text translation REST API server using llama.cpp as the inference engine. By the end, a request like this will return a translation result.
```bash
curl -X POST http://localhost:8080/translate \
-H "Content-Type: application/json" \
-d '{"text": "The weather is nice today. Shall we go for a walk?", "target_lang": "ja"}'
```
```json
{
"translation": "今日はいい天気ですね。散歩に行きましょうか?"
}
```
The "Translation API" is just one example. By swapping out the prompt, you can adapt this to any LLM application you like, such as summarization, code generation, or a chatbot.
Here's the full list of APIs the server will provide.
| Method | Path | Description | Chapter |
| -------- | ---- | ---- | -- |
| `GET` | `/health` | Returns server status | 1 |
| `POST` | `/translate` | Translates text and returns JSON | 2 |
| `POST` | `/translate/stream` | SSE streaming on a per-token basis | 3 |
| `GET` | `/models` | Model list (available / downloaded / selected) | 4 |
| `POST` | `/models/select` | Select a model (automatically downloads if not yet downloaded) | 4 |
In this chapter, let's set up the project environment. We'll fetch the dependency libraries, create the directory structure, configure the build settings, and grab the model file, so that we're ready to start writing code in the next chapter.
## Prerequisites
- A C++20-compatible compiler (GCC 10+, Clang 10+, MSVC 2019 16.8+)
- CMake 3.20 or later
- OpenSSL (used for the HTTPS client in Chapter 4. macOS: `brew install openssl`, Ubuntu: `sudo apt install libssl-dev`)
- Sufficient disk space (model files can be several GB)
## 1.1 What We Will Use
Here are the libraries we'll use.
| Library | Role |
| ----------- | ------ |
| [cpp-httplib](https://github.com/yhirose/cpp-httplib) | HTTP server/client |
| [nlohmann/json](https://github.com/nlohmann/json) | JSON parser |
| [cpp-llamalib](https://github.com/yhirose/cpp-llamalib) | llama.cpp wrapper |
| [llama.cpp](https://github.com/ggml-org/llama.cpp) | LLM inference engine |
| [webview/webview](https://github.com/webview/webview) | Desktop WebView (used in Chapter 6) |
cpp-httplib, nlohmann/json, and cpp-llamalib are header-only libraries. You could just download a single header file with `curl` and `#include` it, but in this book we use CMake's `FetchContent` to fetch them automatically. Declare them in `CMakeLists.txt`, and `cmake -B build` downloads and builds everything for you. webview is used in Chapter 6, so you don't need to worry about it for now.
## 1.2 Directory Structure
The final structure will look like this.
```ascii
translate-app/
├── CMakeLists.txt
├── models/
│ └── (GGUF files)
└── src/
└── main.cpp
```
We don't include library source code in the project. CMake's `FetchContent` fetches them automatically at build time, so all you need is your own code.
Let's create the project directory and initialize a git repository.
```bash
mkdir translate-app && cd translate-app
mkdir src models
git init
```
## 1.3 Obtaining the GGUF Model File
You need a model file for LLM inference. GGUF is the model format used by llama.cpp, and you can find many models on Hugging Face.
Let's start by trying a small model. The quantized version of Google's Gemma 2 2B (~1.6 GB) is a good starting point. It's lightweight but supports multiple languages and works well for translation tasks.
```bash
curl -L -o models/gemma-2-2b-it-Q4_K_M.gguf \
https://huggingface.co/bartowski/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q4_K_M.gguf
```
In Chapter 4, we'll add the ability to download models from within the app using cpp-httplib's client functionality.
## 1.4 CMakeLists.txt
Create a `CMakeLists.txt` in the project root. By declaring dependencies with `FetchContent`, CMake will automatically download and build them for you.
<!-- data-file="CMakeLists.txt" -->
```cmake
cmake_minimum_required(VERSION 3.20)
project(translate-server CXX)
set(CMAKE_CXX_STANDARD 20)
include(FetchContent)
# llama.cpp (LLM inference engine)
FetchContent_Declare(llama
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(llama)
# cpp-httplib (HTTP server/client)
FetchContent_Declare(httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
GIT_TAG master
)
FetchContent_MakeAvailable(httplib)
# nlohmann/json (JSON parser)
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(json)
# cpp-llamalib (header-only llama.cpp wrapper)
FetchContent_Declare(cpp_llamalib
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
GIT_TAG main
)
FetchContent_MakeAvailable(cpp_llamalib)
add_executable(translate-server src/main.cpp)
target_link_libraries(translate-server PRIVATE
httplib::httplib
nlohmann_json::nlohmann_json
cpp-llamalib
)
```
`FetchContent_Declare` tells CMake where to find each library, and `FetchContent_MakeAvailable` fetches and builds them. The first `cmake -B build` will take some time because it downloads all libraries and builds llama.cpp, but subsequent runs will use the cache.
Just link with `target_link_libraries`, and each library's CMake configuration sets up include paths and build settings for you.
## 1.5 Creating the Skeleton Code
We'll use this skeleton code as a base and add functionality chapter by chapter.
<!-- data-file="main.cpp" -->
```cpp
// src/main.cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <csignal>
#include <iostream>
using json = nlohmann::json;
httplib::Server svr;
// Graceful shutdown on `Ctrl+C`
void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
int main() {
// Log requests and responses
svr.set_logger([](const auto &req, const auto &res) {
std::cout << req.method << " " << req.path << " -> " << res.status
<< std::endl;
});
// Health check
svr.Get("/health", [](const auto &, auto &res) {
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
});
// Stub implementations for each endpoint (replaced with real ones in later chapters)
svr.Post("/translate",
[](const auto &req, auto &res) {
res.set_content(json{{"translation", "TODO"}}.dump(), "application/json");
});
svr.Post("/translate/stream",
[](const auto &req, auto &res) {
res.set_content("data: \"TODO\"\n\ndata: [DONE]\n\n", "text/event-stream");
});
svr.Get("/models",
[](const auto &req, auto &res) {
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
});
svr.Post("/models/select",
[](const auto &req, auto &res) {
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
});
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Start the server
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
svr.listen("127.0.0.1", 8080);
}
```
## 1.6 Building and Verifying
Build the project, start the server, and verify that requests work with curl.
```bash
cmake -B build
cmake --build build -j
./build/translate-server
```
From another terminal, try it with curl.
```bash
curl http://localhost:8080/health
# => {"status":"ok"}
```
If you see JSON come back, the setup is complete.
## Next Chapter
Now that the environment is set up, in the next chapter we'll implement the translation REST API on top of this skeleton. We'll run inference with llama.cpp and expose it as an HTTP endpoint with cpp-httplib.
**Next:** [Integrating llama.cpp to Build a REST API](../ch02-rest-api)
+212
View File
@@ -0,0 +1,212 @@
---
title: "2. Integrating llama.cpp to Build a REST API"
order: 2
---
In the skeleton from Chapter 1, `/translate` simply returned `"TODO"`. In this chapter we integrate llama.cpp inference and turn it into an API that actually returns translation results.
Calling the llama.cpp API directly makes the code quite long, so we use a thin wrapper library called [cpp-llamalib](https://github.com/yhirose/cpp-llamalib). It lets you load a model and run inference in just a few lines, keeping the focus on cpp-httplib.
## 2.1 Initializing the LLM
Simply pass the path to a model file to `llamalib::Llama`, and model loading, context creation, and sampler configuration are all taken care of. If you downloaded a different model in Chapter 1, adjust the path accordingly.
```cpp
#include <cpp-llamalib.h>
int main() {
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
// LLM inference takes time, so set a longer timeout (default is 5 seconds)
svr.set_read_timeout(300);
svr.set_write_timeout(300);
// ... Build and start the HTTP server ...
}
```
If you want to change the number of GPU layers, context length, or other settings, you can specify them via `llamalib::Options`.
```cpp
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf", {
.n_gpu_layers = 0, // CPU only
.n_ctx = 4096,
}};
```
## 2.2 The `/translate` Handler
We replace the handler that returned dummy JSON in Chapter 1 with actual inference.
```cpp
svr.Post("/translate",
[&](const httplib::Request &req, httplib::Response &res) {
// Parse JSON (3rd arg `false`: don't throw on failure, check with `is_discarded()`)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
// Validate required fields
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja"); // Default is Japanese
// Build the prompt and run inference
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
try {
auto translation = llm.chat(prompt);
res.set_content(json{{"translation", translation}}.dump(),
"application/json");
} catch (const std::exception &e) {
res.status = 500;
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
}
});
```
`llm.chat()` can throw exceptions during inference (for example, when the context length is exceeded). By catching them with `try/catch` and returning the error as JSON, we prevent the server from crashing.
## 2.3 Complete Code
Here is the finished code with all the changes so far.
<details>
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
```cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <cpp-llamalib.h>
#include <csignal>
#include <iostream>
using json = nlohmann::json;
httplib::Server svr;
// Graceful shutdown on `Ctrl+C`
void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
int main() {
// Load the model downloaded in Chapter 1
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
// LLM inference takes time, so set a longer timeout (default is 5 seconds)
svr.set_read_timeout(300);
svr.set_write_timeout(300);
// Log requests and responses
svr.set_logger([](const auto &req, const auto &res) {
std::cout << req.method << " " << req.path << " -> " << res.status
<< std::endl;
});
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
});
svr.Post("/translate",
[&](const httplib::Request &req, httplib::Response &res) {
// Parse JSON (3rd arg `false`: don't throw on failure, check with `is_discarded()`)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
// Validate required fields
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja"); // Default is Japanese
// Build the prompt and run inference
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
try {
auto translation = llm.chat(prompt);
res.set_content(json{{"translation", translation}}.dump(),
"application/json");
} catch (const std::exception &e) {
res.status = 500;
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
}
});
// Dummy implementations to be replaced with real ones in later chapters
svr.Get("/models",
[](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
});
svr.Post("/models/select",
[](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
});
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Start the server (blocks until `stop()` is called)
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
svr.listen("127.0.0.1", 8080);
}
```
</details>
## 2.4 Testing It Out
Rebuild and start the server, then verify that it now returns actual translation results.
```bash
cmake --build build -j
./build/translate-server
```
```bash
curl -X POST http://localhost:8080/translate \
-H "Content-Type: application/json" \
-d '{"text": "I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.", "target_lang": "ja"}'
# => {"translation":"去年の春に東京を訪れた。桜が綺麗だった。"}
```
In Chapter 1 the response was `"TODO"`, but now you get an actual translation back.
## Next Chapter
The REST API we built in this chapter waits for the entire translation to complete before sending the response, so for long texts the user has to wait with no indication of progress.
In the next chapter, we use SSE (Server-Sent Events) to stream tokens back in real time as they are generated.
**Next:** [Adding Token Streaming with SSE](../ch03-sse-streaming)
@@ -0,0 +1,264 @@
---
title: "3. Adding Token Streaming with SSE"
order: 3
---
The `/translate` endpoint from Chapter 2 returned the entire translation at once after completion. This is fine for short sentences, but for longer text the user has to wait several seconds with nothing displayed.
In this chapter, we add a `/translate/stream` endpoint that uses SSE (Server-Sent Events) to return tokens in real time as they are generated. This is the same approach used by the ChatGPT and Claude APIs.
## 3.1 What is SSE?
SSE is a way to send HTTP responses as a stream. When a client sends a request, the server keeps the connection open and gradually returns events. The format is simple text.
```text
data: "去年の"
data: "春に"
data: "東京を"
data: [DONE]
```
Each line starts with `data:` and events are separated by blank lines. The Content-Type is `text/event-stream`. Tokens are sent as escaped JSON strings, so they appear enclosed in double quotes (we implement this in Section 3.3).
## 3.2 Streaming with cpp-httplib
In cpp-httplib, you can use `set_chunked_content_provider` to send responses incrementally. Each time you write to `sink.os` inside the callback, data is sent to the client.
```cpp
res.set_chunked_content_provider(
"text/event-stream",
[](size_t offset, httplib::DataSink &sink) {
sink.os << "data: hello\n\n";
sink.done();
return true;
});
```
Calling `sink.done()` ends the stream. If the client disconnects mid-stream, writing to `sink.os` will fail and `sink.os.fail()` will return `true`. You can use this to detect disconnection and abort unnecessary inference.
## 3.3 The `/translate/stream` Handler
JSON parsing and validation are the same as the `/translate` endpoint from Chapter 2. The only difference is how the response is returned. We combine the streaming callback of `llm.chat()` with `set_chunked_content_provider`.
```cpp
svr.Post("/translate/stream",
[&](const httplib::Request &req, httplib::Response &res) {
// ... JSON parsing and validation same as /translate ...
res.set_chunked_content_provider(
"text/event-stream",
[&, prompt](size_t, httplib::DataSink &sink) {
try {
llm.chat(prompt, [&](std::string_view token) {
sink.os << "data: "
<< json(std::string(token)).dump(
-1, ' ', false, json::error_handler_t::replace)
<< "\n\n";
return sink.os.good(); // Abort inference on disconnect
});
sink.os << "data: [DONE]\n\n";
} catch (const std::exception &e) {
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
}
sink.done();
return true;
});
});
```
A few key points:
- When you pass a callback to `llm.chat()`, it is called each time a token is generated. If the callback returns `false`, generation is aborted
- After writing to `sink.os`, you can check whether the client is still connected with `sink.os.good()`. If the client has disconnected, it returns `false` to stop inference
- Each token is escaped as a JSON string using `json(token).dump()` before sending. This is safe even for tokens containing newlines or quotes
- The first three arguments of `dump(-1, ' ', false, ...)` are the defaults. What matters is the fourth argument, `json::error_handler_t::replace`. Since the LLM returns tokens at the subword level, multi-byte characters (such as Japanese) can be split mid-character across tokens. Passing an incomplete UTF-8 byte sequence directly to `dump()` would throw an exception, so `replace` safely substitutes them. The browser reassembles the bytes on its end, so everything displays correctly
- The entire lambda is wrapped in `try/catch`. `llm.chat()` can throw exceptions for reasons such as exceeding the context window. If an exception goes uncaught inside the lambda, the server will crash, so we return the error as an SSE event instead
- `data: [DONE]` follows the OpenAI API convention to signal the end of the stream to the client
## 3.4 Complete Code
Here is the complete code with the `/translate/stream` endpoint added to the code from Chapter 2.
<details>
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
```cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <cpp-llamalib.h>
#include <csignal>
#include <iostream>
using json = nlohmann::json;
httplib::Server svr;
// Graceful shutdown on `Ctrl+C`
void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
int main() {
// Load the GGUF model
auto llm = llamalib::Llama{"models/gemma-2-2b-it-Q4_K_M.gguf"};
// LLM inference takes time, so set a longer timeout (default is 5 seconds)
svr.set_read_timeout(300);
svr.set_write_timeout(300);
// Log requests and responses
svr.set_logger([](const auto &req, const auto &res) {
std::cout << req.method << " " << req.path << " -> " << res.status
<< std::endl;
});
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
});
// Standard translation endpoint from Chapter 2
svr.Post("/translate",
[&](const httplib::Request &req, httplib::Response &res) {
// JSON parsing and validation (see Chapter 2 for details)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja");
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
try {
auto translation = llm.chat(prompt);
res.set_content(json{{"translation", translation}}.dump(),
"application/json");
} catch (const std::exception &e) {
res.status = 500;
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
}
});
// SSE streaming translation endpoint
svr.Post("/translate/stream",
[&](const httplib::Request &req, httplib::Response &res) {
// JSON parsing and validation (same as /translate)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja");
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
res.set_chunked_content_provider(
"text/event-stream",
[&, prompt](size_t, httplib::DataSink &sink) {
try {
llm.chat(prompt, [&](std::string_view token) {
sink.os << "data: "
<< json(std::string(token)).dump(
-1, ' ', false, json::error_handler_t::replace)
<< "\n\n";
return sink.os.good(); // Abort inference on disconnect
});
sink.os << "data: [DONE]\n\n";
} catch (const std::exception &e) {
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
}
sink.done();
return true;
});
});
// Dummy implementations to be replaced in later chapters
svr.Get("/models",
[](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"models", json::array()}}.dump(), "application/json");
});
svr.Post("/models/select",
[](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"status", "TODO"}}.dump(), "application/json");
});
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Start the server (blocks until `stop()` is called)
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
svr.listen("127.0.0.1", 8080);
}
```
</details>
## 3.5 Testing It Out
Build and start the server.
```bash
cmake --build build -j
./build/translate-server
```
Using curl's `-N` option to disable buffering, you can see tokens displayed in real time as they arrive.
```bash
curl -N -X POST http://localhost:8080/translate/stream \
-H "Content-Type: application/json" \
-d '{"text": "I had a great time visiting Tokyo last spring. The cherry blossoms were beautiful.", "target_lang": "ja"}'
```
```text
data: "去年の"
data: "春に"
data: "東京を"
data: "訪れた"
data: "。"
data: "桜が"
data: "綺麗だった"
data: "。"
data: [DONE]
```
You should see tokens streaming in one by one. The `/translate` endpoint from Chapter 2 continues to work as well.
## Next Chapter
The server's translation functionality is now complete. In the next chapter, we use cpp-httplib's client functionality to add the ability to fetch and manage models from Hugging Face.
**Next:** [Adding Model Download and Management](../ch04-model-management)
@@ -0,0 +1,788 @@
---
title: "4. Adding Model Download and Management"
order: 4
---
By the end of Chapter 3, the server's translation functionality was fully in place. However, the only model file available is the one we manually downloaded in Chapter 1. In this chapter, we'll use cpp-httplib's **client functionality** to enable downloading and switching Hugging Face models from within the app.
Once complete, you'll be able to manage models with requests like these:
```bash
# Get the list of available models
curl http://localhost:8080/models
```
```json
{
"models": [
{"name": "gemma-2-2b-it", "params": "2B", "size": "1.6 GB", "downloaded": true, "selected": true},
{"name": "gemma-2-9b-it", "params": "9B", "size": "5.8 GB", "downloaded": false, "selected": false},
{"name": "Llama-3.1-8B-Instruct", "params": "8B", "size": "4.9 GB", "downloaded": false, "selected": false}
]
}
```
```bash
# Select a different model (automatically downloads if not yet available)
curl -N -X POST http://localhost:8080/models/select \
-H "Content-Type: application/json" \
-d '{"model": "gemma-2-9b-it"}'
```
```text
data: {"status":"downloading","progress":0}
data: {"status":"downloading","progress":12}
...
data: {"status":"downloading","progress":100}
data: {"status":"loading"}
data: {"status":"ready"}
```
## 4.1 httplib::Client Basics
So far we've only used `httplib::Server`, but cpp-httplib also provides client functionality. Since Hugging Face uses HTTPS, we need a TLS-capable client.
```cpp
#include <httplib.h>
// Including the URL scheme automatically uses SSLClient
httplib::Client cli("https://huggingface.co");
// Automatically follow redirects (Hugging Face redirects to a CDN)
cli.set_follow_location(true);
auto res = cli.Get("/api/models");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
```
To use HTTPS, you need to enable OpenSSL at build time. Add the following to your `CMakeLists.txt`:
```cmake
find_package(OpenSSL REQUIRED)
target_link_libraries(translate-server PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
# macOS: required for loading system certificates
if(APPLE)
target_link_libraries(translate-server PRIVATE "-framework CoreFoundation" "-framework Security")
endif()
```
Defining `CPPHTTPLIB_OPENSSL_SUPPORT` enables `httplib::Client("https://...")` to make TLS connections. On macOS, you also need to link the CoreFoundation and Security frameworks to access the system certificate store. See Section 4.8 for the complete `CMakeLists.txt`.
## 4.2 Defining the Model List
Let's define the list of models that the app can handle. Here are four models we've verified for translation tasks.
```cpp
struct ModelInfo {
std::string name; // Display name
std::string params; // Parameter count
std::string size; // GGUF Q4 size
std::string repo; // Hugging Face repository
std::string filename; // GGUF filename
};
const std::vector<ModelInfo> MODELS = {
{
.name = "gemma-2-2b-it",
.params = "2B",
.size = "1.6 GB",
.repo = "bartowski/gemma-2-2b-it-GGUF",
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
},
{
.name = "gemma-2-9b-it",
.params = "9B",
.size = "5.8 GB",
.repo = "bartowski/gemma-2-9b-it-GGUF",
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
},
{
.name = "Llama-3.1-8B-Instruct",
.params = "8B",
.size = "4.9 GB",
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
},
};
```
## 4.3 Model Storage Location
Up through Chapter 3, we stored models in the `models/` directory within the project. However, when managing multiple models, a dedicated app directory makes more sense. On macOS/Linux we use `~/.translate-app/models/`, and on Windows we use `%APPDATA%\translate-app\models\`.
```cpp
std::filesystem::path get_models_dir() {
#ifdef _WIN32
auto env = std::getenv("APPDATA");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / "translate-app" / "models";
#else
auto env = std::getenv("HOME");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / ".translate-app" / "models";
#endif
}
```
If the environment variable isn't set, it falls back to the current directory. The app creates this directory at startup (`create_directories` won't error even if it already exists).
## 4.4 Rewriting Model Initialization
We rewrite the model initialization at the beginning of `main()`. In Chapter 1 we hardcoded the path, but from here on we support model switching. We track the currently loaded filename in `selected_model` and load the first entry in `MODELS` at startup. The `GET /models` and `POST /models/select` handlers reference and update this variable.
Since cpp-httplib runs handlers concurrently on a thread pool, reassigning `llm` while another thread is calling `llm.chat()` would crash. We add a `std::mutex` to protect against this.
```cpp
int main() {
auto models_dir = get_models_dir();
std::filesystem::create_directories(models_dir);
std::string selected_model = MODELS[0].filename;
auto path = models_dir / selected_model;
// Automatically download the default model if not yet present
if (!std::filesystem::exists(path)) {
std::cout << "Downloading " << selected_model << "..." << std::endl;
if (!download_model(MODELS[0], [](int pct) {
std::cout << "\r" << pct << "%" << std::flush;
return true;
})) {
std::cerr << "\nFailed to download model." << std::endl;
return 1;
}
std::cout << std::endl;
}
auto llm = llamalib::Llama{path};
std::mutex llm_mutex; // Protect access during model switching
// ...
}
```
This ensures that users don't need to manually download models with curl on first launch. It uses the `download_model` function from Section 4.6 and displays progress on the console.
## 4.5 The `GET /models` Handler
This returns the model list with information about whether each model has been downloaded and whether it's currently selected.
```cpp
svr.Get("/models",
[&](const httplib::Request &, httplib::Response &res) {
auto arr = json::array();
for (const auto &m : MODELS) {
auto path = get_models_dir() / m.filename;
arr.push_back({
{"name", m.name},
{"params", m.params},
{"size", m.size},
{"downloaded", std::filesystem::exists(path)},
{"selected", m.filename == selected_model},
});
}
res.set_content(json{{"models", arr}}.dump(), "application/json");
});
```
## 4.6 Downloading Large Files
GGUF models are several gigabytes, so we can't load the entire file into memory. By passing callbacks to `httplib::Client::Get`, we can receive data chunk by chunk.
```cpp
// content_receiver: callback that receives data chunks
// progress: download progress callback
cli.Get(url,
[&](const char *data, size_t len) { // content_receiver
ofs.write(data, len);
return true; // returning false aborts the download
},
[&](size_t current, size_t total) { // progress
int pct = total ? (int)(current * 100 / total) : 0;
std::cout << pct << "%" << std::endl;
return true; // returning false aborts the download
});
```
Let's use this to create a function that downloads models from Hugging Face.
```cpp
#include <filesystem>
#include <fstream>
// Download a model and report progress via progress_cb.
// If progress_cb returns false, the download is aborted.
bool download_model(const ModelInfo &model,
std::function<bool(int)> progress_cb) {
httplib::Client cli("https://huggingface.co");
cli.set_follow_location(true);
cli.set_read_timeout(std::chrono::hours(1));
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
auto path = get_models_dir() / model.filename;
auto tmp_path = std::filesystem::path(path).concat(".tmp");
std::ofstream ofs(tmp_path, std::ios::binary);
if (!ofs) { return false; }
auto res = cli.Get(url,
[&](const char *data, size_t len) {
ofs.write(data, len);
return ofs.good();
},
[&](size_t current, size_t total) {
return progress_cb(total ? (int)(current * 100 / total) : 0);
});
ofs.close();
if (!res || res->status != 200) {
std::filesystem::remove(tmp_path);
return false;
}
// Write to .tmp first, then rename, so that an incomplete file
// is never mistaken for a usable model if the download is interrupted
std::filesystem::rename(tmp_path, path);
return true;
}
```
## 4.7 The `/models/select` Handler
This handles model selection requests. We always respond with SSE, reporting status in sequence: download progress, loading, and ready.
```cpp
svr.Post("/models/select",
[&](const httplib::Request &req, httplib::Response &res) {
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded() || !input.contains("model")) {
res.status = 400;
res.set_content(json{{"error", "'model' is required"}}.dump(),
"application/json");
return;
}
auto name = input["model"].get<std::string>();
// Find the model in the list
auto it = std::find_if(MODELS.begin(), MODELS.end(),
[&](const ModelInfo &m) { return m.name == name; });
if (it == MODELS.end()) {
res.status = 404;
res.set_content(json{{"error", "Unknown model"}}.dump(),
"application/json");
return;
}
const auto &model = *it;
// Always respond with SSE (same format whether already downloaded or not)
res.set_chunked_content_provider(
"text/event-stream",
[&, model](size_t, httplib::DataSink &sink) {
// SSE event sending helper
auto send = [&](const json &event) {
sink.os << "data: " << event.dump() << "\n\n";
};
// Download if not yet present (report progress via SSE)
auto path = get_models_dir() / model.filename;
if (!std::filesystem::exists(path)) {
bool ok = download_model(model, [&](int pct) {
send({{"status", "downloading"}, {"progress", pct}});
return sink.os.good(); // Abort download on client disconnect
});
if (!ok) {
send({{"status", "error"}, {"message", "Download failed"}});
sink.done();
return true;
}
}
// Load and switch to the model
send({{"status", "loading"}});
{
std::lock_guard<std::mutex> lock(llm_mutex);
llm = llamalib::Llama{path};
selected_model = model.filename;
}
send({{"status", "ready"}});
sink.done();
return true;
});
});
```
A few notes:
- We send SSE events directly from the `download_model` progress callback. This is an application of `set_chunked_content_provider` + `sink.os` from Chapter 3
- Since the callback returns `sink.os.good()`, the download stops if the client disconnects. The cancel button we add in Chapter 5 uses this
- When we update `selected_model`, it's reflected in the `selected` flag of `GET /models`
- The `llm` reassignment is protected by `llm_mutex`. The `/translate` and `/translate/stream` handlers also lock the same mutex, so inference can't run during a model switch (see the complete code)
## 4.8 Complete Code
Here is the complete code with model management added to the Chapter 3 code.
<details>
<summary data-file="CMakeLists.txt">Complete code (CMakeLists.txt)</summary>
```cmake
cmake_minimum_required(VERSION 3.20)
project(translate-server CXX)
set(CMAKE_CXX_STANDARD 20)
include(FetchContent)
# llama.cpp
FetchContent_Declare(llama
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(llama)
# cpp-httplib
FetchContent_Declare(httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
GIT_TAG master
)
FetchContent_MakeAvailable(httplib)
# nlohmann/json
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(json)
# cpp-llamalib
FetchContent_Declare(cpp_llamalib
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
GIT_TAG main
)
FetchContent_MakeAvailable(cpp_llamalib)
find_package(OpenSSL REQUIRED)
add_executable(translate-server src/main.cpp)
target_link_libraries(translate-server PRIVATE
httplib::httplib
nlohmann_json::nlohmann_json
cpp-llamalib
OpenSSL::SSL OpenSSL::Crypto
)
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
if(APPLE)
target_link_libraries(translate-server PRIVATE
"-framework CoreFoundation"
"-framework Security"
)
endif()
```
</details>
<details>
<summary data-file="main.cpp">Complete code (main.cpp)</summary>
```cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <cpp-llamalib.h>
#include <algorithm>
#include <csignal>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
using json = nlohmann::json;
// -------------------------------------------------------------------------
// Model definitions
// -------------------------------------------------------------------------
struct ModelInfo {
std::string name;
std::string params;
std::string size;
std::string repo;
std::string filename;
};
const std::vector<ModelInfo> MODELS = {
{
.name = "gemma-2-2b-it",
.params = "2B",
.size = "1.6 GB",
.repo = "bartowski/gemma-2-2b-it-GGUF",
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
},
{
.name = "gemma-2-9b-it",
.params = "9B",
.size = "5.8 GB",
.repo = "bartowski/gemma-2-9b-it-GGUF",
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
},
{
.name = "Llama-3.1-8B-Instruct",
.params = "8B",
.size = "4.9 GB",
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
},
};
// -------------------------------------------------------------------------
// Model storage directory
// -------------------------------------------------------------------------
std::filesystem::path get_models_dir() {
#ifdef _WIN32
auto env = std::getenv("APPDATA");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / "translate-app" / "models";
#else
auto env = std::getenv("HOME");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / ".translate-app" / "models";
#endif
}
// -------------------------------------------------------------------------
// Model download
// -------------------------------------------------------------------------
// If progress_cb returns false, the download is aborted
bool download_model(const ModelInfo &model,
std::function<bool(int)> progress_cb) {
httplib::Client cli("https://huggingface.co");
cli.set_follow_location(true); // Hugging Face redirects to a CDN
cli.set_read_timeout(std::chrono::hours(1)); // Set a long timeout for large models
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
auto path = get_models_dir() / model.filename;
auto tmp_path = std::filesystem::path(path).concat(".tmp");
std::ofstream ofs(tmp_path, std::ios::binary);
if (!ofs) { return false; }
auto res = cli.Get(url,
// content_receiver: receive data chunk by chunk and write to file
[&](const char *data, size_t len) {
ofs.write(data, len);
return ofs.good();
},
// progress: report download progress (returning false aborts)
[&, last_pct = -1](size_t current, size_t total) mutable {
int pct = total ? (int)(current * 100 / total) : 0;
if (pct == last_pct) return true; // Skip if same value
last_pct = pct;
return progress_cb(pct);
});
ofs.close();
if (!res || res->status != 200) {
std::filesystem::remove(tmp_path);
return false;
}
// Rename after download completes
std::filesystem::rename(tmp_path, path);
return true;
}
// -------------------------------------------------------------------------
// Server
// -------------------------------------------------------------------------
httplib::Server svr;
void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
int main() {
// Create the model storage directory
auto models_dir = get_models_dir();
std::filesystem::create_directories(models_dir);
// Automatically download the default model if not yet present
std::string selected_model = MODELS[0].filename;
auto path = models_dir / selected_model;
if (!std::filesystem::exists(path)) {
std::cout << "Downloading " << selected_model << "..." << std::endl;
if (!download_model(MODELS[0], [](int pct) {
std::cout << "\r" << pct << "%" << std::flush;
return true;
})) {
std::cerr << "\nFailed to download model." << std::endl;
return 1;
}
std::cout << std::endl;
}
auto llm = llamalib::Llama{path};
std::mutex llm_mutex; // Protect access during model switching
// Set a long timeout since LLM inference takes time (default is 5 seconds)
svr.set_read_timeout(300);
svr.set_write_timeout(300);
svr.set_logger([](const auto &req, const auto &res) {
std::cout << req.method << " " << req.path << " -> " << res.status
<< std::endl;
});
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
});
// --- Translation endpoint (Chapter 2) ------------------------------------
svr.Post("/translate",
[&](const httplib::Request &req, httplib::Response &res) {
// JSON parsing and validation (see Chapter 2 for details)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja");
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
try {
std::lock_guard<std::mutex> lock(llm_mutex);
auto translation = llm.chat(prompt);
res.set_content(json{{"translation", translation}}.dump(),
"application/json");
} catch (const std::exception &e) {
res.status = 500;
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
}
});
// --- SSE streaming translation (Chapter 3) -------------------------------
svr.Post("/translate/stream",
[&](const httplib::Request &req, httplib::Response &res) {
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja");
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
res.set_chunked_content_provider(
"text/event-stream",
[&, prompt](size_t, httplib::DataSink &sink) {
std::lock_guard<std::mutex> lock(llm_mutex);
try {
llm.chat(prompt, [&](std::string_view token) {
sink.os << "data: "
<< json(std::string(token)).dump(
-1, ' ', false, json::error_handler_t::replace)
<< "\n\n";
return sink.os.good(); // Abort inference on disconnect
});
sink.os << "data: [DONE]\n\n";
} catch (const std::exception &e) {
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
}
sink.done();
return true;
});
});
// --- Model list (Chapter 4) ----------------------------------------------
svr.Get("/models",
[&](const httplib::Request &, httplib::Response &res) {
auto models_dir = get_models_dir();
auto arr = json::array();
for (const auto &m : MODELS) {
auto path = models_dir / m.filename;
arr.push_back({
{"name", m.name},
{"params", m.params},
{"size", m.size},
{"downloaded", std::filesystem::exists(path)},
{"selected", m.filename == selected_model},
});
}
res.set_content(json{{"models", arr}}.dump(), "application/json");
});
// --- Model selection (Chapter 4) -----------------------------------------
svr.Post("/models/select",
[&](const httplib::Request &req, httplib::Response &res) {
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded() || !input.contains("model")) {
res.status = 400;
res.set_content(json{{"error", "'model' is required"}}.dump(),
"application/json");
return;
}
auto name = input["model"].get<std::string>();
auto it = std::find_if(MODELS.begin(), MODELS.end(),
[&](const ModelInfo &m) { return m.name == name; });
if (it == MODELS.end()) {
res.status = 404;
res.set_content(json{{"error", "Unknown model"}}.dump(),
"application/json");
return;
}
const auto &model = *it;
// Always respond with SSE (same format whether already downloaded or not)
res.set_chunked_content_provider(
"text/event-stream",
[&, model](size_t, httplib::DataSink &sink) {
// SSE event sending helper
auto send = [&](const json &event) {
sink.os << "data: " << event.dump() << "\n\n";
};
// Download if not yet present (report progress via SSE)
auto path = get_models_dir() / model.filename;
if (!std::filesystem::exists(path)) {
bool ok = download_model(model, [&](int pct) {
send({{"status", "downloading"}, {"progress", pct}});
return sink.os.good(); // Abort download on client disconnect
});
if (!ok) {
send({{"status", "error"}, {"message", "Download failed"}});
sink.done();
return true;
}
}
// Load and switch to the model
send({{"status", "loading"}});
{
std::lock_guard<std::mutex> lock(llm_mutex);
llm = llamalib::Llama{path};
selected_model = model.filename;
}
send({{"status", "ready"}});
sink.done();
return true;
});
});
// Allow the server to be stopped with `Ctrl+C` (`SIGINT`) or `kill` (`SIGTERM`)
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
svr.listen("127.0.0.1", 8080);
}
```
</details>
## 4.9 Testing
Since we added OpenSSL configuration to CMakeLists.txt, we need to re-run CMake before building.
```bash
cmake -B build
cmake --build build -j
./build/translate-server
```
### Checking the Model List
```bash
curl http://localhost:8080/models
```
The gemma-2-2b-it model downloaded in Chapter 1 should show `downloaded: true` and `selected: true`.
### Switching to a Different Model
```bash
curl -N -X POST http://localhost:8080/models/select \
-H "Content-Type: application/json" \
-d '{"model": "gemma-2-9b-it"}'
```
Download progress streams via SSE, and `"ready"` appears when it's done.
### Comparing Translations Across Models
Let's translate the same sentence with different models.
```bash
# Translate with gemma-2-9b-it (the model we just switched to)
curl -X POST http://localhost:8080/translate \
-H "Content-Type: application/json" \
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
# Switch back to gemma-2-2b-it
curl -N -X POST http://localhost:8080/models/select \
-H "Content-Type: application/json" \
-d '{"model": "gemma-2-2b-it"}'
# Translate the same sentence
curl -X POST http://localhost:8080/translate \
-H "Content-Type: application/json" \
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
```
Translation results vary depending on the model, even with the same code and the same prompt. Since cpp-llamalib automatically applies the appropriate chat template for each model, no code changes are needed.
## Next Chapter
The server's main features are now complete: REST API, SSE streaming, and model download and switching. In the next chapter, we'll add static file serving and build a Web UI you can use from a browser.
**Next:** [Adding a Web UI](../ch05-web-ui)
File diff suppressed because it is too large Load Diff

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