router-for-me/CLIProxyAPI · error
expected root mapping node
Error message
expected root mapping node
What it means
Thrown by the config persistence path when config.yaml's root node is not a mapping. The sanitizer merges sanitized config values into the original document tree to preserve comments and ordering; that merge requires the document root to be a yaml.MappingNode. A sequence, scalar, or alias at the root makes lossless merge impossible.
Source
Thrown at internal/config/config_yaml.go:30
// SaveConfigPreserveComments writes the config back to YAML while preserving existing comments
// and key ordering by loading the original file into a yaml.Node tree and updating values in-place.
func SaveConfigPreserveComments(configFile string, cfg *Config) error {
persistCfg := cfg
// Load original YAML as a node tree to preserve comments and ordering.
data, err := os.ReadFile(configFile)
if err != nil {
return err
}
var original yaml.Node
if err = yaml.Unmarshal(data, &original); err != nil {
return err
}
if original.Kind != yaml.DocumentNode || len(original.Content) == 0 {
return fmt.Errorf("invalid yaml document structure")
}
if original.Content[0] == nil || original.Content[0].Kind != yaml.MappingNode {
return fmt.Errorf("expected root mapping node")
}
// Marshal the current cfg to YAML, then unmarshal to a yaml.Node we can merge from.
rendered, err := yaml.Marshal(persistCfg)
if err != nil {
return err
}
var generated yaml.Node
if err = yaml.Unmarshal(rendered, &generated); err != nil {
return err
}
if generated.Kind != yaml.DocumentNode || len(generated.Content) == 0 || generated.Content[0] == nil {
return fmt.Errorf("invalid generated yaml structure")
}
if generated.Content[0].Kind != yaml.MappingNode {
return fmt.Errorf("expected generated root mapping node")
}
View on GitHub (pinned to 78f0c4079e)
Solutions
- Rewrite config.yaml so the top level is a mapping of key/value pairs (port:, auth-dir:, etc.).
- Move list content under a proper key, e.g. 'models:\n - a\n - b'.
- Start from config.example.yaml which already has a mapping root.
- Confirm you are pointing --config at the right file (it may be an auth JSON list or other data file).
Example fix
# before: config.yaml - port: 8317 - auth-dir: ./auths # after: config.yaml port: 8317 auth-dir: ./auths
Defensive patterns
Strategy: validation
Validate before calling
// Go: assert the config file root is a mapping before persisting.
func configRootIsMapping(path string) (bool, error) {
data, err := os.ReadFile(path)
if err != nil {
return false, err
}
var doc yaml.Node
if err := yaml.Unmarshal(data, &doc); err != nil {
return false, err
}
return doc.Kind == yaml.DocumentNode && len(doc.Content) > 0 &&
doc.Content[0] != nil && doc.Content[0].Kind == yaml.MappingNode, nil
} Prevention
- Keep the top level of config.yaml strictly key: value pairs.
- Put list-shaped data under a named key, never at the document root.
- Double-check --config points at the main config, not a data or auth file.
When it happens
Trigger: config.yaml whose top-level structure is a YAML list ('- a\n- b'), a bare scalar ('8317'), or a null root ('--- ~'), then a management-API config save or other persistence flow runs.
Common situations: Reusing a file that was meant as a data list (e.g. a models list) as the main config; hand-merging configs and accidentally indenting everything under a list item; a misconfigured symlink pointing at an auth-file list instead of config.yaml.
Related errors
- invalid yaml document structure
- decode codex.live-media-relay.allow-private-remote-ips: %w
- decode codex.live-media-relay.disable-private-remote-ips: %w
- failed to parse config file: %w
- parse plugin enabled: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/f368d6654fd9b3c7.
Report an issue: GitHub.