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) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-07 15:08:26 +02:00
co-authored by Claude Opus 4.8
parent a8759f91d5
commit d73ce7b559
+16 -1
View File
@@ -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:"-"`