Commit 3eb101360 for llama.cpp
commit 3eb101360f2666b94910b7c8e3fddbd3fd432487
Author: Adrien Gallouët <angt@huggingface.co>
Date: Wed Oct 14 08:28:24 2026 +0200
vendor : update cpp-httplib to 0.61.0 (#30494)
Signed-off-by: Adrien Gallouët <angt@huggingface.co>
diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py
index 548f25e1d..2e250371b 100755
--- a/scripts/sync_vendor.py
+++ b/scripts/sync_vendor.py
@@ -5,7 +5,7 @@ import os
import sys
import subprocess
-HTTPLIB_VERSION = "refs/tags/v0.60.1"
+HTTPLIB_VERSION = "refs/tags/v0.61.0"
# used by examples/gguf-hash, these repos have no release tag, so we pin a commit
XXHASH_COMMIT = "9f465f1ea932d6ad9a26cd77496311ffa544cd68"
diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp
index 2898454d9..c59cfbf75 100644
--- a/vendor/cpp-httplib/httplib.cpp
+++ b/vendor/cpp-httplib/httplib.cpp
@@ -198,9 +198,12 @@ bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
msg.data += value;
has_data = true;
} else if (field == "id") {
- // Empty id is valid (clears the last event ID)
- msg.id = value;
- has_id = true;
+ // An id that cannot be sent back as Last-Event-ID is ignored. Empty id is
+ // valid (clears the last event ID)
+ if (detail::fields::is_field_value(value)) {
+ msg.id = value;
+ has_id = true;
+ }
} else if (field == "retry") {
// Parse retry interval in milliseconds
// Per the SSE spec, a value that is not all ASCII digits is ignored.
@@ -273,22 +276,36 @@ void SSEClient::run_event_loop() {
SSEMessage current_msg;
auto has_data = false;
auto has_id = false;
+ auto first_line = true;
+ auto after_cr = false;
while (running_.load() && result.next()) {
+ // What is left in the buffer has no line ending
+ auto search_pos = buffer.size();
buffer.append(result.data(), result.size());
- // Process complete lines in the buffer
+ // Process complete lines in the buffer. A line ends with \r\n, \n or \r
+ // per the SSE spec
size_t line_start = 0;
- size_t newline_pos;
+ size_t eol_pos;
- while ((newline_pos = buffer.find('\n', line_start)) !=
+ while ((eol_pos = buffer.find_first_of("\r\n", search_pos)) !=
std::string::npos) {
- auto line = buffer.substr(line_start, newline_pos - line_start);
- line_start = newline_pos + 1;
-
- // Strip the \r of a CRLF line ending so that every field, including
- // one without a colon, sees the same line
- if (!line.empty() && line.back() == '\r') { line.pop_back(); }
+ auto line = buffer.substr(line_start, eol_pos - line_start);
+ line_start = eol_pos + 1;
+ search_pos = line_start;
+
+ // The \n of a \r\n may arrive in a later chunk than its \r
+ auto is_lf_of_crlf =
+ after_cr && line.empty() && buffer[eol_pos] == '\n';
+ after_cr = buffer[eol_pos] == '\r';
+ if (is_lf_of_crlf) { continue; }
+
+ // A UTF-8 BOM at the start of the stream is ignored per the SSE spec
+ if (first_line) {
+ first_line = false;
+ if (line.compare(0, 3, "\xEF\xBB\xBF") == 0) { line.erase(0, 3); }
+ }
// Parse the line and check if event is complete
auto event_complete = parse_sse_line(
@@ -3408,8 +3425,7 @@ const char *get_header_value(const Headers &headers,
}
auto rng = headers.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
+ auto it = bounded_next(rng.first, id, rng.second);
if (it != rng.second) { return it->second.c_str(); }
return def;
}
@@ -3462,8 +3478,7 @@ template <typename Map>
typename Map::mapped_type
get_multimap_value(const Map &m, const std::string &key, size_t id) {
auto rng = m.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
+ auto it = bounded_next(rng.first, id, rng.second);
if (it != rng.second) { return it->second; }
return typename Map::mapped_type();
}
@@ -3692,8 +3707,9 @@ read_content_without_length(Stream &strm, size_t payload_max_length,
if (n < 0) { return ReadContentResult::Error; }
// Check if adding this data would exceed the payload limit
- if (r > payload_max_length ||
- payload_max_length - r < static_cast<size_t>(n)) {
+ if (payload_max_length > 0 &&
+ (r > payload_max_length ||
+ payload_max_length - r < static_cast<size_t>(n))) {
return ReadContentResult::PayloadTooLarge;
}
@@ -3728,8 +3744,9 @@ ReadContentResult read_content_chunked(Stream &strm, T &x,
return ReadContentResult::Success;
}
- if (total_len > payload_max_length ||
- payload_max_length - total_len < static_cast<size_t>(n)) {
+ if (payload_max_length > 0 &&
+ (total_len > payload_max_length ||
+ payload_max_length - total_len < static_cast<size_t>(n))) {
return ReadContentResult::PayloadTooLarge;
}
@@ -4449,7 +4466,7 @@ bool parse_range_header(const std::string &s, Ranges &ranges) try {
return std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
};
- if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
+ if (s.size() > 7 && case_ignore::equal(s.substr(0, 6), "bytes=")) {
const auto pos = static_cast<size_t>(6);
const auto len = static_cast<size_t>(s.size() - 6);
auto all_valid_ranges = true;
@@ -4507,8 +4524,30 @@ bool parse_range_header(const std::string &s, Ranges &ranges) try {
} catch (...) { return false; }
#endif
+// Removes each quoted-string, honoring a quoted-pair (RFC 9110 Section 5.6.4)
+std::string remove_quoted_strings(const std::string &s) {
+ std::string out;
+ auto in_quotes = false;
+ for (size_t i = 0; i < s.size(); i++) {
+ if (s[i] == '"') {
+ in_quotes = !in_quotes;
+ } else if (!in_quotes) {
+ out += s[i];
+ } else if (s[i] == '\\') {
+ i++;
+ }
+ }
+ return out;
+}
+
bool parse_accept_header(const std::string &s,
std::vector<std::string> &content_types) {
+ // Parameter values are not used here, so a quoted-string is dropped rather
+ // than parsed: a ',' or ';' inside one must not be read as a delimiter.
+ if (s.find('"') != std::string::npos) {
+ return parse_accept_header(remove_quoted_strings(s), content_types);
+ }
+
content_types.clear();
// Empty string is considered valid (no preference)
@@ -5583,6 +5622,17 @@ bool is_request_target(const std::string &s) {
return std::all_of(s.begin(), s.end(), is_field_vchar);
}
+// RFC 9110 §7.2: the bytes of uri-host [ ":" port ], i.e. unreserved,
+// sub-delims, "%", ":" and the IP-literal brackets. An empty value is valid.
+bool is_host(const std::string &s) {
+ return std::all_of(s.begin(), s.end(), [](char c) {
+ return is_ascii_alnum(c) || c == '-' || c == '.' || c == '_' || c == '~' ||
+ c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' ||
+ c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||
+ c == '=' || c == '%' || c == ':' || c == '[' || c == ']';
+ });
+}
+
} // namespace fields
bool perform_websocket_handshake(Stream &strm, Request &req,
@@ -6039,12 +6089,14 @@ bool match_hostname(const std::string &pattern,
// Compare each component with wildcard support
// Supports: "*" (full wildcard), "prefix*" (partial wildcard)
// https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484
+ // Only the leftmost label may carry a wildcard (RFC 6125 6.4.3)
auto itr = pattern_components.begin();
for (const auto &h : host_components) {
auto &p = *itr;
- if (!detail::case_ignore::equal(p, h) && p != "*") {
+ auto is_leftmost = itr == pattern_components.begin();
+ if (!detail::case_ignore::equal(p, h) && !(is_leftmost && p == "*")) {
bool partial_match = false;
- if (!p.empty() && p[p.size() - 1] == '*') {
+ if (is_leftmost && !p.empty() && p[p.size() - 1] == '*') {
const auto prefix_length = p.size() - 1;
if (prefix_length == 0) {
partial_match = true;
@@ -6953,8 +7005,7 @@ bool Request::is_multipart_form_data() const {
std::string MultipartFormData::get_field(const std::string &key,
size_t id) const {
auto rng = fields.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
+ auto it = detail::bounded_next(rng.first, id, rng.second);
if (it != rng.second) { return it->second.content; }
return std::string();
}
@@ -9839,6 +9890,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
return write_response(strm, close_connection, req, res);
}
+ // RFC 9112 §3.2: Reject more than one Host field line or an invalid Host
+ // value, so that every recipient of the request sees the same authority.
+ if (req.get_header_value_count("Host") > 1 ||
+ !detail::fields::is_host(req.get_header_value("Host"))) {
+ connection_closed = true;
+ res.status = StatusCode::BadRequest_400;
+ output_error_log(Error::InvalidHeaders, &req);
+ return write_response(strm, close_connection, req, res);
+ }
+
// Check if the request URI doesn't exceed the limit
if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
connection_closed = true;
@@ -11156,7 +11217,8 @@ void ClientImpl::setup_redirect_client(ClientType &client) {
if (!proxy_host_.empty() && proxy_port_ != -1) {
client.set_proxy(proxy_host_, proxy_port_);
- if (!proxy_basic_auth_username_.empty()) {
+ if (!proxy_basic_auth_username_.empty() ||
+ !proxy_basic_auth_password_.empty()) {
client.set_proxy_basic_auth(proxy_basic_auth_username_,
proxy_basic_auth_password_);
}
@@ -11259,8 +11321,8 @@ bool ClientImpl::write_request(Stream &strm, Request &req,
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
// would leak proxy credentials to the destination server.
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
- if (!proxy_basic_auth_username_.empty() &&
- !proxy_basic_auth_password_.empty() &&
+ if ((!proxy_basic_auth_username_.empty() ||
+ !proxy_basic_auth_password_.empty()) &&
!req.has_header("Proxy-Authorization")) {
req.headers.insert(make_basic_authentication_header(
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
@@ -18119,6 +18181,14 @@ void WebSocket::start_heartbeat() {
if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) {
lock.unlock();
close(CloseStatus::GoingAway, "pong timeout");
+ // Wake a read() blocked on the unresponsive peer. Only the read side
+ // is shut down: a TLS backend answers the EOF with an alert, and
+ // writing it to a socket closed for writing raises SIGPIPE.
+#ifdef _WIN32
+ shutdown(strm_.socket(), SD_RECEIVE);
+#else
+ shutdown(strm_.socket(), SHUT_RD);
+#endif
return;
}
lock.unlock();
diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h
index 95432d01e..b786a7c39 100644
--- a/vendor/cpp-httplib/httplib.h
+++ b/vendor/cpp-httplib/httplib.h
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
-#define CPPHTTPLIB_VERSION "0.60.1"
-#define CPPHTTPLIB_VERSION_NUM "0x003c01"
+#define CPPHTTPLIB_VERSION "0.61.0"
+#define CPPHTTPLIB_VERSION_NUM "0x003d00"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -1141,8 +1141,7 @@ private:
pointer operator->() const { return data_ + idx_; }
iterator_t &operator++() {
- // Saturating, so that advancing past the last entry of a key (which
- // get_multimap_value() does when asked for an out-of-range id) stays at
+ // Saturating, so that advancing past the last entry of a key stays at
// end() instead of running off the container.
if (idx_ >= size_) { return *this; }
++idx_;
@@ -3477,13 +3476,20 @@ inline bool is_numeric(const std::string &str) {
return !str.empty() && std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
}
+// std::ranges::next(first, n, last) for C++11
+template <typename It> inline It bounded_next(It first, size_t n, It last) {
+ for (; n > 0 && first != last; --n) {
+ ++first;
+ }
+ return first;
+}
+
inline size_t get_header_value_u64(const Headers &headers,
const std::string &key, size_t def,
size_t id, bool &is_invalid_value) {
is_invalid_value = false;
auto rng = headers.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
+ auto it = bounded_next(rng.first, id, rng.second);
if (it != rng.second) {
if (is_numeric(it->second)) {
// Parse at size_t width so an out-of-range Content-Length is reported
@@ -3968,6 +3974,7 @@ bool is_field_content(const std::string &s);
bool is_field_value(const std::string &s);
bool is_field_valid(const std::string &name, const std::string &value);
bool is_request_target(const std::string &s);
+bool is_host(const std::string &s);
} // namespace fields
} // namespace detail