From 4a7db8e2043d58cf1c002514b8a1415a7df7f01e Mon Sep 17 00:00:00 2001 From: Tasnim Kabir Sadik Date: Sat, 2 May 2026 19:02:39 +0600 Subject: [PATCH] fix: set httpx read timeout for long-running commands and handle non-JSON error responses - Set per-request httpx timeout (command timeout + 10s buffer) in Commands.run() and AsyncCommands.run() for foreground exec calls, preventing HTTP read timeouts on long-running commands - Raise WrennInternalError instead of raw httpx.HTTPStatusError when handle_response() encounters a non-JSON error body (e.g. 502 from a reverse proxy) --- .gitignore | 3 +++ src/wrenn/commands.py | 20 +++++++++++++++++--- src/wrenn/exceptions.py | 7 +++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 23b2ad4..155c313 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,6 @@ cython_debug/ .pypirc CODE_EXECUTION.md + +.opencode/ +.claude/ diff --git a/src/wrenn/commands.py b/src/wrenn/commands.py index 7ca9f44..c8a0380 100644 --- a/src/wrenn/commands.py +++ b/src/wrenn/commands.py @@ -4,7 +4,7 @@ import base64 import json from collections.abc import AsyncIterator, Iterator from dataclasses import dataclass -from typing import overload, Literal +from typing import Literal, overload import httpx import httpx_ws @@ -197,7 +197,15 @@ class Commands: if tag is not None: payload["tag"] = tag - resp = self._http.post(f"/v1/capsules/{self._capsule_id}/exec", json=payload) + http_timeout: httpx.Timeout | None = None + if not background and timeout is not None: + http_timeout = httpx.Timeout(timeout + 10, connect=5.0) + + resp = self._http.post( + f"/v1/capsules/{self._capsule_id}/exec", + json=payload, + timeout=http_timeout, + ) data = handle_response(resp) if background: @@ -374,8 +382,14 @@ class AsyncCommands: if tag is not None: payload["tag"] = tag + http_timeout: httpx.Timeout | None = None + if not background and timeout is not None: + http_timeout = httpx.Timeout(timeout + 10, connect=5.0) + resp = await self._http.post( - f"/v1/capsules/{self._capsule_id}/exec", json=payload + f"/v1/capsules/{self._capsule_id}/exec", + json=payload, + timeout=http_timeout, ) data = handle_response(resp) diff --git a/src/wrenn/exceptions.py b/src/wrenn/exceptions.py index 438cfcb..0f63506 100644 --- a/src/wrenn/exceptions.py +++ b/src/wrenn/exceptions.py @@ -115,8 +115,11 @@ def handle_response(resp: httpx.Response) -> dict | list: try: body = resp.json() except Exception: - resp.raise_for_status() - raise + raise WrennInternalError( + code="internal_error", + message=resp.text or f"HTTP {resp.status_code}", + status_code=resp.status_code, + ) err = body.get("error", {}) code = err.get("code", "internal_error")