| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | < 22 hours ago | 10.7 kB | |
| v0.52.0 source code.tar.gz | < 22 hours ago | 1.9 MB | |
| v0.52.0 source code.zip | < 22 hours ago | 2.1 MB | |
| Totals: 3 Items | 4.1 MB | 0 | |
What's Changed
Breaking changes
Headers,Params,FormFieldsandFormFilesnow preserve insertion order (#2520, [#2523], [#2524]). All four were hash- or key-sorted multimaps, so the order the fields, query parameters and form parts arrived in was lost. They are now aliases of a singledetail::insertion_ordered_multimap<Mapped, KeyEqual>— a flat vector with a linear-scan lookup, which beats hashing for the at mostCPPHTTPLIB_HEADER_MAX_COUNTentries a message carries;Headerscompares field names case-insensitively, the other three case-sensitively. Source-level differences to be aware of:- iterators follow
std::vectorrules, so insertion invalidates them (std::unordered_multimap/std::multimaponly invalidated on erase) value_typeisstd::pair<std::string, Mapped>rather thanstd::pair<const std::string, Mapped>insert(hint, value)is gone- incrementing past the last entry of a key saturates at
end(), so an out-of-rangeidpassed toget_header_value()returns the default instead of running off the container Headersis now an alias rather than a class, so symbols mentioning these types mangle differently — this is an ABI breakError::UnsupportedContentEncodingwas added to theErrorenum (#2518), which shifts the values after it
Ordering fixes enabled by the above
- Return header fields sharing a field name in the order they were received (#2520, fix [#2509]). RFC 9110 5.3 makes that order significant, but
std::unordered_multimapgives no guarantee for equivalent keys: libstdc++ hands duplicates back in reverse insertion order while libc++ uses insertion order, soget_header_value()returned a different field depending on the platform.Hostis now prepended so it keeps leading a request - Determine the final transfer coding across multiple
Transfer-Encodinglines (#2522). RFC 9110 5.3 combines the lines, in order, into one coding list, and RFC 9112 6.1 frames the message as chunked only whenchunkedis that list's final coding. With the order unrecoverable,is_chunked_transfer_encoding()had to fall back to reporting any message namingchunkedon any line as chunked; it now reads the last token of the last line.Transfer-Encoding: chunkedfollowed byTransfer-Encoding: gzipis answered with 400 and closed instead of being read as chunked, whilegzipfollowed bychunkedis still accepted - Preserve the order of query parameters (#2523).
Paramswas astd::multimap, so parsing a query string discarded the order it arrived in and building one back out ofParamshanded the caller an alphabetised query.ClientImpl::send()takes that path whenever a request carriesParamswithout a query already in its path, so a caller signing its query string could not reproduce the order it asked for - Preserve the order of multipart form parts (#2524). RFC 7578 5.2 says a form processor "SHOULD send back results in order" and that "Intermediaries MUST NOT reorder the results", but a handler walking
req.form.fieldssaw the parts alphabetised across field names
New features
- Accept hostnames — not just IP literals — as
set_hostname_addr_map()values (#2515). A non-IP value was passed as theipargument and rejected bygetaddrinfo'sAI_NUMERICHOSTpath; IP literals keep that path while hostnames are now passed as the connect host and resolved.host_is untouched, so it still supplies theHostheader and SNI either way. This also fixes the documented Unix domain socket client example, whose mapped value is a socket path that never reached theAF_UNIXbranch.set_hostname_addr_mapis now documented in the README, which had no entry for it - Build the WebSocket handshake through the
Request/write_request_line/check_and_write_headerspipeline used byClientImpl::open_stream(#2514, thanks @Hyukya). Headers set on the client are now honored — including aHostoverride — while the protocol-mandatoryUpgrade,ConnectionandSec-WebSocket-Key/Versionfields are always overwritten
Performance
- Cut a syscall and the byte-at-a-time line reader out of the request path (#2513).
keep_alive()already polls the socket before invoking the callback, and the stream's first read polled the same socket again beforerecv; the caller now hands the stream what it knows, and only that first read skips the poll. Separately,stream_line_reader::getline()pulled the request line and every header throughstrm_.read(&byte, 1)— 300 virtual calls for a 300-byte header block, none of them syscalls — so a stream can now offer its already-buffered bytes to be scanned for the terminator in one pass. Streams that do no buffering of their own report none and keep the byte loop, soStreamsubclasses outside the library are unaffected. Measured withwrk -t2 -c8, server CPU per request drops from ~32µs to ~23µs and throughput rises 10–15%; the TLS path goes 44.9µs → 40.2µs - Increase the default listen backlog from 5 to 128. Five pending connections overflow easily under connection churn or a burst of simultaneous connects, and on overflow the kernel silently drops the ACK rather than failing fast, so clients stall on SYN/ACK retransmission backoff.
bombardier -c 10 -d 10sshows max latency dropping from 48–89ms to 5.8–11.2ms with p99 unchanged — the fix affects only the extreme tail
Bug fixes
- Apply path encoding in
open_stream(). It passed the caller-supplied path straight to the request line, soset_path_encode()was ignored and"/a b"went on the wire asGET /a b HTTP/1.1, which an RFC 9112 conformant server reads as target/aand versionb. The splitting and encoding moved intodetail::encode_request_target(), shared withClientImpl::write_request. Note the behavior change: with path encoding enabled, CR/LF in the target is now percent-encoded and sent rather than rejected withError::Write, matchingGet(); the CR/LF guard inwrite_request_line()is independent ofpath_encode_and still backstopsset_path_encode(false) - Buffer the WebSocket handshake before writing it. Follow-up to [#2514]: the rebuilt handshake wrote the request line straight to the socket, so a header rejected by
check_and_write_headersleft a truncatedGET /ws HTTP/1.1in the peer's buffer, and every header cost its own small write. It is now built into aBufferStreamand flushed in one go, matchingClientImpl::write_request - Close the listening socket in
Server::stop()even when not serving (#2517).stop()releasedsvr_sock_only underif (is_running_), whichlisten_internal()sets, so a server that bound withbind_to_port()/bind_to_any_port()and never reachedlisten_after_bind()kept its listening descriptor — and the port — for the life of the process. Dropping the gate also removes a TOCTOU against a concurrent accept loop.listen_after_bind()now fails whenstop()already closed the socket, instead of returning success without ever serving and leaving await_until_ready()caller spinning - Pass unrecognized
Content-Encodingvalues through instead of failing (#2518). Since 8bba34e every non-empty codingcreate_decompressor()could not handle was rejected, conflating an unrecognized coding with a known one whose support was not compiled in — soContent-Encoding: UTF-8, which some servers misuse to advertise a charset, failed asError::Read. Only a recognized-but-not-built-in coding is rejected now, codings are matched case-insensitively per RFC 9110 8.4.1 (GZIPused to look unrecognized and hand back a compressed body), andopen_stream()— which silently passed compressed payloads through and never checkedis_valid(), undefined behavior in release builds — applies the same policy.Error::UnsupportedContentEncodingdistinguishes this from a read failure; an unusable decompressor reportsError::Compression - Apply
Rangeonly to a 206 response inwrite_content_with_provider()(#2510, thanks @metsw24-max).apply_ranges()decides the Content-Length and the multipart boundary only for a 206, anddetail::range_error()validatesreq.rangesagainst the content length only for a 2xx, so honoring the ranges under any other status wrote a body that disagreed with the headers already sent, from an unchecked offset. Four paths were affected: a single range under a non-2xx, a suffix range whosefirst_posis still-1when the bounds asserts are compiled out underNDEBUG, two ranges under a status with no boundary, and a non-206 2xx that announced the full content length - Fail
detail::mmap::open()when::mmapreturnsMAP_FAILED(#2511, thanks @metsw24-max).is_open()only comparesaddr_againstnullptr, so the sentinel passed anddata()handed the caller(const char *)-1 - Sanitize uploaded filenames in the upload example to prevent path traversal (#2496, thanks @superm1). It wrote each file using the multipart
Content-Dispositionfilename verbatim, so a client could supply an absolute path or../components and create or overwrite files outside the working directory. Each filename is now reduced to its base name, and the request is rejected with 400 if the result is empty,.,.., or still contains a path separator (including a colon, for Windows drive letters)
Development
- Add a manual A/B throughput benchmark workflow for comparing two refs. Absolute req/s is not usable for this — the same binary run five times on an idle 8-core machine gave 53.9k to 74.6k req/s — so both refs are built and measured alternately in one session, the order flipped each round to cancel ordering bias, and only the ratio of the medians is reported, with significance decided by an exact permutation test. Validated against a patch removing a redundant
poll(): individual measurements ranged 41.7k–93.9k req/s, yet nine rounds resolved a 1.244× speedup at p = 0.019 - Add a manual workflow that runs the committed
benchmark/Makefileand keeps its output in the job summary, with Crow v1.3.1 alongside for reference. Linux and macOS only; Windows needs the Makefile rewritten first, since it relies onnc,&andkill - Run CIFuzz only for pull requests that touch
httplib.hortest/fuzzing. Fuzzing was by far the longest job — 12m20s against 5m7s for everything else — and a pull request touching neither has nothing for it to exercise. Filtering by path rather than shorteningfuzz-secondskeeps OSS-Fuzz's recommended 600-second budget intact for the pull requests that do reach the parsers
Full Changelog: https://github.com/yhirose/cpp-httplib/compare/v0.51.0...v0.52.0