# NousResearch/hermes-agent · Streaming error body read crash in API error handler

## Open now

- Nothing open on GitHub.

A bug where \`_summarize_api_error\` crashes with \`httpx.ResponseNotRead\` when handling streaming errors (such as Gemini free-tier 429s), hiding the real error message. Multiple fix attempts guard the \`response.text\` access against this unread-response condition.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #61112. Verify it addresses your case.
- **Recommended:** read PR #61112 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-61112`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #59769: \[Bug\]: _summarize_api_error crashes with httpx.ResponseNotRead on Gemini streaming errors, hiding the real message (e.g. free-tier quota 429s)

## Prior & competing PRs

- **#61112: fix(agent): guard response.text access in _summarize_api_error against httpx.ResponseNotRead**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `run_agent.py`, `scripts/release.py`, `tests/run_agent/test_summarize_api_error.py`
  - fix hunks:
  - `run_agent.py`

    ```diff
    @@ -2143,7 +2143,10 @@ def _summarize_api_error(error: Exception) -> str:
             # 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 ""
    ```
  - `scripts/release.py`

    ```diff
    @@ -59,6 +59,7 @@
         "[redacted:email]": "ManniBr",  # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles)
         "[redacted:email]": "austinlaw076",  # PR #57563 partial salvage (auth: lazy per-profile Anthropic OAuth file; gateway: whatsapp_cloud/line added to port-binding platform set)
         "[redacted:email]": "liuhao1024",  # PR #56993 salvage (gateway: process-level HERMES_HOME for pid/lock/status identity files; #56986)
    +    "[redacted:email]": "gauravsaxena1997",  # PR #59868 partial salvage (agent: guard response.text against httpx.ResponseNotRead in _summarize_api_error; #59769)
         "[redacted:email]": "irresi",  # PR #59048 salvage (gateway: scope reset banners' session info to the serving profile; #59003)
         "[redacted:email]": "s905060",  # PR #57943 salvage (photon: auto-reinstall stale sidecar node_modules when lockfile is newer than npm's install marker; #59169)
         "[redacted:email]": "l0h1nth",  # PR #32210 salvage (mattermost: accept leading-space slash commands from mobile clients; #25184)
    ```
  - `tests/run_agent/test_summarize_api_error.py`

    ```diff
    @@ -11,6 +11,9 @@
     """
     
     from types import SimpleNamespace
    +from typing import Any
    +
    +import httpx
     
     from run_agent import AIAgent
     
    ```

    ```diff
    @@ -54,3 +57,25 @@ def test_empty_body_fallback_redacts_secrets(monkeypatch):
         summary = AIAgent._summarize_api_error(err)
         assert "[redacted:openai-key]" 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
    +
    ```
- **#59774: fix(agent): guard response.text access in _summarize_api_error against httpx.ResponseNotRead**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#61112`, `#59868`, `#59868`
- **#59791: fix(agent): handle unread streaming response in _summarize_api_error (#59769)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#59868`, `#61112`, `#59868`
- **#60192: fix(agent): guard response.text access in _summarize_api_error against ResponseNotRead (#59769)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#59868`, `#61112`, `#59868`

## Fix status

- Merged to main in PR #61112 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-08`.
