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
- Retry establishing the connection; transient stream resets clear on reconnect.
- Wrap the stream with a full-write helper (io.WriteFull-style) or use the standard QUIC stream implementation.
- 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
- Use QUIC stream implementations that complete full writes
- Retry connection establishment on transient handshake failures
- Verify both sides use identical protocol signature constants
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
- 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/fcf31db54e997058.
Report an issue: GitHub.