diff --git a/decnet/web/router/swarm/api_heartbeat.py b/decnet/web/router/swarm/api_heartbeat.py index 0a8b3607..038889c0 100644 --- a/decnet/web/router/swarm/api_heartbeat.py +++ b/decnet/web/router/swarm/api_heartbeat.py @@ -19,7 +19,7 @@ from datetime import datetime, timezone from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from decnet.config import DeckyConfig from decnet.logging import get_logger @@ -60,7 +60,8 @@ def _extract_peer_fingerprint(scope: dict[str, Any]) -> Optional[str]: if chain: peer_der = chain[0] source = "primary" - except Exception: + except (AttributeError, KeyError, TypeError): + # scope["extensions"]["tls"] structure varies across uvicorn versions peer_der = None if peer_der is None: @@ -71,7 +72,8 @@ def _extract_peer_fingerprint(scope: dict[str, Any]) -> Optional[str]: peer_der = ssl_obj.getpeercert(binary_form=True) if peer_der: source = "fallback" - except Exception: + except (AttributeError, OSError): + # transport may not be an SSL transport, or the handshake may be incomplete peer_der = None if not peer_der: @@ -121,6 +123,7 @@ async def _reconcile_topology_report( try: topos = await repo.list_topologies(status=TopologyStatus.ACTIVE) except Exception: + # Non-fatal: reconcile is best-effort; the host stays alive regardless log.exception("heartbeat: could not list active topologies") return mine = [t for t in topos if t.get("target_host_uuid") == host_uuid] @@ -139,6 +142,7 @@ async def _reconcile_topology_report( try: hydrated = await hydrate(repo, tid) except Exception: + # Non-fatal: skip this topology; mutator reconcile loop will retry log.exception("heartbeat: hydrate failed tid=%s", tid) continue if hydrated is None: @@ -155,6 +159,7 @@ async def _reconcile_topology_report( tid, host_uuid, reported_id, reported_hash, expected, ) except Exception: + # Non-fatal: mutator reconcile loop will detect the mismatch again next heartbeat log.exception("heartbeat: failed to flag resync tid=%s", tid) @@ -192,7 +197,7 @@ async def heartbeat( for decky_dict in status_body.get("deckies") or []: try: d = DeckyConfig(**decky_dict) - except Exception: + except (ValidationError, TypeError): log.exception("heartbeat: skipping malformed decky payload host=%s", req.host_uuid) continue rstate = runtime.get(d.name) or {}