{"record":{"id":"eeb23eca5a6c096a","repo":"juanfont/headscale","slug":"w-s-eeb23e","errorCode":null,"errorMessage":"%w: %s","messagePattern":"%w: %s","errorType":"validation","errorClass":"ErrPathIsDirectory","httpStatus":null,"severity":"error","filePath":"hscontrol/dns/extrarecords.go","lineNumber":46,"sourceCode":"\tupdateCh chan []tailcfg.DNSRecord\n\tcloseCh  chan struct{}\n\thash     [32]byte\n}\n\n// NewExtraRecordsManager creates a new [ExtraRecordsMan] and starts watching the file at the given path.\nfunc NewExtraRecordsManager(path string) (*ExtraRecordsMan, error) {\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating watcher: %w\", err)\n\t}\n\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting file info: %w\", err)\n\t}\n\n\tif fi.IsDir() {\n\t\treturn nil, fmt.Errorf(\"%w: %s\", ErrPathIsDirectory, path)\n\t}\n\n\trecords, hash, err := readExtraRecordsFromPath(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading extra records from path: %w\", err)\n\t}\n\n\ter := &ExtraRecordsMan{\n\t\twatcher:  watcher,\n\t\tpath:     path,\n\t\trecords:  set.SetOf(records),\n\t\thash:     hash,\n\t\tcloseCh:  make(chan struct{}),\n\t\tupdateCh: make(chan []tailcfg.DNSRecord),\n\t}\n\n\terr = watcher.Add(path)\n\tif err != nil {","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/dns/extrarecords.go#L28-L64","documentation":"NewExtraRecordsManager refuses the configured extra-records path because os.Stat reports it is a directory, not a file. Extra records must be a JSON file of tailcfg.DNSRecord entries; the manager reads it and sets up an fsnotify watcher on a file, so a directory is a configuration error. The sentinel ErrPathIsDirectory is wrapped with the path.","triggerScenarios":"Setting dns.extra_records_path in headscale's config to a directory (e.g. '/etc/headscale/' instead of '/etc/headscale/extra-records.json'), or to a path whose parent was created but the file itself was never added.","commonSituations":"Config typo with a trailing slash or missing filename; users pointing at a directory of record files expecting them to be merged; the file not yet created at first startup.","solutions":["Point dns.extra_records_path at a JSON file, e.g. /etc/headscale/extra-records.json containing an array of records.","Create the file with '[]' if you have no extra records yet.","Remove any trailing slash or directory path from the setting."],"exampleFix":"// config before\ndns:\n  extra_records_path: /etc/headscale/records/   # directory -> error\n\n// config after\ndns:\n  extra_records_path: /etc/headscale/records.json # file with []tailcfg.DNSRecord","handlingStrategy":"validation","validationCode":"fi, err := os.Stat(path)\nif err != nil {\n    return err\n}\nif fi.IsDir() {\n    return fmt.Errorf(\"extra records path %q must be a file, not a directory\", path)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Point dns.extra_records_path at a concrete .json filename.","Create the file ('[]' if empty) before first startup.","Lint configs to reject directory paths or trailing slashes for file settings."],"tags":["dns","config","filesystem","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}