Skip to content

Fix: use httpx.AsyncClient in async FastAPI handlers to prevent event loop blocking (fixes #559)#570

Open
SoulSniper-V2 wants to merge 2 commits into
open-jarvis:mainfrom
SoulSniper-V2:fix-async-handlers-559
Open

Fix: use httpx.AsyncClient in async FastAPI handlers to prevent event loop blocking (fixes #559)#570
SoulSniper-V2 wants to merge 2 commits into
open-jarvis:mainfrom
SoulSniper-V2:fix-async-handlers-559

Conversation

@SoulSniper-V2

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR resolves issue #559 by converting several blocking, synchronous httpx.Client() calls inside async def FastAPI routes into their httpx.AsyncClient() equivalents. It also offloads the CPU-bound backend.transcribe() call in api_routes.py to a separate thread via asyncio.to_thread(). This prevents the Uvicorn event loop from freezing and causing poor latency for other parallel requests.

Changes

  • Replaced httpx.Client with async with httpx.AsyncClient in server/routes.py (/v1/models/pull, /v1/models/{name}).
  • Replaced httpx.get/post with async with httpx.AsyncClient in server/agent_manager_routes.py (/verify, /register-webhook, /test).
  • Offloaded Whisper transcription in server/api_routes.py to asyncio.to_thread.
  • Added a regression test tests/server/test_async_handlers_nonblocking.py to verify event loop non-blocking behavior.

Testing

  • Added unit/integration tests
  • Ran locally
  • Verified endpoints

Fixes Issue

Fixes #559

Copilot AI review requested due to automatic review settings June 19, 2026 19:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses #559 by removing blocking I/O and CPU work from FastAPI async routes, primarily by switching outbound HTTP calls to httpx.AsyncClient and offloading synchronous transcription work to a thread to keep the Uvicorn event loop responsive.

Changes:

  • Converted Ollama model pull/delete routes to use httpx.AsyncClient instead of httpx.Client.
  • Converted SendBlue verify/register/test routes to use httpx.AsyncClient instead of httpx.get/post.
  • Offloaded backend.transcribe() in the speech transcription route using asyncio.to_thread(), and added a regression test for non-blocking behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
tests/server/test_async_handlers_nonblocking.py Adds a regression test intended to verify the event loop remains responsive during slow outbound requests.
src/openjarvis/server/routes.py Uses httpx.AsyncClient in async model pull/delete handlers to avoid blocking the event loop.
src/openjarvis/server/api_routes.py Runs synchronous speech transcription in a thread (asyncio.to_thread) to avoid blocking.
src/openjarvis/server/agent_manager_routes.py Uses httpx.AsyncClient in async SendBlue routes to avoid blocking the event loop.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +57 to +60
# Fire both requests concurrently
slow_req = asyncio.create_task(
client.post("/v1/models/pull", json={"name": "test-model"})
)
Comment on lines +52 to +54
with pytest.MonkeyPatch.context() as m:
m.setattr("httpx.AsyncClient.post", mock_post)

Comment on lines 762 to +771
import httpx as _httpx

host = getattr(engine, "_host", "http://localhost:11434")
client = _httpx.Client(base_url=host, timeout=600.0)
try:
resp = client.post(
"/api/pull",
json={"name": model_name, "stream": False},
)
resp.raise_for_status()
async with _httpx.AsyncClient(base_url=host, timeout=600.0) as client:
resp = await client.post(
"/api/pull",
json={"name": model_name, "stream": False},
)
resp.raise_for_status()
jonsaadfalcon pushed a commit that referenced this pull request Jul 1, 2026
Closes #219. Replace synchronous httpx calls in async SendBlue and model-management handlers with awaited httpx.AsyncClient (context-managed close); run Whisper transcription and engine.list_models via asyncio.to_thread so they don't block the event loop; and harden TelemetryStore/aggregator SQLite for concurrency (WAL, synchronous=NORMAL, busy_timeout=5000, plus a write-serializing lock on the shared connection). Adds async-usage assertions and a real 8-thread concurrent-write test. Related: #570 (async httpx, different issue #559).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Fix: use httpx.AsyncClient in async FastAPI handlers to prevent event loop blocking

3 participants