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

  1. Rewrite config.yaml so the top level is a mapping of key/value pairs (port:, auth-dir:, etc.).
  2. Move list content under a proper key, e.g. 'models:\n - a\n - b'.
  3. Start from config.example.yaml which already has a mapping root.
  4. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/f368d6654fd9b3c7. Report an issue: GitHub.