OpenNHP/opennhp · error
relay internal error
Error message
relay internal error
What it means
handleRelay responds with HTTP 502 'relay internal error' when json.Marshal of the RelayForwardMsg fails. Because the message contains only a base64 string and plain fields, this virtually always indicates an internal invariant violation rather than caller input — but it is surfaced as a 502 since the forward cannot be built.
Solutions
- Inspect the logged error ('failed to marshal RelayForwardMsg') to find the offending field
- Fix the struct so all fields are JSON-serializable (base64/encode binary data into strings)
- Add a unit test marshaling RelayForwardMsg with representative data to catch regressions
- Retry the request only after a relay fix; callers cannot remedy this themselves
Example fix
// before: new field of unsupported type breaks Marshal
type RelayForwardMsg struct {
InnerPacket string
OnDone chan struct{} // not JSON-serializable
}
// after: keep struct JSON-safe
type RelayForwardMsg struct {
InnerPacket string
Done bool
} Defensive patterns
Strategy: try-catch
Try / catch
if resp.StatusCode == http.StatusBadGateway {
b, _ := io.ReadAll(resp.Body)
if strings.Contains(string(b), "relay internal error") {
return fmt.Errorf("relay failed to build forward message; report with relay logs (%s)", relayVersion)
}
} Prevention
- Keep RelayForwardMsg fields JSON-serializable; encode binary as base64 strings
- Add a regression test marshaling RelayForwardMsg with production-shaped data
- Review any struct changes for unsupported types (chan, func, cycles)
- Callers: this is a relay-side bug — capture logs and report rather than retrying
When it happens
Trigger: json.Marshal returning an error for the RelayForwardMsg struct — in practice only if a field added to the struct is unsupported by encoding/json (e.g. a channel, func, or cyclic value introduced by a code change).
Common situations: Post-refactor regressions where a new RelayForwardMsg field is of an unmarshalable type; custom MarshalJSON methods returning errors; not triggered by normal client input.
Understand the failure class
Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.
Related errors
- fail to unmarshal confidential computing result
- failed to unmarshal data private key wrapping
- json parsing error
- relay: failed to parse config
- relay: privateKeyBase64 must be set in config
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/a7a7c1de6c6831e0.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/relay/relay.go:1075
if len(waiters) == 0 {
delete(inst.pendingRequests, innerCounter)
}
}
inst.pendingMu.Unlock()
}()
// Construct RelayForwardMsg (standard JSON body).
rlyMsg := &common.RelayForwardMsg{
SourceAddr: &common.NetAddress{
Ip: realAddr.IP.String(),
Port: realAddr.Port,
},
InnerPacket: base64.StdEncoding.EncodeToString(innerPacket),
}
msgBytes, err := json.Marshal(rlyMsg)
if err != nil {
log.Error("[Relay] failed to marshal RelayForwardMsg: %v", err)
http.Error(w, "relay internal error", http.StatusBadGateway)
return
}
// Send the NHP_RLY envelope to the chosen instance.
trxId := rs.device.NextCounterIndex()
md := &core.MsgData{
RemoteAddr: inst.addr,
HeaderType: core.NHP_RLY,
CipherScheme: rs.config.CipherScheme,
TransactionId: trxId,
Message: msgBytes,
PeerPk: cr.pubKey,
}
udpTimeout := rs.config.UDPTimeoutMs
if udpTimeout <= 0 {
udpTimeout = defaultUDPTimeoutMs
}View on GitHub (pinned to 6e04ca5ff0)