{"record":{"id":"afc1a215dc212cd8","repo":"juanfont/headscale","slug":"unmarshalling-records-content-q-w","errorCode":null,"errorMessage":"unmarshalling records, content: %q: %w","messagePattern":"unmarshalling records, content: %q: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/dns/extrarecords.go","lineNumber":206,"sourceCode":"func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error) {\n\tvar zero [32]byte\n\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, zero, fmt.Errorf(\"reading path: %s, err: %w\", path, err)\n\t}\n\n\t// If the read was triggered too fast, and the file is not complete, ignore the update\n\t// if the file is empty. A consecutive update will be triggered when the file is complete.\n\tif len(b) == 0 {\n\t\treturn nil, zero, nil\n\t}\n\n\tvar records []tailcfg.DNSRecord\n\n\terr = json.Unmarshal(b, &records)\n\tif err != nil {\n\t\treturn nil, zero, fmt.Errorf(\"unmarshalling records, content: %q: %w\", string(b), err)\n\t}\n\n\thash := sha256.Sum256(b)\n\n\treturn records, hash, nil\n}\n","sourceCodeStart":188,"sourceCodeEnd":213,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/dns/extrarecords.go#L188-L213","documentation":"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.","triggerScenarios":"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.).","commonSituations":"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).","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)."],"exampleFix":"// before (extra-records.json)\n{\"name\": \"svc.example.com\", \"value\": \"10.0.0.5\"}\n\n// after\n[\n  {\"name\": \"svc.example.com\", \"type\": 1, \"value\": \"10.0.0.5\"}\n]","handlingStrategy":"validation","validationCode":"// Validate before deploying / before headscale reloads:\nvar records []tailcfg.DNSRecord\nif b, err := os.ReadFile(path); err == nil && len(b) > 0 {\n    if err := json.Unmarshal(b, &records); err != nil {\n        return fmt.Errorf(\"extra-records.json invalid: %w\", err)\n    }\n}","typeGuard":"func isValidExtraRecords(b []byte) bool {\n    if len(b) == 0 {\n        return true // empty file is explicitly tolerated\n    }\n    var rs []tailcfg.DNSRecord\n    return json.Unmarshal(b, &rs) == nil\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["dns","json","config","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}