{"record":{"id":"026c026ef63fa004","repo":"AdguardTeam/AdGuardHome","slug":"unwatching-s-s-w","errorCode":null,"errorMessage":"unwatching %s %s: %w","messagePattern":"unwatching (.+?) (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/aghtls/defaultmanager.go","lineNumber":218,"sourceCode":"\terrs = mgr.appendUnwatchErr(errs, \"old key\", old.KeyPath)\n\terrs = mgr.appendWatchErr(errs, \"new cert\", certKey.CertPath)\n\terrs = mgr.appendWatchErr(errs, \"new key\", certKey.KeyPath)\n\n\tmgr.pair = certKey\n\n\treturn errors.Join(errs...)\n}\n\n// appendUnwatchErr stops watching a file at path p described by what and\n// appends an error to the errs slice, if any.  Empty p is ignored.\nfunc (mgr *DefaultManager) appendUnwatchErr(errs []error, what, p string) (result []error) {\n\tif p == \"\" {\n\t\treturn errs\n\t}\n\n\terr := mgr.watcher.Remove(p)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"unwatching %s %s: %w\", what, p, err))\n\t}\n\n\treturn errs\n}\n\n// appendWatchErr starts watching a file at path p described by what and\n// appends an error to the errs slice, if any.  Empty p is ignored.\nfunc (mgr *DefaultManager) appendWatchErr(errs []error, what, p string) (result []error) {\n\tif p == \"\" {\n\t\treturn errs\n\t}\n\n\terr := mgr.watcher.Add(p)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"watching %s %s: %w\", what, p, err))\n\t}\n\n\treturn errs","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/AdguardTeam/AdGuardHome/blob/b41aefbe51c8dde65e2c50f093996afa0502edf9/internal/aghtls/defaultmanager.go#L200-L236","documentation":"The default TLS manager watches cert/key files with fsnotify; when rotating certificates it must first unwatch the old paths. appendUnwatchErr collects errors from watcher.Remove — typically fsnotify.ErrNonExistentWatch when the path wasn't actually being watched.","triggerScenarios":"setLocked rotating certificates when the watcher never had a watch on the old path (previous watch failed silently), the watch was already removed, or the file was deleted before unwatch; errors are accumulated into a slice rather than failing the operation.","commonSituations":"Certificate rotation where the previous cert path changed between reloads; watchers on platforms with flaky fsnotify backends; rapid successive reloads racing watch bookkeeping.","solutions":["Treat this error as non-fatal: it's appended to an error list, so rotation proceeded; check whether the new cert actually loaded","Guard the removal: only call watcher.Remove for paths you successfully added","Upgrade fsnotify if hitting backend quirks; on Linux verify inotify watch limits aren't exceeded","Restart the service to rebuild watcher state cleanly if reloads keep failing"],"exampleFix":"// before\nerrs = appendUnwatchErr(errs, oldPath, \"certificate\")\n\n// after (defensive, if rolling your own watcher use)\nif w, ok := watcher.(*fsnotify.Watcher); ok && isWatched(w, oldPath) {\n    _ = watcher.Remove(oldPath)\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":"func isWatchMissing(err error) bool { return errors.Is(err, fsnotify.ErrNonExistentWatch) }","tryCatchPattern":"// errors are accumulated, not fatal: inspect the slice\nfor _, e := range errs {\n    if errors.Is(e, fsnotify.ErrNonExistentWatch) { continue } // benign\n    log.Warn(\"tls manager\", \"err\", e)\n}","preventionTips":["Only unwatch paths you successfully watched","Rebuild watcher state on service restart after flaky reloads","Check inotify watch limits (fs.inotify.max_user_watches) on Linux","Treat unwatch errors as warnings; verify the new cert loaded instead"],"tags":["fsnotify","file-watching","cert-rotation","tls"],"backgroundTag":"fsnotify-unwatch-failed","analyzedSha":"b41aefbe51c8dde65e2c50f093996afa0502edf9","analyzedAt":"2026-08-27T04:57:55.097Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}