{"record":{"id":"a7a7c1de6c6831e0","repo":"OpenNHP/opennhp","slug":"relay-internal-error","errorCode":null,"errorMessage":"relay internal error","messagePattern":"relay internal error","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"endpoints/relay/relay.go","lineNumber":1075,"sourceCode":"\t\t\tif len(waiters) == 0 {\n\t\t\t\tdelete(inst.pendingRequests, innerCounter)\n\t\t\t}\n\t\t}\n\t\tinst.pendingMu.Unlock()\n\t}()\n\n\t// Construct RelayForwardMsg (standard JSON body).\n\trlyMsg := &common.RelayForwardMsg{\n\t\tSourceAddr: &common.NetAddress{\n\t\t\tIp:   realAddr.IP.String(),\n\t\t\tPort: realAddr.Port,\n\t\t},\n\t\tInnerPacket: base64.StdEncoding.EncodeToString(innerPacket),\n\t}\n\tmsgBytes, err := json.Marshal(rlyMsg)\n\tif err != nil {\n\t\tlog.Error(\"[Relay] failed to marshal RelayForwardMsg: %v\", err)\n\t\thttp.Error(w, \"relay internal error\", http.StatusBadGateway)\n\t\treturn\n\t}\n\n\t// Send the NHP_RLY envelope to the chosen instance.\n\ttrxId := rs.device.NextCounterIndex()\n\tmd := &core.MsgData{\n\t\tRemoteAddr:    inst.addr,\n\t\tHeaderType:    core.NHP_RLY,\n\t\tCipherScheme:  rs.config.CipherScheme,\n\t\tTransactionId: trxId,\n\t\tMessage:       msgBytes,\n\t\tPeerPk:        cr.pubKey,\n\t}\n\n\tudpTimeout := rs.config.UDPTimeoutMs\n\tif udpTimeout <= 0 {\n\t\tudpTimeout = defaultUDPTimeoutMs\n\t}","sourceCodeStart":1057,"sourceCodeEnd":1093,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/relay/relay.go#L1057-L1093","documentation":"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.","triggerScenarios":"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).","commonSituations":"Post-refactor regressions where a new RelayForwardMsg field is of an unmarshalable type; custom MarshalJSON methods returning errors; not triggered by normal client input.","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"],"exampleFix":"// before: new field of unsupported type breaks Marshal\ntype RelayForwardMsg struct {\n    InnerPacket string\n    OnDone chan struct{} // not JSON-serializable\n}\n// after: keep struct JSON-safe\ntype RelayForwardMsg struct {\n    InnerPacket string\n    Done bool\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if resp.StatusCode == http.StatusBadGateway {\n    b, _ := io.ReadAll(resp.Body)\n    if strings.Contains(string(b), \"relay internal error\") {\n        return fmt.Errorf(\"relay failed to build forward message; report with relay logs (%s)\", relayVersion)\n    }\n}","preventionTips":["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"],"tags":["relay","json","internal"],"backgroundTag":"json-marshal-failed","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"}