Commit 7182ef71e for llama.cpp
commit 7182ef71e6d5dd0c8ea76486fac134fcd25025d4
Author: texasich <101962694+texasich@users.noreply.github.com>
Date: Fri Oct 16 10:01:37 2026 -0500
common: honor HTTP_PROXY/HTTPS_PROXY env vars in http client (#21752)
* common: honor HTTP_PROXY/HTTPS_PROXY env vars in http client
common_http_client() was ignoring standard proxy env vars, causing
downloads via -hf and similar flags to fail silently in environments
behind an HTTP proxy (corporate networks, air-gapped infra, etc.).
Checks HTTPS_PROXY/https_proxy for https:// URLs and HTTP_PROXY/http_proxy
for http:// URLs, in that order. If a proxy URL is set and parseable,
passes it to httplib's set_proxy() and set_proxy_basic_auth() as
appropriate. Malformed proxy URLs are silently ignored so existing
callers aren't affected.
Fixes #21694
* common: drop misleading comment about generic proxy fallback
The comment referenced a 'generic one' that doesn't exist in the code — both
HTTP_PROXY and HTTPS_PROXY are scheme-specific. Reviewer noted the mismatch.
Addresses review feedback from @ServeurpersoCom.
* common : warn when the proxy URL from the environment is malformed
* common : honor NO_PROXY when selecting the http proxy
* ci : retrigger checks (test-thread-safety flake)
* common : bypass the proxy for loopback destinations
llama cli talks to its spawned server on 127.0.0.1 through common_http_client, so with HTTP_PROXY set and no NO_PROXY the health polling went to the corporate proxy, which rejects loopback targets, and the wait loop never exits ("Loading model..." hangs forever).
A remote proxy can never reach the client machine's own loopback, so these destinations are now always contacted directly, before NO_PROXY is consulted: localhost, *.localhost, 127.0.0.0/8, ::1 and 0.0.0.0. Similar to Go's net/http. Proxy handling for downloads and remote --server-base endpoints is unchanged.
Signed-off-by: texasich <101962694+texasich@users.noreply.github.com>
* docs: move proxy tip from README to server docs
per review: the top level README stays lean; the proxy howto now lives with the server docs under Usage
Signed-off-by: texasich <101962694+texasich@users.noreply.github.com>
---------
Signed-off-by: texasich <101962694+texasich@users.noreply.github.com>
Co-authored-by: texasich <texasich@users.noreply.github.com>
Co-authored-by: texasich <texasich@gmail.com>
Co-authored-by: marecabo <23156476+marecabo@users.noreply.github.com>
diff --git a/common/http.h b/common/http.h
index 878ad1ce2..10d017f17 100644
--- a/common/http.h
+++ b/common/http.h
@@ -1,5 +1,7 @@
#pragma once
+#include "log.h"
+
#include <cpp-httplib/httplib.h>
#ifdef _WIN32
@@ -97,6 +99,118 @@ static common_http_url common_http_parse_url(const std::string & url) {
return parts;
}
+// match a loopback destination: a remote proxy can never reach the client machine's own
+// loopback, so these hosts are always contacted directly, before NO_PROXY is consulted
+// (similar to Go's net/http)
+static bool common_http_is_loopback(const std::string & host) {
+ auto lower = [](std::string s) {
+ for (auto & c : s) {
+ if (c >= 'A' && c <= 'Z') {
+ c += 'a' - 'A';
+ }
+ }
+ return s;
+ };
+
+ const std::string h = lower(host);
+
+ if (h == "localhost" || h == "::1" || h == "0.0.0.0") {
+ return true;
+ }
+
+ if (h.size() > 10 && h.compare(h.size() - 10, 10, ".localhost") == 0) {
+ return true;
+ }
+
+ // IPv4 literal: exactly four all-digit octets with values 0-255, loopback is 127.0.0.0/8
+ int octets[4] = { 0, 0, 0, 0 };
+ size_t pos = 0;
+ int n = 0;
+ bool complete = false;
+ while (n < 4 && pos <= h.size()) {
+ size_t dot = h.find('.', pos);
+ if (dot == std::string::npos) {
+ dot = h.size();
+ }
+ if (dot == pos || dot - pos > 3) {
+ return false;
+ }
+ int val = 0;
+ for (size_t i = pos; i < dot; i++) {
+ if (h[i] < '0' || h[i] > '9') {
+ return false;
+ }
+ val = val * 10 + (h[i] - '0');
+ }
+ if (val > 255) {
+ return false;
+ }
+ octets[n++] = val;
+ if (dot == h.size()) {
+ complete = (n == 4);
+ break;
+ }
+ pos = dot + 1;
+ }
+
+ return complete && octets[0] == 127;
+}
+
+// match a host against a NO_PROXY list: comma separated entries, "*" bypasses the proxy for every host,
+// an entry matches the host itself or any of its subdomains, a leading dot is optional, ports are ignored
+static bool common_http_proxy_bypass(const std::string & no_proxy, const std::string & host) {
+ auto lower = [](std::string s) {
+ for (auto & c : s) {
+ if (c >= 'A' && c <= 'Z') {
+ c += 'a' - 'A';
+ }
+ }
+ return s;
+ };
+
+ const std::string h = lower(host);
+
+ for (size_t pos = 0; pos < no_proxy.size(); ) {
+ size_t end = no_proxy.find(',', pos);
+ if (end == std::string::npos) {
+ end = no_proxy.size();
+ }
+
+ std::string entry = lower(no_proxy.substr(pos, end - pos));
+ pos = end + 1;
+
+ entry.erase(0, entry.find_first_not_of(" \t"));
+ auto last = entry.find_last_not_of(" \t");
+ if (last == std::string::npos) {
+ continue;
+ }
+ entry.erase(last + 1);
+
+ if (entry == "*") {
+ return true;
+ }
+
+ if (entry.front() == '.') {
+ entry.erase(0, 1);
+ }
+
+ if (entry.empty()) {
+ continue;
+ }
+
+ if (h == entry) {
+ return true;
+ }
+
+ if (h.size() > entry.size() && h[h.size() - entry.size() - 1] == '.' &&
+ h.compare(h.size() - entry.size(), entry.size(), entry) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
static std::pair<httplib::Client, common_http_url> common_http_client(const std::string & url) {
common_http_url parts = common_http_parse_url(url);
@@ -123,6 +237,33 @@ static std::pair<httplib::Client, common_http_url> common_http_client(const std:
cli.set_follow_location(true);
+ // Honor HTTP_PROXY / HTTPS_PROXY environment variables (both lower- and upper-case).
+ auto getenv_s = [](const char * upper, const char * lower) -> std::string {
+ const char * val = std::getenv(upper);
+ if (!val) {
+ val = std::getenv(lower);
+ }
+ return val ? val : "";
+ };
+
+ const std::string proxy_url = (parts.scheme == "https")
+ ? getenv_s("HTTPS_PROXY", "https_proxy")
+ : getenv_s("HTTP_PROXY", "http_proxy");
+
+ if (!proxy_url.empty() && !common_http_is_loopback(parts.host) &&
+ !common_http_proxy_bypass(getenv_s("NO_PROXY", "no_proxy"), parts.host)) {
+ try {
+ common_http_url proxy = common_http_parse_url(proxy_url);
+ cli.set_proxy(proxy.host, proxy.port);
+ if (!proxy.user.empty()) {
+ cli.set_proxy_basic_auth(proxy.user, proxy.password);
+ }
+ } catch (const std::exception & e) {
+ // fall back to a direct connection, the URL itself is not logged as it can carry credentials
+ LOG_WRN("%s: ignoring malformed proxy URL from the environment: %s\n", __func__, e.what());
+ }
+ }
+
return { std::move(cli), std::move(parts) };
}
diff --git a/tools/server/README.md b/tools/server/README.md
index 4e4472ec2..7d697633e 100644
--- a/tools/server/README.md
+++ b/tools/server/README.md
@@ -405,6 +405,10 @@ Recommended `--cors-origins` setting, depending on where the server runs:
Related flags: `--cors-origins`, `--cors-methods`, `--cors-headers`, `--cors-credentials` / `--no-cors-credentials`. Background and rationale: [#25655](https://github.com/ggml-org/llama.cpp/pull/25655).
+### Running behind a proxy
+
+If your host reaches the internet through a corporate proxy, export `HTTP_PROXY` and `HTTPS_PROXY` before running commands that download models (e.g. `export HTTPS_PROXY=http://proxy.example.com:8080`). Hosts listed in `NO_PROXY` and loopback addresses are contacted directly.
+
## Build
`llama-server` is built alongside everything else from the root of the project