OpenNHP/opennhp · error

relay misconfigured: missing X-Real-IP header from local…

Error message

relay misconfigured: missing X-Real-IP header from local reverse proxy

What it means

handleRelay responds with HTTP 502 'relay misconfigured: missing X-Real-IP header from local reverse proxy' when realClientAddr(r) fails because the request carries no X-Real-IP header. The relay is designed to sit behind a local reverse proxy that sets this header, and the real client address is required for sticky-session hashing; without it the request cannot be routed correctly.

Solutions

  1. Configure the reverse proxy to set X-Real-IP: nginx: proxy_set_header X-Real-IP $remote_addr;
  2. Route client traffic through the reverse proxy instead of hitting the relay port directly
  3. Check the proxy config for header-stripping (e.g. proxy_set_header X-Real-IP "") and remove it
  4. If a CDN/LB is in front, ensure it passes the client IP in X-Forwarded-For and the proxy derives X-Real-IP from it

Example fix

# before: nginx location missing header
location /relay {
    proxy_pass http://127.0.0.1:PORT;
}
# after
location /relay {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_pass http://127.0.0.1:PORT;
}
Defensive patterns

Strategy: fallback

Validate before calling

// ensure the reverse proxy sets the header before traffic flows:
// nginx: proxy_set_header X-Real-IP $remote_addr;
// smoke test through the proxy and inspect relay response for 502

Try / catch

if resp.StatusCode == http.StatusBadGateway {
    b, _ := io.ReadAll(resp.Body)
    if strings.Contains(string(b), "X-Real-IP") {
        return errors.New("relay not reached through reverse proxy; fix proxy X-Real-IP config")
    }
}

Prevention

When it happens

Trigger: Sending requests directly to the relay port, bypassing the reverse proxy; a proxy (nginx, Caddy, traefik) that does not set X-Real-IP / X-Forwarded-For; proxy config that strips or fails to forward the header.

Common situations: Local development hitting the relay directly (curl localhost) instead of through nginx; newly deployed proxy without proxy_set_header X-Real-IP $remote_addr; load balancer terminating TLS and not passing client IP headers.

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/5dc7d452ba9ded3b. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/relay/relay.go:988

		return
	}
	n := len(innerPacket)

	// Extract the counter from the inner packet header (bytes [16:24], big-endian uint64).
	// The NHP server echoes this counter in its ACK/COK response, so we use it
	// to match the response back to this HTTP request.
	if n < 24 {
		http.Error(w, "inner packet too short", http.StatusBadRequest)
		return
	}
	innerCounter := binary.BigEndian.Uint64(innerPacket[16:24])

	// Extract real client address before picking an instance so sticky
	// sessions can hash on it.
	realAddr, err := realClientAddr(r)
	if err != nil {
		log.Error("[Relay] %v", err)
		http.Error(w, "relay misconfigured: missing X-Real-IP header from local reverse proxy", http.StatusBadGateway)
		return
	}
	realAddrKey := realAddr.String()

	// Pick a target instance. When StickyInstance is enabled,
	// hash the real client IP so the same client always reaches the same
	// instance — required for stateful flows like OTP→REG where per-
	// instance local state (SQLite) must be consistent across requests.
	// When disabled (default), each request is load-balanced independently.
	var inst *serverInstance
	if cr.sticky && len(cr.instances) > 1 {
		var ok bool
		inst, ok = cr.picker.PickByKey(realAddrKey)
		if !ok {
			http.Error(w, "server has no usable instance", http.StatusServiceUnavailable)
			return
		}
	} else {

View on GitHub (pinned to 6e04ca5ff0)