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

NewSessionClient writes the 6-byte RPC stream protocol signature to the stream as a handshake preamble. This error is returned when io.Writer.Write accepted fewer bytes than the full 6-byte signature (partial write), meaning the handshake preamble was not fully transmitted.

Source

Thrown at tunnelrpc/quic/session_client.go:31

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

// SessionClient calls capnp rpc methods of SessionManager.
type SessionClient struct {
	client         pogs.SessionManager_PogsClient
	transport      rpc.Transport
	requestTimeout time.Duration
}

func NewSessionClient(ctx context.Context, stream io.ReadWriteCloser, requestTimeout time.Duration) (*SessionClient, 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)
	return &SessionClient{
		client:         pogs.NewSessionManager_PogsClient(conn.Bootstrap(ctx), conn),
		transport:      transport,
		requestTimeout: requestTimeout,
	}, nil
}

func (c *SessionClient) 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.SessionManager, metrics.OperationRegisterUdpSession).Inc()
	timer := metrics.NewClientOperationLatencyObserver(metrics.SessionManager, metrics.OperationRegisterUdpSession)
	defer timer.ObserveDuration()

	resp, err := c.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort, closeIdleAfterHint, traceContext)

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Retry or recreate the stream; a partial signature write leaves the handshake unusable
  2. Check whether the stream was closed or reset concurrently during NewSessionClient
  3. Wrap the writer so Write blocks until all bytes are sent (full-write loop) before calling NewSessionClient

Example fix

// before: raw stream may short-write
sessionClient, err := NewSessionClient(ctx, stream, timeout)
// after: ensure full writes first
fw := &fullWriter{w: stream} // Write loops until all bytes are written
sessionClient, err := NewSessionClient(ctx, fw, timeout)
Defensive patterns

Strategy: retry

Try / catch

sc, err := NewSessionClient(ctx, stream, timeout)
if err != nil {
    if strings.Contains(err.Error(), "expect to write") {
        // partial write: recreate stream and retry once
        stream.Close()
        return retryWithNewStream(ctx)
    }
    return err
}

Prevention

When it happens

Trigger: stream.Write(rpcStreamProtocolSignature[:]) returns n < 6 with err == nil — a partial write on the underlying QUIC stream, typically due to the stream being closed/reset concurrently or a short-write-capable writer.

Common situations: Underlying QUIC stream reset while the RPC client is being initialized; using a wrapper writer that performs short writes; network teardown racing session setup.

Related errors


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