← Week 1: Protocol Buffers & gRPC

Day 5: tonic Client — Interceptors and TLS

Phase 3 · Jul 5, 2026

← Week 1: Protocol Buffers & gRPC

Agenda (2–3 hours)

  • Read (45 min): tonic client examples; tonic::transport::Channel documentation; tonic::service::interceptor
  • Study (45 min): How does connection pooling work in tonic? What is the difference between Channel and Endpoint?
  • Practice (45 min): Build a client that adds auth metadata via interceptor, handles streaming, and reconnects on failure
  • Challenge (30 min): Implement client-side load balancing over multiple Endpoints using Channel::balance_channel
← Week 1: Protocol Buffers & gRPC

Basic tonic Client

use tonic::transport::Channel;
use myapp::v1::task_service_client::TaskServiceClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let channel = Channel::from_static("http://localhost:50051")
        .connect()
        .await?;

    let mut client = TaskServiceClient::new(channel);

    let response = client
        .get_task(tonic::Request::new(GetTaskRequest { id: "42".to_string() }))
        .await?;

    println!("{:?}", response.into_inner());
    Ok(())
}
← Week 1: Protocol Buffers & gRPC

Adding Metadata via Interceptor

use tonic::service::interceptor;
use tonic::metadata::MetadataValue;

fn auth_interceptor(token: String) -> impl Fn(Request<()>) -> Result<Request<()>, Status> {
    move |mut req: Request<()>| {
        let token: MetadataValue<_> = format!("Bearer {}", token).parse().unwrap();
        req.metadata_mut().insert("authorization", token);
        Ok(req)
    }
}

let channel = Channel::from_static("http://localhost:50051")
    .connect()
    .await?;

let client = TaskServiceClient::with_interceptor(
    channel,
    auth_interceptor("my-secret-token".to_string()),
);
← Week 1: Protocol Buffers & gRPC

TLS Configuration

use tonic::transport::{Certificate, ClientTlsConfig};

let pem = tokio::fs::read("ca.crt").await?;
let ca = Certificate::from_pem(pem);

let tls = ClientTlsConfig::new()
    .ca_certificate(ca)
    .domain_name("myserver.example.com");

let channel = Channel::from_static("https://myserver.example.com:50051")
    .tls_config(tls)?
    .connect()
    .await?;

For mTLS (client auth): add .identity(client_cert, client_key) to the TLS config.

← Week 1: Protocol Buffers & gRPC

Client Streaming

let mut client = TaskServiceClient::new(channel);

let requests = vec![
    LogEntry { message: "line 1".to_string() },
    LogEntry { message: "line 2".to_string() },
];

let response = client
    .upload_logs(tokio_stream::iter(requests))
    .await?;

println!("uploaded: {:?}", response.into_inner());

tokio_stream::iter wraps any IntoIterator as a stream. For real streaming, use tokio::sync::mpsc with tokio_stream::wrappers::ReceiverStream.

← Week 1: Protocol Buffers & gRPC

Key Takeaways

  • Channel manages the HTTP/2 connection; Endpoint configures a single backend
  • Interceptors are Tower middleware specialized for gRPC request/response
  • TLS and mTLS configuration is done at the Channel level
  • Client-side streaming uses Stream as the request type — wrap with tokio_stream

Tomorrow: gRPC error handling — Status codes, error details, deadlines.