Lesson 20 of 30
gRPC Streaming, Status Codes, Deadlines, and Interceptors
A gRPC call can send a stream of messages in either direction, and every call has a status, metadata, and an optional deadline. This lesson builds an Orders service with all four call types, status codes, metadata, an authentication interceptor, and a client that uses deadlines. It builds on gRPC basics.
| Term | Meaning |
|---|---|
| Server streaming | One request, many responses |
| Client streaming | Many requests, one response |
| Bidirectional streaming | Both sides send messages independently |
| Metadata | Key-value pairs sent as HTTP/2 headers |
| Deadline | The time by which a call must finish |
| Interceptor | A function that inspects a request before the handler runs and can reject it |
Project setup
protoc must be installed, as in the basics lesson.
Cargo.toml
[package]
name = "orders"
version = "0.1.0"
edition = "2021"
[dependencies]
tonic = "0.13"
prost = "0.13"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
tokio-stream = "0.1"
[build-dependencies]
tonic-build = "0.13"build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/orders.proto")?;
Ok(())
}Example 1: The four call types in a schema
The stream keyword on the request, the response, or both selects the call type.
proto/orders.proto
syntax = "proto3";
package storefront.orders.v1;
service Orders {
// Unary
rpc GetOrder(GetOrderRequest) returns (Order);
// Server streaming: one request, many responses
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
// Client streaming: many requests, one response
rpc ImportOrders(stream Order) returns (ImportSummary);
// Bidirectional streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetOrderRequest {
int64 order_id = 1;
}
message Order {
int64 id = 1;
string status = 2;
int64 total_cents = 3;
}
message WatchOrderRequest {
int64 order_id = 1;
}
message OrderEvent {
int64 order_id = 1;
string status = 2;
}
message ImportSummary {
int32 imported = 1;
}
message ChatMessage {
string text = 1;
}- Each message on a stream is length-prefixed, so the receiver reads whole messages one at a time.
- A sender ends its side by half-closing. The server ends a call by returning a status.
- HTTP/2 flow control applies per stream. A slow reader stops the sender once the window is full, which gives backpressure (the fast side waits for the slow side) with no extra code.
The Rust side is split into one module per call type. service.rs implements the generated trait and delegates each method. A server-streaming method returns a boxed stream, and the trait names it as an associated type. The crate compiles once every file from Examples 1 to 6 exists.
src/lib.rs
use std::pin::Pin;
use tokio_stream::Stream;
use tonic::Status;
pub mod pb {
tonic::include_proto!("storefront.orders.v1");
}
pub mod auth;
pub mod chat;
pub mod import;
pub mod service;
pub mod unary;
pub mod watch;
pub type BoxStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send>>;src/service.rs
use tonic::{Request, Response, Status, Streaming};
use crate::pb::{
orders_server::Orders, ChatMessage, GetOrderRequest, ImportSummary, Order, OrderEvent,
WatchOrderRequest,
};
use crate::{chat, import, unary, watch, BoxStream};
#[derive(Default)]
pub struct OrdersService;
#[tonic::async_trait]
impl Orders for OrdersService {
type WatchOrderStream = BoxStream<OrderEvent>;
type ChatStream = BoxStream<ChatMessage>;
async fn get_order(
&self,
request: Request<GetOrderRequest>,
) -> Result<Response<Order>, Status> {
unary::get_order(request).await
}
async fn watch_order(
&self,
request: Request<WatchOrderRequest>,
) -> Result<Response<Self::WatchOrderStream>, Status> {
watch::watch_order(request).await
}
async fn import_orders(
&self,
request: Request<Streaming<Order>>,
) -> Result<Response<ImportSummary>, Status> {
import::import_orders(request).await
}
async fn chat(
&self,
request: Request<Streaming<ChatMessage>>,
) -> Result<Response<Self::ChatStream>, Status> {
chat::chat(request).await
}
}Example 2: Server streaming
The handler returns a stream of Result<OrderEvent, Status>. A spawned task writes events into a bounded Tokio channel, and the receiver, wrapped as a stream, becomes the response.
src/watch.rs
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};
use crate::pb::{OrderEvent, WatchOrderRequest};
use crate::BoxStream;
pub async fn watch_order(
request: Request<WatchOrderRequest>,
) -> Result<Response<BoxStream<OrderEvent>>, Status> {
let order_id = request.into_inner().order_id;
if order_id <= 0 {
return Err(Status::invalid_argument("order id must be positive"));
}
let (tx, rx) = tokio::sync::mpsc::channel(4);
tokio::spawn(async move {
for status in ["placed", "paid", "shipped"] {
let event = OrderEvent {
order_id,
status: status.to_owned(),
};
if tx.send(Ok(event)).await.is_err() {
// The client disconnected or cancelled: stop producing.
break;
}
}
});
Ok(Response::new(
Box::pin(ReceiverStream::new(rx)) as BoxStream<OrderEvent>
))
}- The channel holds 4 messages, so the producer waits when the client reads slowly.
tx.sendfails when the receiver is dropped, which happens on cancel or disconnect. Checking the error is how a spawned producer learns to stop.- The client reads with
message().await, which returnsResult<Option<T>, Status>.Nonemeans the server finished cleanly.
Example 3: Client streaming
The server receives a Streaming<Order> and reads it in a loop. It answers once after the client half-closes.
src/import.rs
use tonic::{Request, Response, Status, Streaming};
use crate::pb::{ImportSummary, Order};
pub async fn import_orders(
request: Request<Streaming<Order>>,
) -> Result<Response<ImportSummary>, Status> {
let mut stream = request.into_inner();
let mut imported = 0;
while let Some(order) = stream.message().await? {
if order.id <= 0 {
return Err(Status::invalid_argument("order id must be positive"));
}
imported += 1;
}
Ok(Response::new(ImportSummary { imported }))
}The server processes each message as it arrives instead of buffering the batch, and it can reject the call early. The client passes any stream of messages: an iterator for a fixed batch, a channel receiver for a live feed.
Example 4: Bidirectional streaming
The two directions are independent. This handler answers each inbound message, but a server could answer in batches or send without receiving. Order is preserved within one direction and is undefined between directions.
src/chat.rs
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, Streaming};
use crate::pb::ChatMessage;
use crate::BoxStream;
pub async fn chat(
request: Request<Streaming<ChatMessage>>,
) -> Result<Response<BoxStream<ChatMessage>>, Status> {
let mut inbound = request.into_inner();
let (tx, rx) = tokio::sync::mpsc::channel(8);
tokio::spawn(async move {
while let Ok(Some(message)) = inbound.message().await {
let reply = ChatMessage {
text: format!("echo: {}", message.text),
};
if tx.send(Ok(reply)).await.is_err() {
break;
}
}
});
Ok(Response::new(
Box::pin(ReceiverStream::new(rx)) as BoxStream<ChatMessage>
))
}A stream keeps a connection open, pins its messages to one backend, and cannot be retried as a whole once messages were sent. Unary calls fit single request-response exchanges. Streams fit data that is a sequence.
Example 5: Status codes and metadata
A Status has a code from a fixed set and a message. Callers react to the code without parsing text.
| Code | Use for | Retry |
|---|---|---|
InvalidArgument |
The request is malformed regardless of state | No |
NotFound |
The entity does not exist | No |
AlreadyExists |
A create conflicts with an existing entity | No |
PermissionDenied |
The caller is known but not allowed | No |
Unauthenticated |
Credentials are missing or invalid | After fixing credentials |
FailedPrecondition |
The system state does not allow the call | No, until state changes |
ResourceExhausted |
Quota or rate limit reached | After backoff |
Aborted |
Concurrency conflict, such as a failed optimistic check | Yes, at a higher level |
Unavailable |
The service cannot serve now | Yes, with backoff |
DeadlineExceeded |
The deadline passed | Only if the call is idempotent |
Unimplemented |
The method is not supported | No |
Internal |
A bug or broken invariant | No |
Tonic has one constructor per code, such as Status::not_found(...). NotFound corresponds to HTTP 404, InvalidArgument to 400, Unauthenticated to 401, PermissionDenied to 403, and Unavailable to 503. The resilience patterns retry rules depend on these codes. Retrying a non-idempotent call after DeadlineExceeded can run it twice, because the server may finish after the client gave up. See SQL transactions for idempotency keys.
Metadata carries information about the call, such as tokens and request ids. Keys are lowercase ASCII. Values are ASCII strings, except keys ending in -bin, which hold bytes.
The handler below reads x-request-id from metadata and a CallerId that the interceptor in Example 6 stores in the request extensions. Order 2 sleeps for 2 seconds so that Example 7 can show a deadline.
src/unary.rs
use std::time::Duration;
use tonic::{Request, Response, Status};
use crate::auth::CallerId;
use crate::pb::{GetOrderRequest, Order};
pub async fn get_order(request: Request<GetOrderRequest>) -> Result<Response<Order>, Status> {
let request_id = request
.metadata()
.get("x-request-id")
.and_then(|value| value.to_str().ok())
.unwrap_or("none")
.to_owned();
let caller = request.extensions().get::<CallerId>().map(|caller| caller.0);
let order_id = request.into_inner().order_id;
match order_id {
id if id <= 0 => Err(Status::invalid_argument("order id must be positive")),
1 => Ok(Response::new(Order {
id: 1,
status: format!("paid; request_id={request_id}; caller={caller:?}"),
total_cents: 1250,
})),
2 => {
tokio::time::sleep(Duration::from_secs(2)).await;
Ok(Response::new(Order {
id: 2,
status: "shipped".to_owned(),
total_cents: 830,
}))
}
id => Err(Status::not_found(format!("order {id} not found"))),
}
}Propagate the request id and trace context from metadata into the current span, as in observability.
Example 6: An authentication interceptor and the server
An interceptor is a function FnMut(Request<()>) -> Result<Request<()>, Status>. It sees metadata and extensions but not the typed message. It returns the request to continue or a Status to reject the call.
src/auth.rs
use tonic::{Request, Status};
#[derive(Clone, Copy, Debug)]
pub struct CallerId(pub u64);
pub fn check_auth(mut request: Request<()>) -> Result<Request<()>, Status> {
let token = request
.metadata()
.get("authorization")
.and_then(|value| value.to_str().ok());
match token {
Some("Bearer secret-token") => {
request.extensions_mut().insert(CallerId(42));
Ok(request)
}
_ => Err(Status::unauthenticated("missing or invalid token")),
}
}The fixed token only shows the mechanism. Real validation uses the JWT checks from OAuth, JWT, and mTLS.
src/main.rs
use orders::{auth::check_auth, pb::orders_server::OrdersServer, service::OrdersService};
use tonic::transport::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "127.0.0.1:50052".parse()?;
println!("orders listening on {addr}");
Server::builder()
.add_service(OrdersServer::with_interceptor(OrdersService, check_auth))
.serve(addr)
.await?;
Ok(())
}Interceptors cannot read messages or responses. Timing, result logging, and response changes use a Tower layer, which wraps the whole service. Tonic servers are Tower services, so the layers from Tower middleware attach with Server::builder().layer(...).
Example 7: Deadlines, cancellation, and the client
A deadline is the point in time by which a call must finish. A timeout is the duration that produces it. Request::set_timeout sends the duration in the grpc-timeout header so the server knows how long the caller waits.
Propagation. If a gateway allows 500 ms and Orders calls Catalog with a fresh 5 s timeout, Catalog keeps working after the gateway gave up. Each hop passes the remaining budget, computed from the time already spent, to the next call.
Cancellation happens when the caller drops the call future, the deadline passes, or the connection closes. The server drops the handler future at its next .await.
- Code between
.awaitpoints always runs to completion. - A task started with
tokio::spawnis not cancelled with the handler. The producers in Examples 2 and 4 stop only becausetx.sendfails. - A database transaction held across an
.awaitis rolled back when the handler is dropped. Work that must survive cancellation belongs in a spawned task or in the outbox pattern.
The client below guards the slow call with a local tokio::time::timeout of 300 ms, which drops the call future and cancels it. The function add_token is a client interceptor that attaches the token to every call.
src/bin/client.rs
use std::time::Duration;
use orders::pb::{
orders_client::OrdersClient, ChatMessage, GetOrderRequest, ImportSummary, Order,
WatchOrderRequest,
};
use tonic::{transport::Channel, Request, Status};
fn add_token(mut request: Request<()>) -> Result<Request<()>, Status> {
request
.metadata_mut()
.insert("authorization", "Bearer secret-token".parse().unwrap());
Ok(request)
}
fn sample_order(id: i64) -> Order {
Order {
id,
status: "new".to_owned(),
total_cents: 100,
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let channel = Channel::from_static("http://127.0.0.1:50052").connect().await?;
let mut authed = OrdersClient::with_interceptor(channel.clone(), add_token);
let mut anonymous = OrdersClient::new(channel);
// Unary with metadata.
let mut request = Request::new(GetOrderRequest { order_id: 1 });
request
.metadata_mut()
.insert("x-request-id", "req-1042".parse().unwrap());
let first = authed.get_order(request).await?.into_inner();
println!("order {}: {}", first.id, first.status);
// Interceptor rejects a call without a token.
if let Err(status) = anonymous.get_order(GetOrderRequest { order_id: 1 }).await {
println!("{:?}: {}", status.code(), status.message());
}
// Status code from the handler.
if let Err(status) = authed.get_order(GetOrderRequest { order_id: 99 }).await {
println!("{:?}: {}", status.code(), status.message());
}
// Deadline: the server sleeps 2 s, the caller stops waiting after 300 ms.
let mut slow = Request::new(GetOrderRequest { order_id: 2 });
slow.set_timeout(Duration::from_secs(1));
let guarded = tokio::time::timeout(Duration::from_millis(300), authed.get_order(slow)).await;
if guarded.is_err() {
println!("slow order dropped after 300 ms");
}
// Server streaming.
let mut events = authed
.watch_order(WatchOrderRequest { order_id: 7 })
.await?
.into_inner();
while let Some(event) = events.message().await? {
println!("order {} is {}", event.order_id, event.status);
}
// Client streaming.
let summary: ImportSummary = authed
.import_orders(tokio_stream::iter(vec![sample_order(1), sample_order(2)]))
.await?
.into_inner();
println!("imported {}", summary.imported);
if let Err(status) = authed
.import_orders(tokio_stream::iter(vec![sample_order(3), sample_order(0)]))
.await
{
println!("{:?}: {}", status.code(), status.message());
}
// Bidirectional streaming.
let outbound = tokio_stream::iter(["hello", "status", "bye"].map(|text| ChatMessage {
text: text.to_owned(),
}));
let mut replies = authed.chat(outbound).await?.into_inner();
while let Some(reply) = replies.message().await? {
println!("{}", reply.text);
}
Ok(())
}Complete example
Prerequisites: Rust, protoc. File tree:
orders/
├── Cargo.toml
├── build.rs
├── proto/
│ └── orders.proto
└── src/
├── lib.rs
├── service.rs
├── watch.rs
├── import.rs
├── chat.rs
├── unary.rs
├── auth.rs
├── main.rs
└── bin/
└── client.rsCargo.toml
[package]
name = "orders"
version = "0.1.0"
edition = "2021"
[dependencies]
tonic = "0.13"
prost = "0.13"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
tokio-stream = "0.1"
[build-dependencies]
tonic-build = "0.13"build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/orders.proto")?;
Ok(())
}proto/orders.proto
syntax = "proto3";
package storefront.orders.v1;
service Orders {
// Unary
rpc GetOrder(GetOrderRequest) returns (Order);
// Server streaming: one request, many responses
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
// Client streaming: many requests, one response
rpc ImportOrders(stream Order) returns (ImportSummary);
// Bidirectional streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetOrderRequest {
int64 order_id = 1;
}
message Order {
int64 id = 1;
string status = 2;
int64 total_cents = 3;
}
message WatchOrderRequest {
int64 order_id = 1;
}
message OrderEvent {
int64 order_id = 1;
string status = 2;
}
message ImportSummary {
int32 imported = 1;
}
message ChatMessage {
string text = 1;
}src/lib.rs
use std::pin::Pin;
use tokio_stream::Stream;
use tonic::Status;
pub mod pb {
tonic::include_proto!("storefront.orders.v1");
}
pub mod auth;
pub mod chat;
pub mod import;
pub mod service;
pub mod unary;
pub mod watch;
pub type BoxStream<T> = Pin<Box<dyn Stream<Item = Result<T, Status>> + Send>>;src/service.rs
use tonic::{Request, Response, Status, Streaming};
use crate::pb::{
orders_server::Orders, ChatMessage, GetOrderRequest, ImportSummary, Order, OrderEvent,
WatchOrderRequest,
};
use crate::{chat, import, unary, watch, BoxStream};
#[derive(Default)]
pub struct OrdersService;
#[tonic::async_trait]
impl Orders for OrdersService {
type WatchOrderStream = BoxStream<OrderEvent>;
type ChatStream = BoxStream<ChatMessage>;
async fn get_order(
&self,
request: Request<GetOrderRequest>,
) -> Result<Response<Order>, Status> {
unary::get_order(request).await
}
async fn watch_order(
&self,
request: Request<WatchOrderRequest>,
) -> Result<Response<Self::WatchOrderStream>, Status> {
watch::watch_order(request).await
}
async fn import_orders(
&self,
request: Request<Streaming<Order>>,
) -> Result<Response<ImportSummary>, Status> {
import::import_orders(request).await
}
async fn chat(
&self,
request: Request<Streaming<ChatMessage>>,
) -> Result<Response<Self::ChatStream>, Status> {
chat::chat(request).await
}
}src/watch.rs
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};
use crate::pb::{OrderEvent, WatchOrderRequest};
use crate::BoxStream;
pub async fn watch_order(
request: Request<WatchOrderRequest>,
) -> Result<Response<BoxStream<OrderEvent>>, Status> {
let order_id = request.into_inner().order_id;
if order_id <= 0 {
return Err(Status::invalid_argument("order id must be positive"));
}
let (tx, rx) = tokio::sync::mpsc::channel(4);
tokio::spawn(async move {
for status in ["placed", "paid", "shipped"] {
let event = OrderEvent {
order_id,
status: status.to_owned(),
};
if tx.send(Ok(event)).await.is_err() {
// The client disconnected or cancelled: stop producing.
break;
}
}
});
Ok(Response::new(
Box::pin(ReceiverStream::new(rx)) as BoxStream<OrderEvent>
))
}src/import.rs
use tonic::{Request, Response, Status, Streaming};
use crate::pb::{ImportSummary, Order};
pub async fn import_orders(
request: Request<Streaming<Order>>,
) -> Result<Response<ImportSummary>, Status> {
let mut stream = request.into_inner();
let mut imported = 0;
while let Some(order) = stream.message().await? {
if order.id <= 0 {
return Err(Status::invalid_argument("order id must be positive"));
}
imported += 1;
}
Ok(Response::new(ImportSummary { imported }))
}src/chat.rs
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, Streaming};
use crate::pb::ChatMessage;
use crate::BoxStream;
pub async fn chat(
request: Request<Streaming<ChatMessage>>,
) -> Result<Response<BoxStream<ChatMessage>>, Status> {
let mut inbound = request.into_inner();
let (tx, rx) = tokio::sync::mpsc::channel(8);
tokio::spawn(async move {
while let Ok(Some(message)) = inbound.message().await {
let reply = ChatMessage {
text: format!("echo: {}", message.text),
};
if tx.send(Ok(reply)).await.is_err() {
break;
}
}
});
Ok(Response::new(
Box::pin(ReceiverStream::new(rx)) as BoxStream<ChatMessage>
))
}src/unary.rs
use std::time::Duration;
use tonic::{Request, Response, Status};
use crate::auth::CallerId;
use crate::pb::{GetOrderRequest, Order};
pub async fn get_order(request: Request<GetOrderRequest>) -> Result<Response<Order>, Status> {
let request_id = request
.metadata()
.get("x-request-id")
.and_then(|value| value.to_str().ok())
.unwrap_or("none")
.to_owned();
let caller = request.extensions().get::<CallerId>().map(|caller| caller.0);
let order_id = request.into_inner().order_id;
match order_id {
id if id <= 0 => Err(Status::invalid_argument("order id must be positive")),
1 => Ok(Response::new(Order {
id: 1,
status: format!("paid; request_id={request_id}; caller={caller:?}"),
total_cents: 1250,
})),
2 => {
tokio::time::sleep(Duration::from_secs(2)).await;
Ok(Response::new(Order {
id: 2,
status: "shipped".to_owned(),
total_cents: 830,
}))
}
id => Err(Status::not_found(format!("order {id} not found"))),
}
}src/auth.rs
use tonic::{Request, Status};
#[derive(Clone, Copy, Debug)]
pub struct CallerId(pub u64);
pub fn check_auth(mut request: Request<()>) -> Result<Request<()>, Status> {
let token = request
.metadata()
.get("authorization")
.and_then(|value| value.to_str().ok());
match token {
Some("Bearer secret-token") => {
request.extensions_mut().insert(CallerId(42));
Ok(request)
}
_ => Err(Status::unauthenticated("missing or invalid token")),
}
}src/main.rs
use orders::{auth::check_auth, pb::orders_server::OrdersServer, service::OrdersService};
use tonic::transport::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "127.0.0.1:50052".parse()?;
println!("orders listening on {addr}");
Server::builder()
.add_service(OrdersServer::with_interceptor(OrdersService, check_auth))
.serve(addr)
.await?;
Ok(())
}src/bin/client.rs
use std::time::Duration;
use orders::pb::{
orders_client::OrdersClient, ChatMessage, GetOrderRequest, ImportSummary, Order,
WatchOrderRequest,
};
use tonic::{transport::Channel, Request, Status};
fn add_token(mut request: Request<()>) -> Result<Request<()>, Status> {
request
.metadata_mut()
.insert("authorization", "Bearer secret-token".parse().unwrap());
Ok(request)
}
fn sample_order(id: i64) -> Order {
Order {
id,
status: "new".to_owned(),
total_cents: 100,
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let channel = Channel::from_static("http://127.0.0.1:50052").connect().await?;
let mut authed = OrdersClient::with_interceptor(channel.clone(), add_token);
let mut anonymous = OrdersClient::new(channel);
// Unary with metadata.
let mut request = Request::new(GetOrderRequest { order_id: 1 });
request
.metadata_mut()
.insert("x-request-id", "req-1042".parse().unwrap());
let first = authed.get_order(request).await?.into_inner();
println!("order {}: {}", first.id, first.status);
// Interceptor rejects a call without a token.
if let Err(status) = anonymous.get_order(GetOrderRequest { order_id: 1 }).await {
println!("{:?}: {}", status.code(), status.message());
}
// Status code from the handler.
if let Err(status) = authed.get_order(GetOrderRequest { order_id: 99 }).await {
println!("{:?}: {}", status.code(), status.message());
}
// Deadline: the server sleeps 2 s, the caller stops waiting after 300 ms.
let mut slow = Request::new(GetOrderRequest { order_id: 2 });
slow.set_timeout(Duration::from_secs(1));
let guarded = tokio::time::timeout(Duration::from_millis(300), authed.get_order(slow)).await;
if guarded.is_err() {
println!("slow order dropped after 300 ms");
}
// Server streaming.
let mut events = authed
.watch_order(WatchOrderRequest { order_id: 7 })
.await?
.into_inner();
while let Some(event) = events.message().await? {
println!("order {} is {}", event.order_id, event.status);
}
// Client streaming.
let summary: ImportSummary = authed
.import_orders(tokio_stream::iter(vec![sample_order(1), sample_order(2)]))
.await?
.into_inner();
println!("imported {}", summary.imported);
if let Err(status) = authed
.import_orders(tokio_stream::iter(vec![sample_order(3), sample_order(0)]))
.await
{
println!("{:?}: {}", status.code(), status.message());
}
// Bidirectional streaming.
let outbound = tokio_stream::iter(["hello", "status", "bye"].map(|text| ChatMessage {
text: text.to_owned(),
}));
let mut replies = authed.chat(outbound).await?.into_inner();
while let Some(reply) = replies.message().await? {
println!("{}", reply.text);
}
Ok(())
}Run the server in one terminal and the client in another:
cargo run --bin orders
cargo run --bin clientServer output:
orders listening on 127.0.0.1:50052Client output:
order 1: paid; request_id=req-1042; caller=Some(42)
Unauthenticated: missing or invalid token
NotFound: order 99 not found
slow order dropped after 300 ms
order 7 is placed
order 7 is paid
order 7 is shipped
imported 2
InvalidArgument: order id must be positive
echo: hello
echo: status
echo: byecurl does not speak gRPC framing, so the Rust client is the test tool here. Reflection and grpcurl come in gRPC in production.
Summary
- A
.protorpcusesstreamon the request, the response, or both to define streaming calls. - Server streaming returns a boxed stream, usually built from an mpsc receiver. Client streaming and bidirectional handlers read a
Streaming<T>withmessage().await. - Bounded channels plus HTTP/2 flow control give backpressure. A failed
sendtells a producer to stop. Statuscodes classify failures, and each code implies a retry policy.Unavailableis the standard retryable code.- Metadata carries tokens and trace ids. Keys are lowercase, and binary values use the
-binsuffix. grpc-timeoutcarries the deadline. Each hop passes the remaining time, not a fresh timeout.- Dropping a handler future cancels it at the next
.await. Spawned tasks need their own stop condition. - Interceptors see metadata and extensions only. Tower layers see whole requests and responses.
