From 7e59c4883b95b202381d3518da4ed07aa8661904 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Tue, 23 Dec 2025 11:13:54 +0100 Subject: [PATCH] fix: add HTTP timeout configuration to prevent indefinite hangs The UniFi controller HTTP client was created without a timeout, causing unpoller to hang indefinitely when the controller becomes unresponsive. This resulted in random stops where polling would cease until the container was restarted. Changes: - Add Timeout field to Controller struct (cnfg.Duration) - Set default timeout of 60 seconds - Pass timeout to unifi.Config when creating the client - Log timeout value on startup for visibility The timeout can be configured via: - Config file: timeout = "60s" - Environment: UP_UNIFI_DEFAULT_TIMEOUT=60s Fixes issue where container would hang overnight: 2025/12/22 22:29:27 - Requesting https://unifi/.../stat/sta [~2 hour gap - request hung indefinitely] 2025/12/23 00:17:57 - Unmarshalling Device Type: udm... --- pkg/inputunifi/input.go | 64 ++++++++++++++++++++++--------------- pkg/inputunifi/interface.go | 2 +- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/pkg/inputunifi/input.go b/pkg/inputunifi/input.go index 91f20c84..52838321 100644 --- a/pkg/inputunifi/input.go +++ b/pkg/inputunifi/input.go @@ -11,16 +11,18 @@ import ( "github.com/unpoller/unifi/v5" "github.com/unpoller/unpoller/pkg/poller" + "golift.io/cnfg" ) // PluginName is the name of this input plugin. const PluginName = "unifi" const ( - defaultURL = "https://127.0.0.1:8443" - defaultUser = "unifipoller" - defaultPass = "unifipoller" - defaultSite = "all" + defaultURL = "https://127.0.0.1:8443" + defaultUser = "unifipoller" + defaultPass = "unifipoller" + defaultSite = "all" + defaultTimeout = 60 * time.Second ) // InputUnifi contains the running data. @@ -34,28 +36,29 @@ type InputUnifi struct { // Controller represents the configuration for a UniFi Controller. // Each polled controller may have its own configuration. type Controller struct { - VerifySSL *bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` - SaveAnomal *bool `json:"save_anomalies" toml:"save_anomalies" xml:"save_anomalies" yaml:"save_anomalies"` - SaveAlarms *bool `json:"save_alarms" toml:"save_alarms" xml:"save_alarms" yaml:"save_alarms"` - SaveEvents *bool `json:"save_events" toml:"save_events" xml:"save_events" yaml:"save_events"` - SaveSyslog *bool `json:"save_syslog" toml:"save_syslog" xml:"save_syslog" yaml:"save_syslog"` - SaveProtectLogs *bool `json:"save_protect_logs" toml:"save_protect_logs" xml:"save_protect_logs" yaml:"save_protect_logs"` - ProtectThumbnails *bool `json:"protect_thumbnails" toml:"protect_thumbnails" xml:"protect_thumbnails" yaml:"protect_thumbnails"` - SaveIDs *bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"` - SaveDPI *bool `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"` - SaveRogue *bool `json:"save_rogue" toml:"save_rogue" xml:"save_rogue" yaml:"save_rogue"` - HashPII *bool `json:"hash_pii" toml:"hash_pii" xml:"hash_pii" yaml:"hash_pii"` - DropPII *bool `json:"drop_pii" toml:"drop_pii" xml:"drop_pii" yaml:"drop_pii"` - SaveSites *bool `json:"save_sites" toml:"save_sites" xml:"save_sites" yaml:"save_sites"` - CertPaths []string `json:"ssl_cert_paths" toml:"ssl_cert_paths" xml:"ssl_cert_path" yaml:"ssl_cert_paths"` - User string `json:"user" toml:"user" xml:"user" yaml:"user"` - Pass string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` - APIKey string `json:"api_key" toml:"api_key" xml:"api_key" yaml:"api_key"` - URL string `json:"url" toml:"url" xml:"url" yaml:"url"` - Sites []string `json:"sites" toml:"sites" xml:"site" yaml:"sites"` - DefaultSiteNameOverride string `json:"default_site_name_override" toml:"default_site_name_override" xml:"default_site_name_override" yaml:"default_site_name_override"` - Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"` - ID string `json:"id,omitempty"` // this is an output, not an input. + VerifySSL *bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` + SaveAnomal *bool `json:"save_anomalies" toml:"save_anomalies" xml:"save_anomalies" yaml:"save_anomalies"` + SaveAlarms *bool `json:"save_alarms" toml:"save_alarms" xml:"save_alarms" yaml:"save_alarms"` + SaveEvents *bool `json:"save_events" toml:"save_events" xml:"save_events" yaml:"save_events"` + SaveSyslog *bool `json:"save_syslog" toml:"save_syslog" xml:"save_syslog" yaml:"save_syslog"` + SaveProtectLogs *bool `json:"save_protect_logs" toml:"save_protect_logs" xml:"save_protect_logs" yaml:"save_protect_logs"` + ProtectThumbnails *bool `json:"protect_thumbnails" toml:"protect_thumbnails" xml:"protect_thumbnails" yaml:"protect_thumbnails"` + SaveIDs *bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"` + SaveDPI *bool `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"` + SaveRogue *bool `json:"save_rogue" toml:"save_rogue" xml:"save_rogue" yaml:"save_rogue"` + HashPII *bool `json:"hash_pii" toml:"hash_pii" xml:"hash_pii" yaml:"hash_pii"` + DropPII *bool `json:"drop_pii" toml:"drop_pii" xml:"drop_pii" yaml:"drop_pii"` + SaveSites *bool `json:"save_sites" toml:"save_sites" xml:"save_sites" yaml:"save_sites"` + Timeout cnfg.Duration `json:"timeout" toml:"timeout" xml:"timeout" yaml:"timeout"` + CertPaths []string `json:"ssl_cert_paths" toml:"ssl_cert_paths" xml:"ssl_cert_path" yaml:"ssl_cert_paths"` + User string `json:"user" toml:"user" xml:"user" yaml:"user"` + Pass string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` + APIKey string `json:"api_key" toml:"api_key" xml:"api_key" yaml:"api_key"` + URL string `json:"url" toml:"url" xml:"url" yaml:"url"` + Sites []string `json:"sites" toml:"sites" xml:"site" yaml:"sites"` + DefaultSiteNameOverride string `json:"default_site_name_override" toml:"default_site_name_override" xml:"default_site_name_override" yaml:"default_site_name_override"` + Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"` + ID string `json:"id,omitempty"` // this is an output, not an input. } // Config contains our configuration data. @@ -134,6 +137,7 @@ func (u *InputUnifi) getUnifi(c *Controller) error { URL: c.URL, SSLCert: certs, VerifySSL: *c.VerifySSL, + Timeout: c.Timeout.Duration, ErrorLog: u.LogErrorf, // Log all errors. DebugLog: u.LogDebugf, // Log debug messages. }) @@ -296,6 +300,10 @@ func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop if len(c.Sites) == 0 { c.Sites = []string{defaultSite} } + + if c.Timeout.Duration == 0 { + c.Timeout.Duration = defaultTimeout + } } // setControllerDefaults sets defaults for the for controllers. @@ -393,6 +401,10 @@ func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint c.DefaultSiteNameOverride = u.Default.DefaultSiteNameOverride } + if c.Timeout.Duration == 0 { + c.Timeout = u.Default.Timeout + } + return c } diff --git a/pkg/inputunifi/interface.go b/pkg/inputunifi/interface.go index 4dd13181..60e3be52 100644 --- a/pkg/inputunifi/interface.go +++ b/pkg/inputunifi/interface.go @@ -114,7 +114,7 @@ func (u *InputUnifi) DebugInput() (bool, error) { } func (u *InputUnifi) logController(c *Controller) { - u.Logf(" => URL: %s (verify SSL: %v)", c.URL, *c.VerifySSL) + u.Logf(" => URL: %s (verify SSL: %v, timeout: %v)", c.URL, *c.VerifySSL, c.Timeout.Duration) if len(c.CertPaths) > 0 { u.Logf(" => Cert Files: %s", strings.Join(c.CertPaths, ", "))