{"record":{"id":"868496b7928f888c","repo":"OpenNHP/opennhp","slug":"relay-no-upstream-configured-add-at-least-one","errorCode":null,"errorMessage":"relay: no upstream configured; add at least one [[Servers]] with one [[Servers.Instances]]","messagePattern":"relay: no upstream configured; add at least one \\[\\[Servers\\]\\] with one \\[\\[Servers\\.Instances\\]\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/relay/config.go","lineNumber":209,"sourceCode":"\t\tcfg.Servers = []Server{{\n\t\t\tPubKeyBase64: cfg.NHPServerPublicKeyBase64,\n\t\t\tInstances: []ServerInstance{{\n\t\t\t\tHost: cfg.NHPServerHost,\n\t\t\t\tPort: cfg.NHPServerPort,\n\t\t\t}},\n\t\t}}\n\tcase hasLegacy && len(cfg.Servers) > 0:\n\t\t// Both forms present. The [[Servers]] block wins because it's\n\t\t// strictly more expressive; but a copy-paste upgrade that left\n\t\t// the old fields behind would silently route to whatever the\n\t\t// new block declares and drop the legacy values. Log loudly so\n\t\t// the operator notices and can remove the dead config.\n\t\tlog.Warning(\"[Relay] both legacy nhpServer* fields and [[Servers]] blocks are set; \" +\n\t\t\t\"the legacy fields are ignored — remove them from config.toml to silence this warning\")\n\t}\n\n\tif len(cfg.Servers) == 0 {\n\t\treturn fmt.Errorf(\"relay: no upstream configured; add at least one [[Servers]] with one [[Servers.Instances]]\")\n\t}\n\n\tseenFP := make(map[string]int, len(cfg.Servers))\n\t// seenAddr catches a server+instance pair duplicated under the SAME\n\t// pubkey — the \"operator copied a [[Servers]] block and forgot to\n\t// change the instance\" mistake. The dedupe key is (fingerprint, addr),\n\t// NOT addr alone: resolveTarget routes by PeerPk, so two DISTINCT\n\t// pubkeys legitimately sharing one host:port (a SNI/header-routed\n\t// front-end, or port-multiplexed identities) is a valid topology and\n\t// must not be a hard config-load failure. Only same-pubkey + same-addr\n\t// is the unambiguous copy-paste error.\n\ttype addrOrigin struct {\n\t\tserver   int\n\t\tinstance int\n\t}\n\tseenAddr := make(map[string]addrOrigin)\n\tfor i := range cfg.Servers {\n\t\tc := &cfg.Servers[i]","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/relay/config.go#L191-L227","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (config.toml)\nprivateKeyBase64 = \"...\"\n// after\nprivateKeyBase64 = \"...\"\n[[Servers]]\npubKeyBase64 = \"<server pubkey>\"\n[[Servers.Instances]]\nhost = \"10.0.0.5\"\nport = 10161","handlingStrategy":"validation","validationCode":"func hasUpstream(cfg *relay.Config) bool {\n\tfor _, s := range cfg.Servers {\n\t\tif len(s.Instances) > 0 { return true }\n\t}\n\treturn false\n}","typeGuard":null,"tryCatchPattern":"if err := cfg.Normalize(); err != nil {\n\tif strings.Contains(err.Error(), \"no upstream configured\") {\n\t\treturn errors.New(\"add a [[Servers]] block with [[Servers.Instances]] to relay config\")\n\t}\n\treturn err\n}","preventionTips":["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"],"tags":["relay","config","validation","missing-upstream"],"backgroundTag":"missing-required-config-field","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}