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
- Fix the JSON to be an array: [{"name":"example.com","type":5,"value":"1.2.3.4"}].
- Validate the file with jq before deploying: jq empty extra-records.json.
- If caused by a partial write during reload, ensure writes are atomic (write temp file + rename) and that the final content is complete.
- 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
- Run 'jq empty extra-records.json' in CI/config checks.
- Write atomically (temp + rename) so fsnotify reloads never see half-written JSON.
- Use the array-of-objects shape with tailcfg.DNSRecord field names and types.
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
- hostname contains invalid IP address
- %w: %s
- unmarshalling dns extra records: %w
- path cannot be empty
- busy_timeout must be >= 0
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/afc1a215dc212cd8.
Report an issue: GitHub.