OpenNHP/opennhp · error

relay: no upstream configured; add at least one [[Servers]]…

Error message

relay: no upstream configured; add at least one [[Servers]] with one [[Servers.Instances]]

What it means

normalize rejects a relay config that defines no upstream servers: cfg.Servers is empty after legacy-field migration. The relay's only purpose is forwarding to NHP server instances, so at least one [[Servers]] block containing one [[Servers.Instances]] entry is mandatory.

Solutions

  1. Add at least one [[Servers]] block with publicKeyBase64 and a nested [[Servers.Instances]] with host and port
  2. Use [[Servers]] (double brackets) — a single [Servers] section does not create the expected array of tables
  3. If migrating from the legacy single-server fields, convert nhpServerHost/nhpServerPort/nhpServerPublicKeyBase64 into a [[Servers]] + [[Servers.Instances]] block

Example fix

// before (config.toml)
privateKeyBase64 = "..."
// after
privateKeyBase64 = "..."
[[Servers]]
pubKeyBase64 = "<server pubkey>"
[[Servers.Instances]]
host = "10.0.0.5"
port = 10161
Defensive patterns

Strategy: validation

Validate before calling

func hasUpstream(cfg *relay.Config) bool {
	for _, s := range cfg.Servers {
		if len(s.Instances) > 0 { return true }
	}
	return false
}

Try / catch

if err := cfg.Normalize(); err != nil {
	if strings.Contains(err.Error(), "no upstream configured") {
		return errors.New("add a [[Servers]] block with [[Servers.Instances]] to relay config")
	}
	return err
}

Prevention

When it happens

Trigger: normalize (from LoadConfig at startup or direct unit-test invocation) finds len(cfg.Servers) == 0 — the config has no [[Servers]] TOML array-of-tables entries and the legacy nhpServer* fields were absent or could not populate Servers.

Common situations: Operator writes a single [Servers] table (not [[Servers]]) so it does not parse as an array; copying a minimal config that only has privateKeyBase64; legacy nhpServerHost/Port/PublicKey fields removed in a version upgrade without adding [[Servers]] blocks; config template renders an empty servers section.

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/868496b7928f888c. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/relay/config.go:209

		cfg.Servers = []Server{{
			PubKeyBase64: cfg.NHPServerPublicKeyBase64,
			Instances: []ServerInstance{{
				Host: cfg.NHPServerHost,
				Port: cfg.NHPServerPort,
			}},
		}}
	case hasLegacy && len(cfg.Servers) > 0:
		// Both forms present. The [[Servers]] block wins because it's
		// strictly more expressive; but a copy-paste upgrade that left
		// the old fields behind would silently route to whatever the
		// new block declares and drop the legacy values. Log loudly so
		// the operator notices and can remove the dead config.
		log.Warning("[Relay] both legacy nhpServer* fields and [[Servers]] blocks are set; " +
			"the legacy fields are ignored — remove them from config.toml to silence this warning")
	}

	if len(cfg.Servers) == 0 {
		return fmt.Errorf("relay: no upstream configured; add at least one [[Servers]] with one [[Servers.Instances]]")
	}

	seenFP := make(map[string]int, len(cfg.Servers))
	// seenAddr catches a server+instance pair duplicated under the SAME
	// pubkey — the "operator copied a [[Servers]] block and forgot to
	// change the instance" mistake. The dedupe key is (fingerprint, addr),
	// NOT addr alone: resolveTarget routes by PeerPk, so two DISTINCT
	// pubkeys legitimately sharing one host:port (a SNI/header-routed
	// front-end, or port-multiplexed identities) is a valid topology and
	// must not be a hard config-load failure. Only same-pubkey + same-addr
	// is the unambiguous copy-paste error.
	type addrOrigin struct {
		server   int
		instance int
	}
	seenAddr := make(map[string]addrOrigin)
	for i := range cfg.Servers {
		c := &cfg.Servers[i]

View on GitHub (pinned to 6e04ca5ff0)