nats-io/nats-server · error
failed to read PROXY protocol header: %w
Error message
failed to read PROXY protocol header: %w
What it means
In the backward-compatible readProxyProtoV2Header helper, io.ReadFull fails while reading the 16-byte fixed v2 header. Read timeouts (net.Error with Timeout() true) are mapped to the dedicated sentinel errProxyProtoTimeout; all other read failures (EOF, unexpected EOF, connection reset) are wrapped with this message via %w. It signals an incomplete v2 header on the wire, not a semantic parse failure.
Source
Thrown at server/client_proxyproto.go:304
// readProxyProtoV2Header is kept for backward compatibility and direct testing.
// It reads and parses a PROXY protocol v2 header from the connection.
// If the command is LOCAL (health check), it returns nil for addr and no error.
// If the command is PROXY, it returns the parsed address information.
// The connection must be fresh (no data read yet).
func readProxyProtoV2Header(conn net.Conn) (*proxyProtoAddr, error) {
// Set read deadline to prevent hanging on slow/malicious clients
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
return nil, err
}
defer conn.SetReadDeadline(time.Time{})
// Read fixed header (16 bytes)
header := make([]byte, proxyProtoV2HeaderSize)
if _, err := io.ReadFull(conn, header); err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return nil, errProxyProtoTimeout
}
return nil, fmt.Errorf("failed to read PROXY protocol header: %w", err)
}
// Validate signature (first 12 bytes)
if string(header[:12]) != proxyProtoV2Sig {
return nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
}
// Continue with parsing after signature
return parseProxyProtoV2Header(conn, header[12:16])
}
// parseProxyProtoV2Header parses v2 protocol after signature has been validated.
// header contains the 4 bytes: ver/cmd, fam/proto, addr-len (2 bytes).
func parseProxyProtoV2Header(conn net.Conn, header []byte) (*proxyProtoAddr, error) {
// Parse version and command
verCmd := header[0]
version := verCmd & proxyProtoV2VerMask
command := verCmd & proxyProtoCmdMaskView on GitHub (pinned to 3a66a489d2)
Solutions
- Inspect the wrapped error: errors.Is(err, io.ErrUnexpectedEOF) means the peer closed early; check whether the sender actually emits the full 16-byte v2 header.
- Match sender and receiver configuration: the peer must be set to send-proxy-v2 (binary); a v1 sender on a v2-only path will fail header parsing.
- For probe/monitoring clients, send a complete LOCAL command v2 header (16 bytes, ver/cmd 0x20, fam/proto 0x00, len 0x0000) or close without writing.
- If the error persists under load, check for LB idle timeouts or MTU/proxy truncation between the proxy tier and the server.
Example fix
// before: v1-format sender against v2 parser
// conn.Write([]byte("PROXY TCP4 1.2.3.4 5.6.7.8 1000 2000\r\n"))
// after: send a binary v2 header
// sig := []byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A")
// conn.Write(append(sig, 0x20, 0x11, 0x00, 0x0C, /* 12 bytes IPv4 addr data */...)) Defensive patterns
Strategy: type-guard
Validate before calling
// Before treating a conn as PROXY v2, ensure you can expect at least 16 bytes:
// there is no pre-call validation for a stream, but you can pre-classify errors:
// readProxyProtoV2Header already maps timeouts to errProxyProtoTimeout.
func expectFullV2Header(senderConfiguredForV2 bool) error {
if !senderConfiguredForV2 {
return errors.New("upstream not configured for PROXY v2; header read will fail")
}
return nil
} Type guard
func classifyV2HeaderReadErr(err error) string {
if errors.Is(err, errProxyProtoTimeout) {
return "timeout"
}
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
return "peer-closed-early"
}
var ne net.Error
if errors.As(err, &ne) {
return "net-error"
}
return "other"
} Try / catch
addr, err := readProxyProtoV2Header(conn)
if err != nil {
if errors.Is(err, errProxyProtoTimeout) {
// slow client: drop silently
} else if errors.Is(err, io.ErrUnexpectedEOF) {
// probe or misconfigured sender: count metric, close
}
conn.Close()
return
} Prevention
- Verify the upstream proxy emits the full 16-byte binary v2 header atomically.
- Use readProxyProtoHeader (version-detecting) unless v2-only input is guaranteed.
- Keep probe tooling sending full LOCAL headers; empty open/close probes will surface as this error.
- Alert on error-rate changes: EOF-class spikes mean sender config drift; timeout spikes mean slow clients.
When it happens
Trigger: readProxyProtoV2Header calls io.ReadFull(conn, header) with proxyProtoV2HeaderSize (16) bytes; the peer sends fewer than 16 bytes before closing or resetting (io.ErrUnexpectedEOF, ECONNRESET), so the wrap at line 304 fires. Direct tests (TestClientProxyProtoV2Parse*) drive this via a net.Pipe-style conn.
Common situations: Health probes that open a connection and close without sending a full header; clients speaking PROXY v1 text or no PROXY protocol at all on a port expecting v2 (their short/odd payloads fail the 16-byte read); flaky network links dropping the connection mid-header; load balancers with short connection idle timeouts.
Related errors
- failed to read v2 header: %w
- failed to read v2 signature: %w
- failed to discard LOCAL command address data: %w
- failed to discard UNSPEC address address data: %w
- failed to read IPv4 address data: %w
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/e1331d424e2fa393.
Report an issue: GitHub.