fix(agent): guard response.text access in _summarize_api_error against httpx.ResponseNotRead

When an API error carries an httpx.Response whose body was consumed via
iter_bytes() during streaming error handling (e.g. GeminiAPIError from
agent/gemini_native_adapter.py), accessing .text raises
httpx.ResponseNotRead. The secondary exception replaced the real,
already-computed provider error (429 free-tier quota guidance) with the
generic 'Attempted to access streaming response content' message on
every turn.

Guard the .text access so it degrades to an empty snippet and falls
through to the str(error) fallback, which carries the full original
message. Mirrors the existing guards in
agent/error_classifier.py::_extract_error_body() and
agent/gemini_native_adapter.py::gemini_http_error().

Fixes #59769

Salvaged from PR #59868 (guard + regression test); the unrelated
desktop Ctrl-C fix bundled in that PR was intentionally dropped and is
triaged separately.
This commit is contained in:
gauravsaxena1997 2026-07-09 01:50:08 +05:30 committed by kshitij
parent 31e39dec84
commit 0569a637d0
2 changed files with 30 additions and 1 deletions

View file

@ -2143,7 +2143,10 @@ class AIAgent:
# widens exposure vs the old empty-body "HTTP 400" string).
response = getattr(error, "response", None)
if response is not None:
snippet = (getattr(response, "text", None) or "").strip()
try:
snippet = (getattr(response, "text", None) or "").strip()
except Exception:
snippet = ""
if snippet:
status_code = getattr(error, "status_code", None)
prefix = f"HTTP {status_code}: " if status_code else ""

View file

@ -12,6 +12,10 @@ provider error. This is a diagnostic improvement and is platform-agnostic.
from types import SimpleNamespace
from typing import Any
import httpx
from run_agent import AIAgent
@ -54,3 +58,25 @@ def test_empty_body_fallback_redacts_secrets(monkeypatch):
summary = AIAgent._summarize_api_error(err)
assert "sk-proj-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdef" not in summary
def test_unread_streaming_response_does_not_crash_and_falls_back_to_exception_message():
"""Unread streaming responses must not replace the real provider error."""
class _StreamingError(Exception):
def __init__(self):
super().__init__("Gemini HTTP 429: quota exceeded")
self.status_code = 429
self.response: Any = None
err = _StreamingError()
class _UnreadStreamingResponse:
@property
def text(self):
raise httpx.ResponseNotRead()
err.response = _UnreadStreamingResponse()
summary = AIAgent._summarize_api_error(err)
assert "HTTP 429" in summary
assert "Gemini HTTP 429: quota exceeded" in summary