From d73ce7b55962af03d60b2ec9eb007e2d92360656 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 7 Jun 2026 13:58:06 +0200 Subject: [PATCH] fix(setup): bound speaker HTTP GETs so offline devices fail fast (refs #451) Manager.HTTPGet defaulted to http.Get, which uses http.DefaultClient with no timeout. An offline speaker therefore hung the caller for the OS-level TCP timeout (~30 s). The admin device list refreshes every device's live /info on each load (updateDeviceInfo per row), so a handful of offline speakers each held a request for 30 s. Server-side those run concurrently and never blocked other routes, but the browser's ~6-connections-per-origin limit got saturated by the long-held /info requests, which made the whole admin page (and navigating away from it) feel stuck. Give HTTPGet a 5 s timeout (liveDeviceHTTPTimeout): ample for a healthy speaker on the LAN, quick to fail a dead one. Applies to the /info, /presets, /recents, /sources, inspect, and peer-probe GETs. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/service/setup/setup.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/service/setup/setup.go b/pkg/service/setup/setup.go index bb6e51e..ab5eb0f 100644 --- a/pkg/service/setup/setup.go +++ b/pkg/service/setup/setup.go @@ -178,12 +178,27 @@ func NewManager(serverURL string, ds *datastore.DataStore, cm *certmanager.Certi NewSession: func(deviceIP, deviceID string, stepTimeout time.Duration) (StateMachine, error) { return DialSession(deviceIP, deviceID, SessionConfig{StepTimeout: stepTimeout}) }, - HTTPGet: http.Get, + // Bound the speaker GETs (/info, /presets, /recents, /sources, + // inspect, peer probes). The default http.Get uses + // http.DefaultClient, which has no timeout, so an offline speaker + // hangs the caller for the full OS-level TCP timeout (~30 s). That + // is fine for a one-off, but the admin device list refreshes every + // device's live /info on each load, so a few offline speakers used + // to stall the page for 30 s each. A healthy speaker answers in well + // under a second on the LAN; liveDeviceHTTPTimeout fails the dead + // ones fast instead. + HTTPGet: (&http.Client{Timeout: liveDeviceHTTPTimeout}).Get, MgmtUsername: "admin", MgmtPassword: "change_me!", } } +// liveDeviceHTTPTimeout bounds the plain HTTP GETs the Manager makes to a +// speaker's :8090 endpoints. Generous enough for a healthy speaker on a +// busy LAN, short enough that an offline speaker fails quickly rather than +// holding a request (and a browser connection slot) for ~30 s. +const liveDeviceHTTPTimeout = 5 * time.Second + // DeviceInfoXML represents the XML structure from :8090/info type DeviceInfoXML struct { XMLName xml.Name `xml:"info" json:"-"`