OpenNHP/opennhp · critical

: no [[Servers]] configured

Error message

%s: no [[Servers]] configured

What it means

clusterconfig.Normalize validates a cluster list and rejects an empty slice outright — at least one [[Servers]] entry is mandatory. The label prefix (consumer label or "cluster") identifies which config section failed validation.

Solutions

  1. Add at least one [[Servers]] block with PubKeyBase64 to the config file
  2. Re-run the deploy/config template rendering so [[Servers]] entries are emitted
  3. Check that the TOML is being read from the expected path (an empty/missing file yields zero clusters)
  4. Fix indentation/table syntax so the [[Servers]] entries actually parse as array-of-tables

Example fix

// before (server.toml)
# [[Servers]]
# PubKeyBase64 = "..."
// after
[[Servers]]
Name = "nhp-server-1"
PubKeyBase64 = "<base64 server public key>"
Defensive patterns

Strategy: validation

Validate before calling

var clusters []clusterconfig.ClusterConfig
if err := toml.Unmarshal(data, &clusters); err != nil { return err }
if len(clusters) == 0 {
    return errors.New("config must define at least one [[Servers]] block")
}
err := clusterconfig.Normalize(clusters, opts)

Try / catch

if err := clusterconfig.Normalize(clusters, opts); err != nil {
    return fmt.Errorf("cluster config invalid: %w", err)
}

Prevention

When it happens

Trigger: Calling Normalize with clusters == nil or len(clusters) == 0, typically from normalizeAndExpand/normalizeClusters/updateServerPeers when a TOML file contained no [[Servers]] array entries.

Common situations: config/server.toml with the [[Servers]] table commented out or deleted; a template rendering that dropped all server entries; parsing a file with only top-level keys and no clusters array.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/7e447f665aab8500. Report an issue: GitHub.

Appendix: source

Thrown at nhp/common/clusterconfig/clusterconfig.go:140

// auto-upgrade in-place. After it returns, every cluster has at least
// one Instances entry and a normalised LoadBalance. Returned errors are
// surfaced to the operator at load time; warnings (deprecation,
// recoverable inconsistencies) go through deprecate.
//
// deprecate is a callback so callers control how the warning is
// surfaced — production code passes log.Warning; tests can capture
// invocations for assertions. A nil deprecate is treated as no-op.
func Normalize(clusters []*ClusterConfig, opts Options, deprecate func(string, ...any)) error {
	if deprecate == nil {
		deprecate = func(string, ...any) {}
	}
	label := opts.ConsumerLabel
	if label == "" {
		label = "cluster"
	}

	if len(clusters) == 0 {
		return fmt.Errorf("%s: no [[Servers]] configured", label)
	}
	for i, c := range clusters {
		if c == nil {
			return fmt.Errorf("%s: [[Servers]][%d] is nil", label, i)
		}
		if c.PubKeyBase64 == "" {
			return fmt.Errorf("%s: [[Servers]][%d] missing PubKeyBase64", label, i)
		}
		if opts.RequireName {
			if c.Name == "" {
				return fmt.Errorf("%s: [[Servers]][%d] (%s) missing Name — clusters are referenced from resource.toml by Name",
					label, i, c.PubKeyBase64)
			}
			if len(c.Name) > NameMaxLen {
				return fmt.Errorf("%s: [[Servers]][%d] Name %q exceeds %d chars",
					label, i, c.Name, NameMaxLen)
			}
			if !clusterNameRegex.MatchString(c.Name) {

View on GitHub (pinned to 6e04ca5ff0)