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