juanfont/headscale · error
reading path: %s, err: %w
Error message
reading path: %s, err: %w
What it means
os.ReadFile failed while loading (or hot-reloading) the extra-records JSON file; the path and the underlying OS error are both included. This fires from both NewExtraRecordsManager (startup) and the fsnotify reload loop (runtime), since both call readExtraRecordsFromPath.
Source
Thrown at hscontrol/dns/extrarecords.go:193
// Release the lock before the (potentially blocking) send so a slow or
// absent consumer cannot stall Records() readers, and abort the send on
// shutdown instead of leaking this goroutine on the closed-down channel.
e.mu.Unlock()
select {
case e.updateCh <- toSend:
case <-e.closeCh:
}
}
// readExtraRecordsFromPath reads a JSON file of [tailcfg.DNSRecord]
// and returns the records and the hash of the file.
func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error) {
var zero [32]byte
b, err := os.ReadFile(path)
if err != nil {
return nil, zero, fmt.Errorf("reading path: %s, err: %w", path, err)
}
// If the read was triggered too fast, and the file is not complete, ignore the update
// if the file is empty. A consecutive update will be triggered when the file is complete.
if len(b) == 0 {
return nil, zero, nil
}
var records []tailcfg.DNSRecord
err = json.Unmarshal(b, &records)
if err != nil {
return nil, zero, fmt.Errorf("unmarshalling records, content: %q: %w", string(b), err)
}
hash := sha256.Sum256(b)
return records, hash, nilView on GitHub (pinned to 565fd254d0)
Solutions
- Check the OS error in the chain (ENOENT, EACCES) — ENOENT during reload usually means the writer uses rename; ensure the final path exists after each write.
- Restore correct ownership/permissions so the headscale process can read the file.
- At startup, verify the path exists and is a regular file before starting headscale.
- For reload failures, the manager keeps the last good records; fix the file and touch it to trigger another reload.
Defensive patterns
Strategy: retry
Validate before calling
// Before starting headscale:
fi, err := os.Stat(cfg.ExtraRecordsPath)
if err != nil || !fi.Mode().IsRegular() {
return fmt.Errorf("extra records file missing or not regular: %w", err)
} Try / catch
records, hash, err := readExtraRecordsFromPath(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// transient during atomic-rename reloads: keep previous records,
// wait for the next fsnotify event instead of failing
return
}
return err
} Prevention
- Write the file atomically (temp file + rename) so reloads never see a missing path.
- Ensure the headscale process has read permission on the file and its directory.
- Keep the previous records on reload failure — the manager does this; don't clear state on a transient read error.
When it happens
Trigger: The file was deleted or renamed between the fsnotify event and the read (classic atomic-rename race: editors write a temp file and rename over the target), permissions lost, or the path became unreadable at runtime. Note: an empty file is tolerated (treated as no update), so this error means the read itself failed, not emptiness.
Common situations: Config management (ansible/sed -i) replacing the file via rename while headscale watches it; file removed by cleanup jobs; permission changes; NFS mounts dropping.
Related errors
- setting up extrarecord manager: %w
- %w: %s
- node name is not unique
- error resolving host
- hostname contains invalid IP address
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/f3bc2f719279532d.
Report an issue: GitHub.