juanfont/headscale · error · ErrPathIsDirectory

%w: %s

Error message

%w: %s

What it means

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.

Source

Thrown at hscontrol/dns/extrarecords.go:46

	updateCh chan []tailcfg.DNSRecord
	closeCh  chan struct{}
	hash     [32]byte
}

// NewExtraRecordsManager creates a new [ExtraRecordsMan] and starts watching the file at the given path.
func NewExtraRecordsManager(path string) (*ExtraRecordsMan, error) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, fmt.Errorf("creating watcher: %w", err)
	}

	fi, err := os.Stat(path)
	if err != nil {
		return nil, fmt.Errorf("getting file info: %w", err)
	}

	if fi.IsDir() {
		return nil, fmt.Errorf("%w: %s", ErrPathIsDirectory, path)
	}

	records, hash, err := readExtraRecordsFromPath(path)
	if err != nil {
		return nil, fmt.Errorf("reading extra records from path: %w", err)
	}

	er := &ExtraRecordsMan{
		watcher:  watcher,
		path:     path,
		records:  set.SetOf(records),
		hash:     hash,
		closeCh:  make(chan struct{}),
		updateCh: make(chan []tailcfg.DNSRecord),
	}

	err = watcher.Add(path)
	if err != nil {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Point dns.extra_records_path at a JSON file, e.g. /etc/headscale/extra-records.json containing an array of records.
  2. Create the file with '[]' if you have no extra records yet.
  3. Remove any trailing slash or directory path from the setting.

Example fix

// config before
dns:
  extra_records_path: /etc/headscale/records/   # directory -> error

// config after
dns:
  extra_records_path: /etc/headscale/records.json # file with []tailcfg.DNSRecord
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(path)
if err != nil {
    return err
}
if fi.IsDir() {
    return fmt.Errorf("extra records path %q must be a file, not a directory", path)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/eeb23eca5a6c096a. Report an issue: GitHub.