nats-io/nats-server · error

failed to read v2 header: %w

Error message

failed to read v2 header: %w

What it means

This error is returned when, after the 12-byte v2 signature has been validated, io.ReadFull fails to read the remaining 4 fixed header bytes (ver/cmd, fam/proto, addr-len). The underlying I/O error (EOF, connection reset, timeout) is wrapped with %w so callers can inspect it with errors.Is/errors.As. It indicates the peer closed or stalled mid-header, not a semantic protocol violation.

Source

Thrown at server/client_proxyproto.go:275

		// v1 parser expects "PROXY " prefix already consumed
		return readProxyProtoV1Header(conn)
	case 2:
		// Read rest of v2 signature (bytes 6-11, total 6 more bytes)
		remaining := make([]byte, 6)
		if _, err := io.ReadFull(conn, remaining); err != nil {
			return nil, nil, fmt.Errorf("failed to read v2 signature: %w", err)
		}

		// Verify full signature
		fullSig := string(firstBytes) + string(remaining)
		if fullSig != proxyProtoV2Sig {
			return nil, nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
		}

		// Read rest of header: ver/cmd, fam/proto, addr-len (4 bytes)
		header := make([]byte, 4)
		if _, err := io.ReadFull(conn, header); err != nil {
			return nil, nil, fmt.Errorf("failed to read v2 header: %w", err)
		}

		// Continue with parsing
		addr, err := parseProxyProtoV2Header(conn, header)
		return addr, nil, err
	default:
		return nil, nil, fmt.Errorf("unsupported PROXY protocol version: %d", version)
	}
}

// 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 {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Check the wrapped cause with errors.Is(err, os.ErrDeadlineExceeded) or net.Error.Timeout() to distinguish slow clients (timeout) from early disconnects (EOF/reset).
  2. Ensure the upstream proxy writes the complete 16-byte v2 header atomically and does not close the connection before finishing it.
  3. Increase LB/proxy idle timeouts or fix probe tools that open and immediately close connections; probes should send a full LOCAL v2 header or just close cleanly.
  4. If timeouts are frequent from slow clients, review proxyProtoReadTimeout (5s) and network latency between proxy and server.

Example fix

// before: probe sends partial header then closes
// conn.Write([]byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A")); conn.Close()
// after: probe sends full LOCAL v2 header before any close
// hdr := append([]byte("\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"), 0x20, 0x00, 0x00, 0x00)
// conn.Write(hdr); conn.Close()
Defensive patterns

Strategy: type-guard

Type guard

func isTimeout(err error) bool {
	var ne net.Error
	if errors.As(err, &ne) {
		return ne.Timeout()
	}
	return errors.Is(err, os.ErrDeadlineExceeded)
}

Try / catch

addr, extra, err := readProxyProtoHeader(conn)
if err != nil {
	switch {
	case isTimeout(err):
		// slow/malicious client: drop connection
	case errors.Is(err, io.ErrUnexpectedEOF):
		// peer closed mid-header: likely a probe; ignore or rate-limit
	default:
		// unexpected I/O error: alert on persistence
	}
	conn.Close()
}

Prevention

When it happens

Trigger: readProxyProtoHeader detects v2, validates the full 12-byte signature, then calls io.ReadFull(conn, header) for 4 more bytes; the connection returns fewer than 4 bytes (io.ErrUnexpectedEOF), times out against the 5-second proxyProtoReadTimeout deadline, or resets before the header completes.

Common situations: Health-check probes that open a TCP connection, send only the signature (or partial bytes), and immediately close; network interruption between the proxy and the server; an aggressive idle-timeout on the LB killing half-written headers; a slow or malicious client that never completes the header and hits the read deadline.

Related errors


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