Skip to content

vendor : update cpp-httplib to 0.52.0 - #26485

Open
cabelo wants to merge 1 commit into
ggml-org:masterfrom
cabelo:cpp-httplib-0.52.0
Open

vendor : update cpp-httplib to 0.52.0#26485
cabelo wants to merge 1 commit into
ggml-org:masterfrom
cabelo:cpp-httplib-0.52.0

Conversation

@cabelo

@cabelo cabelo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Overview

Additional information

Requirements

What's Changed

v0.52.0

Breaking changes

  • Headers, Params, FormFields and FormFiles now preserve insertion order. 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 single detail::insertion_ordered_multimap<Mapped, KeyEqual> — a flat vector with a linear-scan lookup, which beats hashing for the at most CPPHTTPLIB_HEADER_MAX_COUNT entries a message carries; Headers compares field names case-insensitively, the other three case-sensitively. Source-level differences to be aware of:
    • iterators follow std::vector rules, so insertion invalidates them (std::unordered_multimap/std::multimap only invalidated on erase)
    • value_type is std::pair<std::string, Mapped> rather than std::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-range id passed to get_header_value() returns the default instead of running off the container
    • Headers is now an alias rather than a class, so symbols mentioning these types mangle differently — this is an ABI break
  • Error::UnsupportedContentEncoding was added to the Error enum, 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. RFC 9110 5.3 makes that order significant, but std::unordered_multimap gives no 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. Host is now prepended so it keeps leading a request
  • Determine the final transfer coding across multiple Transfer-Encoding lines. RFC 9110 5.3 combines the lines, in order, into one coding list, and RFC 9112 6.1 frames the message as chunked only when chunked is that list's final coding. With the order unrecoverable, is_chunked_transfer_encoding() had to fall back to reporting any message naming chunked on any line as chunked; it now reads the last token of the last line. Transfer-Encoding: chunked followed by Transfer-Encoding: gzip is answered with 400 and closed instead of being read as chunked, while gzip followed by chunked is still accepted
  • Preserve the order of query parameters. Params was a std::multimap, so parsing a query string discarded the order it arrived in and building one back out of Params handed the caller an alphabetised query. 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
  • Preserve the order of multipart form parts. 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.fields saw the parts alphabetised across field names

New features

  • Accept hostnames — not just IP literals — as set_hostname_addr_map() values. A non-IP value was passed as the ip argument and rejected by getaddrinfo's AI_NUMERICHOST path; IP literals keep that path while hostnames are now passed as the connect host and resolved. host_ is untouched, so it still supplies the Host header and SNI either way. This also fixes the documented Unix domain socket client example, whose mapped value is a socket path that never reached the AF_UNIX branch. set_hostname_addr_map is now documented in the README, which had no entry for it
  • Build the WebSocket handshake through the Request / write_request_line / check_and_write_headers pipeline used by ClientImpl::open_stream. Headers set on the client are now honored — including a Host override — while the protocol-mandatory Upgrade, Connection and Sec-WebSocket-Key/Version fields are always overwritten

Performance

  • Cut a syscall and the byte-at-a-time line reader out of the request path. keep_alive() already polls the socket before invoking the callback, and the stream's first read polled the same socket again before recv; 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 through strm_.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, so Stream subclasses outside the library are unaffected. Measured with wrk -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 10s shows 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, so set_path_encode() was ignored and "/a b" went on the wire as GET /a b HTTP/1.1, which an RFC 9112 conformant server reads as target /a and version b. The splitting and encoding moved into detail::encode_request_target(), shared with ClientImpl::write_request. Note the 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(); the CR/LF guard in write_request_line() is independent of path_encode_ and still backstops set_path_encode(false)
  • Buffer the WebSocket handshake before writing it. Follow-up to : 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 in the peer's buffer, and every header cost its own small write. It is now built into a BufferStream and flushed in one go, matching ClientImpl::write_request
  • Close the listening socket in Server::stop() even when not serving. stop() released svr_sock_ only under if (is_running_), which listen_internal() sets, so a server that bound with bind_to_port() / bind_to_any_port() and never reached listen_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 when stop() already closed the socket, instead of returning success without ever serving and leaving a wait_until_ready() caller spinning
  • Pass unrecognized Content-Encoding values through instead of failing. Since every non-empty coding create_decompressor() could not handle was rejected, conflating an unrecognized coding with a known one whose support was not compiled in — so Content-Encoding: UTF-8, which some servers misuse to advertise a charset, failed as Error::Read. Only a recognized-but-not-built-in coding is rejected now, codings are matched case-insensitively per RFC 9110 8.4.1 (GZIP used to look unrecognized and hand back a compressed body), and open_stream() — which silently passed compressed payloads through and never checked is_valid(), undefined behavior in release builds — applies the same policy. Error::UnsupportedContentEncoding distinguishes this from a read failure; an unusable decompressor reports Error::Compression
  • Apply Range only to a 206 response in write_content_with_provider() . apply_ranges() decides the Content-Length and the multipart boundary only for a 206, and detail::range_error() validates req.ranges against 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 whose first_pos is still -1 when the bounds asserts are compiled out under NDEBUG, two ranges under a status with no boundary, and a non-206 2xx that announced the full content length
  • Fail detail::mmap::open() when ::mmap returns MAP_FAILED. is_open() only compares addr_ against nullptr, so the sentinel passed and data() handed the caller (const char *)-1
  • Sanitize uploaded filenames in the upload example to prevent path traversal. It wrote each file using the multipart Content-Disposition filename 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/Makefile and 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 on nc, & and kill
  • Run CIFuzz only for pull requests that touch httplib.h or test/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 shortening fuzz-seconds keeps OSS-Fuzz's recommended 600-second budget intact for the pull requests that do reach the parsers

@github-actions github-actions Bot added the vendor label Aug 3, 2026
@cabelo
cabelo marked this pull request as ready for review August 3, 2026 02:07
@cabelo
cabelo requested a review from ggerganov as a code owner August 3, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants