juanfont/headscale · error

unmarshalling records, content: %q: %w

Error message

unmarshalling records, content: %q: %w

What it means

The extra-records file was read successfully but its content is not valid JSON for the expected shape: an array of tailcfg.DNSRecord objects. The full offending content is quoted in the error to make diagnosis immediate, alongside the json.Unmarshal error. An empty file is explicitly allowed (skipped), so this only fires on present-but-malformed content.

Source

Thrown at hscontrol/dns/extrarecords.go:206

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, nil
}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Fix the JSON to be an array: [{"name":"example.com","type":5,"value":"1.2.3.4"}].
  2. Validate the file with jq before deploying: jq empty extra-records.json.
  3. If caused by a partial write during reload, ensure writes are atomic (write temp file + rename) and that the final content is complete.
  4. Check field types against tailcfg.DNSRecord (Name string, Type int, Value string, Priority int).

Example fix

// before (extra-records.json)
{"name": "svc.example.com", "value": "10.0.0.5"}

// after
[
  {"name": "svc.example.com", "type": 1, "value": "10.0.0.5"}
]
Defensive patterns

Strategy: validation

Validate before calling

// Validate before deploying / before headscale reloads:
var records []tailcfg.DNSRecord
if b, err := os.ReadFile(path); err == nil && len(b) > 0 {
    if err := json.Unmarshal(b, &records); err != nil {
        return fmt.Errorf("extra-records.json invalid: %w", err)
    }
}

Type guard

func isValidExtraRecords(b []byte) bool {
    if len(b) == 0 {
        return true // empty file is explicitly tolerated
    }
    var rs []tailcfg.DNSRecord
    return json.Unmarshal(b, &rs) == nil
}

Prevention

When it happens

Trigger: Writing a single object instead of an array ({"name":...} vs [{"name":...}]), trailing commas, comments, YAML accidentally used, truncated JSON from a partial write caught by the fsnotify reload, or wrong field names/types inside records (name must be a string, type an integer, etc.).

Common situations: Hand-editing the file and forgetting array brackets; config-management templates emitting YAML into a .json file; editors mid-save triggering a reload of a half-written file (a consecutive reload usually self-heals once the write completes).

Related errors


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