Skip to content

Client-side streaming

Hub › gRPC › Advanced › Client-side streaming

Goal

Build a client-streaming RPC that uploads a file in chunks (client sends multiple messages, server sends one response).

Prerequisites

The proto

Add to chat.proto:

protobuf
service ChatService {
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
  rpc Upload(stream FileChunk) returns (UploadStatus);  // new
}

message FileChunk {
  string filename = 1;
  bytes content = 2;
}

message UploadStatus {
  int64 size_bytes = 1;
  string message = 2;
}

Server handler

typescript
Upload: (call: grpc.ServerReadableStream<FileChunk>, callback: grpc.sendUnaryData<UploadStatus>) => {
  let totalBytes = 0;
  let fileName = '';

  call.on('data', (chunk: FileChunk) => {
    fileName = chunk.filename;
    totalBytes += chunk.content.length;
    console.log(`Received ${chunk.content.length} bytes for ${fileName}`);
  });

  call.on('end', () => {
    console.log(`Upload complete: ${fileName}, ${totalBytes} bytes`);
    callback(null, {
      sizeBytes: totalBytes,
      message: `Uploaded ${fileName} (${totalBytes} bytes)`,
    });
  });

  call.on('error', (err) => {
    callback({
      code: grpc.status.INTERNAL,
      details: `Upload failed: ${err.message}`,
    });
  });
}

Client

typescript
import * as fs from 'fs';

const call = client.Upload((error, response: UploadStatus) => {
  if (error) {
    console.error(`Upload failed: ${error.details}`);
  } else {
    console.log(`Server: ${response.message}`);
  }
});

const FILE = 'example.txt';
const CHUNK_SIZE = 64 * 1024; // 64KB
const fd = fs.openSync(FILE, 'r');
const buffer = Buffer.alloc(CHUNK_SIZE);
let offset = 0;
let bytesRead: number;

while ((bytesRead = fs.readSync(fd, buffer, 0, CHUNK_SIZE, offset)) > 0) {
  call.write({
    filename: FILE,
    content: buffer.subarray(0, bytesRead),
  });
  offset += bytesRead;
}
fs.closeSync(fd);
call.end();

Flow control

The client respects server-side flow control automatically via HTTP/2 flow control windows. To manually pause:

typescript
if (call.writableEnded) return; // stop sending

// Check write queue
call.write(chunk, (err) => {
  if (err) console.error('Write error:', err);
});

Checkpoint

bash
echo "Hello World from gRPC upload" > example.txt
npx ts-node server.ts &
npx ts-node client-upload.ts
# → "Upload complete: example.txt, 32 bytes"
# → "Server: Uploaded example.txt (32 bytes)"

Next: gRPC-web — call gRPC from browsers via Envoy.