Skip to content

Fix peer.py catching httpx exceptions around requests calls (#131)#181

Open
ClaydeCode wants to merge 4 commits into
mainfrom
fix/clayde/peer-requests-exception-handling
Open

Fix peer.py catching httpx exceptions around requests calls (#131)#181
ClaydeCode wants to merge 4 commits into
mainfrom
fix/clayde/peer-requests-exception-handling

Conversation

@ClaydeCode

Copy link
Copy Markdown
Contributor

Fixes #131.

Problem

shard_core/service/peer.py update_peer_meta calls requests.get(...) and then response.raise_for_status(), which raises requests.exceptions.HTTPError — but the handler caught httpx.HTTPStatusError. So any non-2xx peer response escaped the handler, propagated through the asyncio.gather in update_all_peer_pubkeys (which had no return_exceptions), and aborted the whole peer-refresh cycle for that tick (only contained by PeriodicTask retrying a minute later).

Change

  • Catch requests.HTTPError instead of httpx.HTTPStatusError, so a non-2xx peer response marks that peer unreachable (as intended) instead of escaping. Dropped the now-unused httpx import.
  • Add return_exceptions=True to the gather, with a per-peer warning log, so any unexpected exception from one peer (e.g. a wrong-identity KeyError) can no longer abort refreshing the others.
  • New tests/test_peer_refresh.py with two regression tests over update_all_peer_pubkeys.

Verification

tests/test_peer_refresh.py — 2 passed. Full suite: 227 passed; the one unrelated failure (test_app_lifecycle.py::test_app_starts_and_stops, a Docker container-status timing test) also fails on a clean origin/main on this runner, so it is pre-existing and not caused by this change.

Both new tests were proven to have teeth (each fails when the corresponding half of the fix is reverted):

  • revert the exception type → test_unreachable_peer_does_not_block_others fails (bad peer no longer marked unreachable),
  • remove return_exceptions=Truetest_unexpected_peer_error_does_not_block_others fails (gather re-raises),
  • remove the warning log → test_unexpected_peer_error_does_not_block_others fails (warning assertion).

Review panel

Three reviewers ran (adversarial always-on; test-adversary because the diff adds real logic + tests; DevEx because it touches tests). No blocking findings from any. Advisories addressed:

  • Decorative is_reachable is True assertion (test-adversary, adversarial, DevEx) — fixed: the good peer is now inserted is_reachable=False, so the success path's flip to True is actually asserted.
  • Test 1 couldn't distinguish the HTTPError path from a ConnectionError fallback (test-adversary) — fixed: assert len(rsps.calls) == 2 pins that both whoareyou requests fired.
  • Test 2 didn't assert isolation to the broken peer, and the new warning log was unasserted (test-adversary, DevEx) — fixed: assert the broken peer is left untouched and that a warning containing its short_id was emitted.
  • Exception-alias style inconsistency (DevEx) — fixed: use requests.HTTPError to read symmetrically with the adjacent requests.ConnectionError handler.

Advisories intentionally not addressed (out of scope for #131, pre-existing): other requests failure modes (Timeout, SSLError) don't mark a peer unreachable; _on_peer_write calls update_peer_meta outside the guarded gather. Neither is introduced or worsened by this diff.

🤖 Generated with Claude Code

ClaydeCode and others added 4 commits July 22, 2026 01:23
update_peer_meta calls requests.get and then response.raise_for_status(),
which raises requests.exceptions.HTTPError, but the handler caught
httpx.HTTPStatusError. Any non-2xx peer response therefore escaped the
handler instead of marking the peer unreachable. Catch the correct
exception type and drop the now-unused httpx import.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
update_all_peer_pubkeys gathered update_peer_meta for every peer without
return_exceptions, so one peer raising (e.g. an unexpected error not caught
inside update_peer_meta) aborted refreshing all remaining peers for that
cycle. Collect exceptions instead and log each, so one bad peer cannot
starve the others.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Two tests over update_all_peer_pubkeys: a peer returning 500 is marked
unreachable without blocking other peers' updates (covers the corrected
requests.HTTPError handling), and a peer raising an unexpected error does
not abort the refresh cycle for the others (covers gather return_exceptions).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Use requests.HTTPError alias to read symmetrically with the adjacent
  requests.ConnectionError handler.
- Insert the good peer as is_reachable=False so the success path's flip to
  True is actually asserted (was decorative).
- Assert both whoareyou requests fired, pinning the HTTPError branch rather
  than a silent ConnectionError fallback.
- Assert the unexpected-error peer is left untouched and that the swallowed
  failure is surfaced via a warning log.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

peer.py catches httpx exceptions around requests calls — real HTTP errors abort the peer-refresh cycle

1 participant