fix(overview): one bad health response no longer stops the dashboard host (scadaproj#29)

ZbHealthReportClient caught only HttpRequestException/OperationCanceledException,
so an unknown Content-Type charset (InvalidOperationException from
ReadAsStringAsync) escaped the probe, faulted Task.WhenAll and the poller
BackgroundService, and StopHost took the dashboard down.

- client: final catch-all returns an Unreachable result for that instance
- poller: per-target Guarded probe + catch-and-log per sweep in ExecuteAsync
- tests: unknown charset (both tiers), arbitrary handler exception, sweep with
  one bad instance, and the BackgroundService survival loop
- code-reviews: Overview-001 Resolved; README regenerated

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 14:42:23 -05:00
parent 477245f5db
commit c25acd6093
6 changed files with 172 additions and 17 deletions
+3 -3
View File
@@ -10,7 +10,7 @@
| Last reviewed | 2026-11-09 |
| Reviewer | Claude |
| Commit reviewed | `91fb2b4` |
| Open findings | 5 |
| Open findings | 4 |
## Summary
@@ -71,7 +71,7 @@ live-acceptance checks).
| Severity | High |
| Priority | P1 |
| Category | Error handling & resilience |
| Status | Open |
| Status | Resolved |
| Location | `ZB.MOM.WW.Overview/src/ZB.MOM.WW.Overview/Polling/ZbHealthReportClient.cs:99` (catch list `:122-134`; unguarded loop `ZB.MOM.WW.Overview/src/ZB.MOM.WW.Overview/Polling/OverviewPollerService.cs:86-95`) |
**Description**
@@ -125,7 +125,7 @@ its first sweep: a crash loop. Operators lose the fleet view because of a single
**Resolution**
_Unresolved._
Resolved 2026-11-09 in the commit tagged `(scadaproj#29)` — `ZbHealthReportClient.SendAsync` now ends with a catch-all that returns an Unreachable result (keeping the status code when one was received), so an unknown `charset` or any other non-shutdown exception costs only that instance. Defence in depth: `OverviewPollerService` wraps each per-target probe (`Guarded`) and catches-and-logs per sweep in `ExecuteAsync`, so neither a throwing probe nor a throwing sweep can fault the `BackgroundService`. Regression tests: `ZbHealthReportClientTests.Probe_UnknownCharset_IsUnreachableNotThrown` (both tiers), `Probe_ArbitraryHandlerException_IsUnreachableNotThrown`, `OverviewPollerServiceTests.Sweep_OneInstanceWithAnUnreadableBody_DoesNotFaultTheSweep` and `ExecuteLoop_SurvivesAnInstanceWithAnUnreadableBody` — all five fail without the fix (the loop test with the same `InvalidOperationException` from the finding); the poller tests also pass with the client fix reverted, proving the per-target guard independently.
### Overview-002 — Leader address is resolved against every instance of the application, not the group, so co-located groups steal each other's chip