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
- Attach the serialized inner NHP packet as the request body before sending
- Check client-side serialization for silent failures returning empty bytes and handle those errors before POSTing
- 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
- Assert packet non-empty before every POST
- Handle serializer errors instead of swallowing them (empty bytes often mean a failed build)
- Add a client unit test that the built packet has length > 0
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
- relay: privateKeyBase64 must be set in config
- relay: no upstream configured; add at least one [[Servers]]…
- relay: server # missing publicKeyBase64
- relay: server # (fingerprint ) has no [[Servers.Instances]]
- relay: server # instance # missing host
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 stickyView on GitHub (pinned to 6e04ca5ff0)