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
- Add at least one [[Servers]] block with publicKeyBase64 and a nested [[Servers.Instances]] with host and port
- Use [[Servers]] (double brackets) — a single [Servers] section does not create the expected array of tables
- 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
- Use [[Servers]] (double brackets) array-of-tables syntax, never [Servers]
- Start from the example config that includes a populated [[Servers]] block
- Add a config lint step that asserts at least one server with instances exists
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
- relay: privateKeyBase64 must be set in config
- relay: server # missing publicKeyBase64
- relay: server # (fingerprint ) has no [[Servers.Instances]]
- relay: server # instance # missing host
- relay: server # instance # missing or invalid port
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)