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 & proxyProtoCmdMask

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/e1331d424e2fa393. Report an issue: GitHub.