OpenNHP/opennhp · error

failed to read body

Error message

failed to read body

What it means

handleRelay responds with HTTP 400 'failed to read body' when io.ReadAll fails while draining the request body (capped at maxPacketSize+1 via io.LimitReader). This means the underlying body reader returned an error before EOF, so the inner NHP packet could not be obtained. It guards the relay from forwarding corrupt or truncated payloads to an NHP server instance.

Solutions

  1. Ensure the client sends the complete body and keeps the connection open until the relay responds
  2. Check intermediate reverse proxies for body-read/timeout limits (e.g. nginx proxy_read_timeout, client_body_timeout) and raise them
  3. Retry the request from the client; this is typically a transient transport failure
  4. If testing, verify the test harness provides a valid non-failing io.Reader for r.Body

Example fix

// before: client aborts mid-send
req, _ := http.NewRequest("POST", url, brokenReader)
// after: buffer the packet fully client-side so the body read cannot fail
req, _ := http.NewRequest("POST", url, bytes.NewReader(packetBytes))
req.ContentLength = int64(len(packetBytes))
Defensive patterns

Strategy: retry

Validate before calling

// client: send a fully buffered body and keep the connection open
if len(packet) == 0 { return errors.New("empty packet") }
req, _ := http.NewRequest("POST", relayURL, bytes.NewReader(packet))
req.ContentLength = int64(len(packet))

Try / catch

resp, err := http.DefaultClient.Do(req)
if err != nil { return retryWithBackoff(req) }
if resp.StatusCode == 400 { body, _ := io.ReadAll(resp.Body); log.Printf("relay rejected body: %s", body) }

Prevention

When it happens

Trigger: POSTing to the relay endpoint with a request body whose read fails mid-stream: client disconnects before the body finishes transferring, an upstream reverse proxy aborts the connection, or chunked transfer-encoding is cut off before the terminating chunk.

Common situations: Flaky mobile/browser clients dropping the connection mid-upload; proxies (nginx, Cloudflare) timing out and closing the upstream body; tests that close the request body or hand the handler a failing reader.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/3b3669bdd3b844f9. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/relay/relay.go:961

func (rs *RelayServer) handleRelay(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	cr, status, errMsg := rs.resolveServer(r)
	if cr == nil {
		http.Error(w, errMsg, status)
		return
	}

	// Read inner NHP packet from request body. Cap at maxPacketSize+1 so we
	// can reject oversize bodies without pulling an unbounded amount into
	// memory. A single r.Body.Read() is not guaranteed to return the full
	// payload; io.ReadAll drains until EOF.
	innerPacket, err := io.ReadAll(io.LimitReader(r.Body, int64(maxPacketSize)+1))
	if err != nil {
		http.Error(w, "failed to read body", http.StatusBadRequest)
		return
	}
	if len(innerPacket) == 0 {
		http.Error(w, "empty packet", http.StatusBadRequest)
		return
	}
	if len(innerPacket) > maxPacketSize {
		http.Error(w, "packet too large", http.StatusBadRequest)
		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

View on GitHub (pinned to 6e04ca5ff0)