hermes - 💡(How to fix) Fix concurrency: TOCTOU race in get_async_client() leaks orphaned async HTTP client

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…

tools/openrouter_client.py:get_async_client() has a time-of-check/time-of-use (TOCTOU) race that can create multiple orphaned async HTTP clients, causing resource leaks and potential process-exit hangs.

Root Cause

_client = None

def get_async_client():
    global _client
    if _client is None:                              # ← both threads pass
        client, _model = resolve_provider_client(...)  # ← both create client
        _client = client                             # ← second write wins
    return _client

Two threads calling get_async_client() concurrently can both observe _client is None, both call resolve_provider_client("openrouter", async_mode=True), and race to write _client. The loser's async client is never returned or closed, leaving an orphaned httpx.AsyncClient with an open connection pool.

Fix Action

Fix / Workaround

  • Resource leak: orphaned async client holds OS file descriptors for the HTTP connection pool
  • Process-exit hang / ResourceWarning: httpx.AsyncClient and AsyncHttpxClientWrapper emit warnings (or block) when their pools are garbage-collected after the event loop is closed — this is the root of the AsyncHttpxClientWrapper.__del__ noise that agent/auxiliary_client.py already documents and workarounds

Code Example

_client = None

def get_async_client():
    global _client
    if _client is None:                              # ← both threads pass
        client, _model = resolve_provider_client(...)  # ← both create client
        _client = client                             # ← second write wins
    return _client

---

import threading

_client = None
_client_lock = threading.Lock()

def get_async_client():
    global _client
    if _client is None:
        with _client_lock:
            if _client is None:
                client, _model = resolve_provider_client("openrouter", async_mode=True)
                if client is None:
                    raise ValueError("OPENROUTER_API_KEY environment variable not set")
                _client = client
    return _client
RAW_BUFFERClick to expand / collapse

Summary

tools/openrouter_client.py:get_async_client() has a time-of-check/time-of-use (TOCTOU) race that can create multiple orphaned async HTTP clients, causing resource leaks and potential process-exit hangs.

Root Cause

_client = None

def get_async_client():
    global _client
    if _client is None:                              # ← both threads pass
        client, _model = resolve_provider_client(...)  # ← both create client
        _client = client                             # ← second write wins
    return _client

Two threads calling get_async_client() concurrently can both observe _client is None, both call resolve_provider_client("openrouter", async_mode=True), and race to write _client. The loser's async client is never returned or closed, leaving an orphaned httpx.AsyncClient with an open connection pool.

Impact

  • Resource leak: orphaned async client holds OS file descriptors for the HTTP connection pool
  • Process-exit hang / ResourceWarning: httpx.AsyncClient and AsyncHttpxClientWrapper emit warnings (or block) when their pools are garbage-collected after the event loop is closed — this is the root of the AsyncHttpxClientWrapper.__del__ noise that agent/auxiliary_client.py already documents and workarounds

Expected Fix

Double-checked locking, consistent with the pattern used in agent/auxiliary_client.py (which already has _client_cache_lock):

import threading

_client = None
_client_lock = threading.Lock()

def get_async_client():
    global _client
    if _client is None:
        with _client_lock:
            if _client is None:
                client, _model = resolve_provider_client("openrouter", async_mode=True)
                if client is None:
                    raise ValueError("OPENROUTER_API_KEY environment variable not set")
                _client = client
    return _client

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

hermes - 💡(How to fix) Fix concurrency: TOCTOU race in get_async_client() leaks orphaned async HTTP client