OpenNHP/opennhp · error

empty packet

Error message

empty packet

What it means

handleRelay responds with HTTP 400 'empty packet' when the request body contains zero bytes after reading. An empty payload cannot be a valid inner NHP packet, so the relay rejects it before parsing the header counter or forwarding to a server instance.

Solutions

  1. Attach the serialized inner NHP packet as the request body before sending
  2. Check client-side serialization for silent failures returning empty bytes and handle those errors before POSTing
  3. Ensure Content-Length or chunked encoding actually carries data

Example fix

// before
req, _ := http.NewRequest("POST", relayURL, nil)
// after
if len(packet) == 0 { return errors.New("no packet to send") }
req, _ := http.NewRequest("POST", relayURL, bytes.NewReader(packet))
Defensive patterns

Strategy: validation

Validate before calling

if len(packet) == 0 {
    return errors.New("refusing to send empty NHP packet")
}

Try / catch

if resp.StatusCode == http.StatusBadRequest {
    b, _ := io.ReadAll(resp.Body)
    if strings.Contains(string(b), "empty packet") {
        return fmt.Errorf("client sent empty body: packet builder returned %d bytes", len(packet))
    }
}

Prevention

When it happens

Trigger: POSTing to the relay endpoint with an empty body (Content-Length: 0 or no body at all), as exercised by TestRouting_EmptyBodyReturns400.

Common situations: Client code builds the HTTP request but forgets to attach the serialized NHP packet; a serializer returns nil/empty bytes on error and the error is swallowed; curl/Postman tests without a payload.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at endpoints/relay/relay.go:965

	}

	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
	}
	innerCounter := binary.BigEndian.Uint64(innerPacket[16:24])

	// Extract real client address before picking an instance so sticky

View on GitHub (pinned to 6e04ca5ff0)