router-for-me/CLIProxyAPI · error

invalid yaml document structure

Error message

invalid yaml document structure

What it means

Thrown by the config persistence path (config_yaml.go) when the on-disk config.yaml parses as YAML but is not a well-formed single YAML document with content. yaml.Unmarshal succeeded, but the resulting root node is not a DocumentNode or has zero Content nodes (e.g. a completely empty file or a null document). The sanitizer needs a document tree to merge sanitized values back while preserving comments.

Source

Thrown at internal/config/config_yaml.go:27

	"gopkg.in/yaml.v3"
)

// 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 {

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Put at least one real mapping entry into config.yaml (e.g. 'port: 8317') or restore it from config.example.yaml.
  2. If the file is empty by accident, copy config.example.yaml to config.yaml and re-edit.
  3. Check the file was not truncated by a previous crash or a half-completed atomic write.
  4. Verify with 'cat config.yaml' that the file contains a top-level mapping, not just comments or '---'.

Example fix

# before: config.yaml is empty or only comments
# (file exists, zero mappings)

# after
port: 8317
auth-dir: ./auths
Defensive patterns

Strategy: validation

Validate before calling

#!/bin/sh
# config.yaml must contain at least one top-level mapping entry.
if [ ! -s config.yaml ] || ! grep -qE '^[A-Za-z0-9_-]+:' config.yaml; then
  echo "config.yaml is empty or lacks a top-level mapping" >&2
  exit 1
fi

Try / catch

if err := persistConfig(cfg, "config.yaml"); err != nil {
    if strings.Contains(err.Error(), "invalid yaml document structure") {
        log.Errorf("config.yaml is empty or has no document content; restore it from config.example.yaml")
    }
    return err
}

Prevention

When it happens

Trigger: config.yaml exists but is empty (0 bytes), contains only comments/whitespace, contains only '---' or 'null', or has multiple documents where unmarshal takes a degenerate root. Triggered when the management API or config save flow persists a sanitized config.

Common situations: Fresh deployment where config.yaml was created empty by a container volume mount or a truncated write; a crashed previous save leaving a stub file; a config template that is all comments; kubernetes ConfigMount of an empty file over config.yaml.

Related errors


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