hermes - ✅(Solved) Fix _build_keepalive_http_client() ignores NO_PROXY, causing 502 on local endpoints [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#14966Fetched 2026-04-24 10:43:56
View on GitHub
Comments
1
Participants
2
Timeline
10
Reactions
0
Author
Participants
Timeline (top)
labeled ×4cross-referenced ×2referenced ×2closed ×1

Error Message

def _should_bypass_proxy_for_url(url: str) -> bool: try: from urllib.parse import urlparse import urllib.request host = urlparse(url).hostname or "" return urllib.request.proxy_bypass_environment(host) except Exception: return False

Root Cause

In run_agent.py, _build_keepalive_http_client() unconditionally reads the proxy from env and passes it to httpx.Client:

def _build_keepalive_http_client() -> Any:
    ...
    _proxy = _get_proxy_from_env()  # ignores NO_PROXY
    return _httpx.Client(
        transport=_httpx.HTTPTransport(socket_options=_sock_opts),
        proxy=_proxy,  # hard-coded, no bypass check
    )

When transport= is passed explicitly, httpx no longer auto-reads NO_PROXY (see httpx docs).

Fix Action

Workaround

Until fixed, unset proxy env vars before running Hermes:

unset HTTP_PROXY HTTPS_PROXY
hermes ...

Or bypass Bifrost and connect directly to the remote API (which does need the proxy).

PR fix notes

PR #15052: fix: P1 batch — Discord wildcard, ACP+MCP, NO_PROXY bypass, resume-after-compression

Description (problem / solution / changelog)

Four high-impact P1 fixes, salvaged with contributor attribution preserved via rebase-merge.

Fixes

  • #14920 — Discord wildcard "*" silently drops all messages Salvaged from @mrunmayee17's PR #14930 (allowed_channels). Extended to cover free_response_channels and ignored_channels which had the same "*" literal-in-set bug, and added regression tests for all three.
  • #14986 — MCP tools invisible in ACP sessions (hardcoded toolsets) Salvaged from @camaragon's PR #14709 unchanged. ACP sessions now expand enabled_toolsets with configured MCP servers at agent creation, and the tool-surface refresh after dynamic MCP server registration preserves the additions.
  • #14966 — _build_keepalive_http_client ignores NO_PROXY → 502 on local endpoints Salvaged from @shamork's PR #14546 unchanged. Added regression tests for _get_proxy_for_base_url() and the full keepalive-client path.
  • #15000 — --resume <id> loads empty chat after context compression New fix (no contributor PR existed). SessionDB.resolve_resume_session_id() walks the parent_session_id chain forward and redirects resume targets to the first descendant with messages. Wired into all three CLI resume entry points (_preload_resumed_session, _init_agent, /resume).

Also closed

  • #14933, #14938 — DeepSeek V4 reasoning_content issues closed as unactionable (both submitted with empty issue bodies; asked reporters to re-file with a reproducer).
  • PRs #14930, #14709, #14546 will be closed with credit referencing this PR after merge.
  • PR #14885 (duplicate NO_PROXY fix by @fqx) will be closed with credit — @shamork's implementation was preferred because it reuses stdlib urllib.request.proxy_bypass_environment() which correctly handles wildcards, leading dots, and CIDR-like patterns that #14885's custom matcher missed.

Attribution

FixContributorEmail → GH
#14920@mrunmayee17[email protected]
#14986@camaragonnoreply form
#14966@shamork[email protected]

All three added to scripts/release.py AUTHOR_MAP in the tail commit.

Validation

Test fileResult
tests/gateway/test_discord_allowed_channels.py15/15 ✓
tests/acp/test_session.py + tests/acp/test_server.py83/83 ✓
tests/run_agent/test_create_openai_client_proxy_env.py9/9 ✓ (4 new NO_PROXY tests)
tests/hermes_state/test_resolve_resume_session_id.py8/8 ✓ (new file)

E2E for #15000: reproduced the exact 6-session compression chain from the issue body (5 empty + 1 with 119 messages); resolve_resume_session_id correctly redirects to the 5th session from any of the 5 empty ones and returns the msg-bearing session unchanged.

Merge method

Rebase merge — each contributor's cherry-picked commit preserves their authorship. Squash would flatten all seven commits into one authored by us, losing credit.

Closes #14920, #14966, #14986, #15000 Supersedes #14546, #14709, #14885, #14930

Changed files

  • acp_adapter/server.py (modified, +9/-3)
  • acp_adapter/session.py (modified, +28/-1)
  • cli.py (modified, +49/-0)
  • gateway/platforms/discord.py (modified, +13/-4)
  • hermes_state.py (modified, +65/-0)
  • run_agent.py (modified, +26/-5)
  • scripts/release.py (modified, +4/-0)
  • tests/acp/test_server.py (modified, +7/-1)
  • tests/acp/test_session.py (modified, +37/-0)
  • tests/gateway/test_discord_allowed_channels.py (added, +104/-0)
  • tests/hermes_state/test_resolve_resume_session_id.py (added, +96/-0)
  • tests/run_agent/test_create_openai_client_proxy_env.py (modified, +76/-1)

PR #14885: fix: honor NO_PROXY in _build_keepalive_http_client

Description (problem / solution / changelog)

Problem

When a custom transport= is passed to httpx.Client, httpx disables automatic env-var proxy handling (allow_env_proxies requires transport=None). The previous commit addressed this by reading HTTP_PROXY explicitly via proxy=, but that causes httpx to route all requests through the proxy — NO_PROXY is silently ignored when proxy= is set explicitly.

This breaks any LLM endpoint whose hostname appears in NO_PROXY (e.g. Tailscale IPs, internal addresses). The keepalive client would receive 503s from the proxy instead of connecting directly to the model server.

Fix

Switch from proxy= to a mounts= dict:

  • Each host in NO_PROXY gets a direct HTTPTransport (with keepalive socket options).
  • Fall-through patterns (http://, https://) route through the proxy.
  • If no proxy is configured, fall back to a plain direct client as before.

This mirrors how httpx's own env-var proxy logic works internally.

Reproduction

On a system with HTTP_PROXY pointing to an outbound proxy and NO_PROXY containing an internal/Tailscale LLM endpoint:

  • Before: all requests from the keepalive client go through the proxy → 503 from proxy
  • After: NO_PROXY hosts connect directly; everything else proxied correctly

Changed files

  • run_agent.py (modified, +16/-5)

Code Example

# ~/.hermes/.env
HTTP_PROXY=http://192.168.10.5:7890
HTTPS_PROXY=http://192.168.10.5:7890
NO_PROXY=127.0.0.1,localhost,192.168.88.0/24

---

$ hermes --model kimi-code/kimi-k2.6 --base-url http://127.0.0.1:8080/v1
# → API call failed: HTTP 502

---

def _build_keepalive_http_client() -> Any:
    ...
    _proxy = _get_proxy_from_env()  # ignores NO_PROXY
    return _httpx.Client(
        transport=_httpx.HTTPTransport(socket_options=_sock_opts),
        proxy=_proxy,  # hard-coded, no bypass check
    )

---

def _should_bypass_proxy_for_url(url: str) -> bool:
    try:
        from urllib.parse import urlparse
        import urllib.request
        host = urlparse(url).hostname or ""
        return urllib.request.proxy_bypass_environment(host)
    except Exception:
        return False

---

if "http_client" not in client_kwargs:
    _base_url = str(client_kwargs.get("base_url", "") or "")
    _proxy = None if _should_bypass_proxy_for_url(_base_url) else _get_proxy_from_env()
    keepalive_http = self._build_keepalive_http_client(proxy=_proxy)
    ...

---

unset HTTP_PROXY HTTPS_PROXY
hermes ...
RAW_BUFFERClick to expand / collapse

Bug description

When HTTP_PROXY / HTTPS_PROXY environment variables are set, Hermes creates a custom httpx.Client with an explicit proxy argument in _build_keepalive_http_client(). This bypasses httpx's built-in NO_PROXY handling, causing requests to local endpoints (e.g. http://127.0.0.1:8080/v1 for a local Bifrost gateway) to be forwarded through the proxy, which returns 502 Bad Gateway.

Minimal reproduction

# ~/.hermes/.env
HTTP_PROXY=http://192.168.10.5:7890
HTTPS_PROXY=http://192.168.10.5:7890
NO_PROXY=127.0.0.1,localhost,192.168.88.0/24
$ hermes --model kimi-code/kimi-k2.6 --base-url http://127.0.0.1:8080/v1
# → API call failed: HTTP 502

Direct curl to the same endpoint works fine. Only Hermes fails because it injects proxy=http://192.168.10.5:7890 into httpx.Client regardless of NO_PROXY.

Root cause

In run_agent.py, _build_keepalive_http_client() unconditionally reads the proxy from env and passes it to httpx.Client:

def _build_keepalive_http_client() -> Any:
    ...
    _proxy = _get_proxy_from_env()  # ignores NO_PROXY
    return _httpx.Client(
        transport=_httpx.HTTPTransport(socket_options=_sock_opts),
        proxy=_proxy,  # hard-coded, no bypass check
    )

When transport= is passed explicitly, httpx no longer auto-reads NO_PROXY (see httpx docs).

Suggested fix

Check NO_PROXY against the request base_url before passing proxy to httpx.Client. Python's urllib.request.proxy_bypass_environment() handles domains, IPs, and CIDR ranges correctly:

def _should_bypass_proxy_for_url(url: str) -> bool:
    try:
        from urllib.parse import urlparse
        import urllib.request
        host = urlparse(url).hostname or ""
        return urllib.request.proxy_bypass_environment(host)
    except Exception:
        return False

Then in _create_openai_client():

if "http_client" not in client_kwargs:
    _base_url = str(client_kwargs.get("base_url", "") or "")
    _proxy = None if _should_bypass_proxy_for_url(_base_url) else _get_proxy_from_env()
    keepalive_http = self._build_keepalive_http_client(proxy=_proxy)
    ...

Workaround

Until fixed, unset proxy env vars before running Hermes:

unset HTTP_PROXY HTTPS_PROXY
hermes ...

Or bypass Bifrost and connect directly to the remote API (which does need the proxy).

extent analysis

TL;DR

Check the NO_PROXY environment variable against the request URL before passing the proxy to httpx.Client to prevent local endpoints from being forwarded through the proxy.

Guidance

  • Modify the _build_keepalive_http_client function to conditionally pass the proxy to httpx.Client based on the NO_PROXY environment variable and the request URL.
  • Use the urllib.request.proxy_bypass_environment function to correctly handle domains, IPs, and CIDR ranges in the NO_PROXY variable.
  • As a temporary workaround, unset the HTTP_PROXY and HTTPS_PROXY environment variables before running Hermes or bypass the local Bifrost gateway and connect directly to the remote API.

Example

def _should_bypass_proxy_for_url(url: str) -> bool:
    try:
        from urllib.parse import urlparse
        import urllib.request
        host = urlparse(url).hostname or ""
        return urllib.request.proxy_bypass_environment(host)
    except Exception:
        return False

Notes

The provided fix and workaround assume that the NO_PROXY environment variable is correctly set and that the httpx library is being used as intended. Additional testing and verification may be necessary to ensure the fix works as expected in all scenarios.

Recommendation

Apply the suggested fix by modifying the _build_keepalive_http_client function to conditionally pass the proxy to httpx.Client based on the NO_PROXY environment variable and the request URL, as this will provide a more robust and long-term solution to the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING