cloudflare/cloudflared · error

expect to write %d bytes for RPC stream protocol signature,

Error message

expect to write %d bytes for RPC stream protocol signature, wrote %d

What it means

NewCloudflaredClient writes the RPC stream protocol signature bytes to the QUIC stream as a handshake. If the write returns fewer bytes than the 6-byte signature (without an error), the stream is in an inconsistent state and the client aborts with this error.

Source

Thrown at tunnelrpc/quic/cloudflared_client.go:32

	"github.com/cloudflare/cloudflared/tunnelrpc"
	"github.com/cloudflare/cloudflared/tunnelrpc/metrics"
	"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)

// CloudflaredClient calls capnp rpc methods of SessionManager and ConfigurationManager.
type CloudflaredClient struct {
	client         pogs.CloudflaredServer_PogsClient
	transport      rpc.Transport
	requestTimeout time.Duration
}

func NewCloudflaredClient(ctx context.Context, stream io.ReadWriteCloser, requestTimeout time.Duration) (*CloudflaredClient, error) {
	n, err := stream.Write(rpcStreamProtocolSignature[:])
	if err != nil {
		return nil, err
	}
	if n != len(rpcStreamProtocolSignature) {
		return nil, fmt.Errorf("expect to write %d bytes for RPC stream protocol signature, wrote %d", len(rpcStreamProtocolSignature), n)
	}
	transport := tunnelrpc.SafeTransport(stream)
	conn := tunnelrpc.NewClientConn(transport)
	client := pogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn)
	return &CloudflaredClient{
		client:         client,
		transport:      transport,
		requestTimeout: requestTimeout,
	}, nil
}

func (c *CloudflaredClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfterHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) {
	ctx, cancel := context.WithTimeout(ctx, c.requestTimeout)
	defer cancel()
	defer metrics.CapnpMetrics.ClientOperations.WithLabelValues(metrics.Cloudflared, metrics.OperationRegisterUdpSession).Inc()
	timer := metrics.NewClientOperationLatencyObserver(metrics.Cloudflared, metrics.OperationRegisterUdpSession)
	defer timer.ObserveDuration()

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Retry establishing the connection; transient stream resets clear on reconnect.
  2. Wrap the stream with a full-write helper (io.WriteFull-style) or use the standard QUIC stream implementation.
  3. Check for peer disconnects/version mismatch between client and server signatures.

Example fix

// before
n, err := stream.Write(rpcStreamProtocolSignature[:])
// after
n, err := io.WriteString(stream, string(rpcStreamProtocolSignature[:])) // or loop until all bytes written
if n != len(rpcStreamProtocolSignature) { return nil, fmt.Errorf(...) }
Defensive patterns

Strategy: retry

Try / catch

client, err := tunnelrpc.NewCloudflaredClient(ctx, stream, timeout)
if err != nil {
    if strings.Contains(err.Error(), "expect to write") {
        // stream handshake failed; reconnect and retry once
        stream, err = dial(ctx)
        client, err = tunnelrpc.NewCloudflaredClient(ctx, stream, timeout)
    }
}

Prevention

When it happens

Trigger: Calling NewCloudflaredClient where stream.Write(rpcStreamProtocolSignature[:]) returns n < 6 with err == nil — a short write on the underlying stream.

Common situations: Stream closed or reset mid-handshake by the peer; transport congestion/cancellation trimming the write; custom/broken io.ReadWriteCloser implementations that don't guarantee full writes.

Related errors


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/fcf31db54e997058. Report an issue: GitHub.