{"record":{"id":"512c3c8f713082e3","repo":"juanfont/headscale","slug":"fatal-error-reading-config-file-w","errorCode":null,"errorMessage":"fatal error reading config file: %w","messagePattern":"fatal error reading config file: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/types/config.go","lineNumber":503,"sourceCode":"\tviper.SetDefault(\"node.routes.ha.probe_interval\", \"10s\")\n\tviper.SetDefault(\"node.routes.ha.probe_timeout\", \"5s\")\n\n\tviper.SetDefault(\"tuning.notifier_send_timeout\", \"800ms\")\n\tviper.SetDefault(\"tuning.batch_change_delay\", \"800ms\")\n\tviper.SetDefault(\"tuning.node_mapsession_buffered_chan_size\", 30)\n\tviper.SetDefault(\"tuning.node_store_batch_size\", defaultNodeStoreBatchSize)\n\tviper.SetDefault(\"tuning.node_store_batch_timeout\", \"500ms\")\n\n\tviper.SetDefault(\"prefixes.allocation\", string(IPAllocationStrategySequential))\n\n\terr := viper.ReadInConfig()\n\tif err != nil {\n\t\tif _, ok := errors.AsType[viper.ConfigFileNotFoundError](err); ok {\n\t\t\tlog.Warn().Msg(\"no config file found, using defaults\")\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"fatal error reading config file: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// resolveEphemeralInactivityTimeout resolves the ephemeral inactivity timeout\n// from config, supporting both the new key (node.ephemeral.inactivity_timeout)\n// and the old key (ephemeral_node_inactivity_timeout) for backwards compatibility.\n//\n// We cannot use viper.RegisterAlias here because aliases silently ignore\n// config values set under the alias name. If a user writes the new key in\n// their config file, RegisterAlias redirects reads to the old key (which\n// has no config value), returning only the default and discarding the\n// user's setting.\nfunc resolveEphemeralInactivityTimeout() time.Duration {\n\t// New key takes precedence if explicitly set in config.\n\tif viper.IsSet(\"node.ephemeral.inactivity_timeout\") &&\n\t\tviper.GetString(\"node.ephemeral.inactivity_timeout\") != \"\" {","sourceCodeStart":485,"sourceCodeEnd":521,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/types/config.go#L485-L521","documentation":"viper.ReadInConfig failed with an error other than ConfigFileNotFoundError (which is tolerated with a warning and defaults). This means the config file was found but could not be read or parsed: malformed YAML/TOML syntax, unreadable file permissions, or an I/O error. Wrapped as a fatal configuration error at startup.","triggerScenarios":"Starting headscale with a config.yaml containing invalid YAML (tabs, unclosed quotes, bad indentation), a TOML file with syntax errors, or a file the process lacks read permission on. Only the not-found case is soft; everything else is fatal.","commonSituations":"Hand-edited config with a stray tab or unquoted special character; config mounted read-only with wrong ownership in Docker; secrets-injection tooling wrote a truncated file; switching config formats (yaml→toml) leaving mixed syntax.","solutions":["Run a YAML/TOML linter on the config file; the wrapped error gives the exact line/column","Compare against config-example.yaml shipped in the repo for structural reference","Check file permissions: the headscale process must be able to read the path given to --config","If a templating system writes the file, verify the rendered output, not the template"],"exampleFix":"# before (invalid: tab indent, unquoted yes-ish token)\nodesc: my tailnet\n\tserver_url: https://hs.example.com\n\n# after\nderp:\n  server:\n    enabled: false\nserver_url: https://hs.example.com","handlingStrategy":"validation","validationCode":"// CI check before deploy:\nimport yaml, sys\ncfg = yaml.safe_load(open(sys.argv[1]))  # raises on syntax error\nprint(\"ok\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Lint config YAML/TOML in CI on every change","Use config-example.yaml as the structural baseline","After secret-templating, validate the rendered file, not the template","Remember: a MISSING config is fine (defaults); a MALFORMED one is fatal"],"tags":["config","yaml","startup","viper","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}