juanfont/headscale · error
unmarshalling dns extra records: %w
Error message
unmarshalling dns extra records: %w
What it means
viper.UnmarshalKey("dns.extra_records", &[]tailcfg.DNSRecord) failed while loading DNS config: the configured value's shape does not match a list of DNS records (name + type + value). Emitted from loadDNSConfig, so the whole DNS config load fails at startup or reload.
Source
Thrown at hscontrol/types/config.go:925
// err := viper.UnmarshalKey("dns", &dns)
// if err != nil {
// return DNSConfig{}, fmt.Errorf("unmarshalling dns config: %w", err)
// }
dns.MagicDNS = viper.GetBool("dns.magic_dns")
dns.BaseDomain = viper.GetString("dns.base_domain")
dns.OverrideLocalDNS = viper.GetBool("dns.override_local_dns")
dns.Nameservers.Global = viper.GetStringSlice("dns.nameservers.global")
dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split")
dns.SearchDomains = viper.GetStringSlice("dns.search_domains")
dns.ExtraRecordsPath = viper.GetString("dns.extra_records_path")
if viper.IsSet("dns.extra_records") {
var extraRecords []tailcfg.DNSRecord
err := viper.UnmarshalKey("dns.extra_records", &extraRecords)
if err != nil {
return DNSConfig{}, fmt.Errorf("unmarshalling dns extra records: %w", err)
}
dns.ExtraRecords = extraRecords
}
return dns, nil
}
// parseResolvers converts nameserver strings into DNS resolvers.
// If a nameserver is a valid IP, it will be used as a regular resolver.
// If a nameserver is a valid URL, it will be used as a DoH resolver.
// If a nameserver is neither a valid URL nor a valid IP, it will be ignored.
// When domain is non-empty, it is included in the warning for invalid entries.
func parseResolvers(nameservers []string, domain string) []*dnstype.Resolver {
var resolvers []*dnstype.Resolver
for _, nsStr := range nameservers {
if _, err := netip.ParseAddr(nsStr); err == nil { //nolint:noinlineerrView on GitHub (pinned to 565fd254d0)
Solutions
- Use the documented list-of-mappings form with name/type/value keys (see the extra_records example in config-example.yaml)
- Validate the YAML structure with yamllint before restart
- Ensure type is a valid DNS record type string like A, AAAA, or TXT
Example fix
# before
dns:
extra_records:
example.com: { type: A, value: 1.2.3.4 }
# after
dns:
extra_records:
- name: example.com
type: A
value: 1.2.3.4 Defensive patterns
Strategy: validation
Validate before calling
// Structural pre-check mirroring the unmarshal target:
recs := viper.Get("dns.extra_records")
if recs != nil {
if _, ok := recs.([]any); !ok {
return errors.New("dns.extra_records must be a list of {name,type,value} mappings")
}
} Type guard
func isValidExtraRecords(v any) bool {
list, ok := v.([]any)
if !ok { return false }
for _, r := range list {
m, ok := r.(map[string]any)
if !ok { return false }
if _, ok := m["name"]; !ok { return false }
if _, ok := m["value"]; !ok { return false }
}
return true
} Prevention
- Write extra_records as a YAML list of mappings with name/type/value keys
- Quote record values that look like numbers or contain colons
- Validate rendered config in CI with a YAML schema check
When it happens
Trigger: dns.extra_records set to a mapping instead of a list ({...} vs [...]), records missing required fields, or values of the wrong type (e.g. value: 3600 where a string is expected). Only fires when viper.IsSet sees the key.
Common situations: Converting older headscale configs where extra_records used a different schema; YAML indentation making a record a nested map; copy-pasting a record from docs with the wrong field names (data vs value).
Related errors
- renaming node: %w
- %w: %s
- unmarshalling records, content: %q: %w
- errOIDCIssuerInvalid
- fatal error reading config file: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/dd8da8a74900a4f7.
Report an issue: GitHub.