juanfont/headscale · error

setting up extrarecord manager: %w

Error message

setting up extrarecord manager: %w

What it means

dns.NewExtraRecordsManager failed at startup when dns_config.extra_records_path is set (hscontrol/dns/extrarecords.go:34). The manager creates an fsnotify watcher, stats the file (rejecting directories with ErrPathIsDirectory), parses it as a JSON array of DNS records, and adds a watch. Failure means the path does not exist, is a directory, holds invalid JSON or wrong record fields, or the inotify watch could not be registered.

Source

Thrown at hscontrol/app.go:588

	}

	h.state.SetDERPMap(derpMap)

	// Start ephemeral node garbage collector and schedule all nodes
	// that are already in the database and ephemeral. If they are still
	// around between restarts, they will reconnect and the GC will
	// be cancelled.
	go h.ephemeralGC.Start()

	ephmNodes := h.state.ListEphemeralNodes()
	for _, node := range ephmNodes.All() {
		h.ephemeralGC.Schedule(node.ID(), h.cfg.Node.Ephemeral.InactivityTimeout)
	}

	if h.cfg.DNSConfig.ExtraRecordsPath != "" {
		h.extraRecordMan, err = dns.NewExtraRecordsManager(h.cfg.DNSConfig.ExtraRecordsPath)
		if err != nil {
			return fmt.Errorf("setting up extrarecord manager: %w", err)
		}

		h.cfg.SetExtraRecords(h.extraRecordMan.Records())

		go h.extraRecordMan.Run()
		defer h.extraRecordMan.Close()
	}

	// Start all scheduled tasks, e.g. expiring nodes, derp updates and
	// records updates
	scheduleCtx, scheduleCancel := context.WithCancel(context.Background())
	defer scheduleCancel()

	go h.scheduledTasks(scheduleCtx)

	// Prepare group for running listeners
	errorGroup := new(errgroup.Group)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the wrapped stage: 'getting file info' (missing path), 'path is directory', 'reading extra records from path' (JSON/parse), or 'creating/adding watcher' (inotify limits)
  2. Ensure the path is a single regular file containing a JSON array like [{"name":"foo.example.com","type":"A","value":"1.2.3.4"}]
  3. Validate with jq before starting headscale
  4. If inotify-limited, raise fs.inotify.max_user_instances/max_user_watches or unset extra_records_path

Example fix

# before
dns_config:
  extra_records_path: /etc/headscale/extra_records/  # directory -> ErrPathIsDirectory

# after
dns_config:
  extra_records_path: /etc/headscale/extra_records.json
Defensive patterns

Strategy: validation

Validate before calling

if p := cfg.DNSConfig.ExtraRecordsPath; p != "" {
    fi, err := os.Stat(p)
    if err != nil { return fmt.Errorf("extra_records_path missing: %s", p) }
    if fi.IsDir() { return fmt.Errorf("extra_records_path must be a file: %s", p) }
    b, _ := os.ReadFile(p)
    var rs []tailcfg.DNSRecord
    if err := json.Unmarshal(b, &rs); err != nil {
        return fmt.Errorf("extra_records_path is not a JSON array of records: %w", err)
    }
}

Try / catch

if _, err := dns.NewExtraRecordsManager(path); err != nil {
    if errors.Is(err, dns.ErrPathIsDirectory) {
        // point config at the records file itself, not its folder
    }
    // watcher/inotify failures: check fs.inotify limits or drop extra_records_path
}

Prevention

When it happens

Trigger: extra_records_path pointing at a missing file or a directory (explicit ErrPathIsDirectory), a JSON file whose entries do not match the tailcfg.DNSRecord shape (missing/misspelled name/type/value), malformed JSON, or fsnotify/inotify exhaustion (too many watches, ulimit on inotify instances).

Common situations: Ops putting a whole directory of records instead of one file; hand-edited JSON with trailing commas or comments; schema drift after upgrading (record fields renamed); systems with a low fs.inotify.max_user_instances limit where watcher creation fails.

Related errors


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