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
- Retry or recreate the stream; a partial signature write leaves the handshake unusable
- Check whether the stream was closed or reset concurrently during NewSessionClient
- 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
- Do not close or cancel the stream concurrently with NewSessionClient
- Use a writer that guarantees full writes (loop until n == len(b))
- Check for stream reset errors in surrounding QUIC code before session setup
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
- expect to write %d bytes for RPC stream protocol signature,
- unknown protocol %v
- unknown protocol %v
- invalid datagram type expected
- payload length is too large to be bundled in datagram
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/6a7b87ee85767f37.
Report an issue: GitHub.