Skip to content

Add streaming-tour and middleware examples#46

Merged
iainmcgin merged 2 commits intomainfrom
feat/examples-streaming-middleware
Apr 3, 2026
Merged

Add streaming-tour and middleware examples#46
iainmcgin merged 2 commits intomainfrom
feat/examples-streaming-middleware

Conversation

@iainmcgin
Copy link
Copy Markdown
Collaborator

Closes #45

Adds two small focused example crates that close the most-asked-about gaps for new adopters: protocol-mechanics demonstration of all four RPC types, and server-side tower middleware composition.

examples/streaming-tour

A trivial `NumberService` exercising every ConnectRPC RPC type:

RPC Type Semantics
`Square` Unary n -> n*n
`Range` Server streaming emit `count` integers from `start`
`Sum` Client streaming total all values
`RunningSum` Bidirectional streaming emit running total after each request

The methods are deliberately tiny - the example exists to show handler signatures and client invocation patterns side-by-side. Ships server + client binaries plus an integration test that spins up the server in-process and verifies every RPC.

examples/middleware

Server-side tower middleware composition around the connect router:

  • `AuthLayer` - hand-rolled `tower::Layer` that validates a `Bearer ` header against a static map. On success, stamps a `UserId` into the request extensions. On failure, short-circuits with a 401 in the Connect-protocol JSON error shape.
  • `TraceLayer` - tower-http request/response logging.
  • `TimeoutLayer` - per-request handler deadline.

Layers compose via `ServiceBuilder` mounted on `axum::Router::layer()`, so axum handles body conversion from `ConnectRpcBody` to `axum::body::Body`. The handler reads `UserId` from `Context::extensions()`, performs a per-secret permission check, and writes a `x-served-by` response trailer via `Context::set_trailer()`.

The client demonstrates `ClientConfig::default_header` for the auth header, `ClientConfig::default_timeout` for a default deadline, and `CallOptions::with_timeout` for a per-call override. Integration tests cover four paths: authorized success (verifying the trailer arrives), missing auth header, invalid token, and permission denied.

Verification

  • `cargo build -p streaming-tour-example -p middleware-example` clean
  • `cargo test -p streaming-tour-example -p middleware-example` - 8/8 tests pass
  • `cargo clippy -p streaming-tour-example -p middleware-example --all-targets -- -D warnings` clean
  • `cargo +nightly fmt --all -- --check` clean
  • `cargo check --workspace --all-features --all-targets` clean (workspace-wide)

Branch order note

This branch is off main (pre-#44). When #44 lands, I'll rebase this branch onto main and add `rust-version.workspace = true` to the two new examples' `Cargo.toml` to match the rest of the workspace.

@iainmcgin
Copy link
Copy Markdown
Collaborator Author

[claude code] Updated per review feedback: switched proto packages from connectrpc.tour.v1 / connectrpc.middleware_demo.v1 to anthropic.connectrpc.tour.v1 / anthropic.connectrpc.middleware_demo.v1 (matching the existing convention from examples/multiservice/), and switched both new .proto files from syntax = "proto3" to edition = "2023" with features.field_presence = IMPLICIT to keep proto3-equivalent semantics. (Edition 2024 requires protoc 30+; CI's apt-installed protoc and the local 29.6 toolchain only support up to edition 2023.) Tests still 8/8 passing.

@iainmcgin iainmcgin force-pushed the feat/examples-streaming-middleware branch from 69bea69 to 1dff769 Compare April 3, 2026 19:33
@iainmcgin
Copy link
Copy Markdown
Collaborator Author

[claude code] Removed the IMPLICIT field-presence override, so the new examples now use edition 2023's actual defaults (EXPLICIT presence). All scalar/string fields are now Option<T> in generated Rust code. Source code updated accordingly: writes wrap in Some(...) and reads use .unwrap_or(...) to retain proto3-equivalent zero-default semantics. Tests still 8/8 passing.

@iainmcgin
Copy link
Copy Markdown
Collaborator Author

[claude code] Refactored the auth middleware from a hand-rolled tower::Layer/tower::Service pair to axum::middleware::from_fn_with_state. The connectrpc-specific lesson ("middleware writes to request extensions, handler reads via Context::extensions") is unchanged, but the AuthLayer/Service trait wiring is replaced with a plain async fn. Net delete of ~85 lines, same end-to-end behavior. Tests still 4/4 passing.

@iainmcgin iainmcgin force-pushed the feat/examples-streaming-middleware branch 2 times, most recently from 484cf3a to b2b499e Compare April 3, 2026 20:31
@iainmcgin iainmcgin requested a review from asacamano April 3, 2026 20:33
Two small example crates that close the most-asked-about gaps for new
adopters.

streaming-tour: a NumberService demonstrating all four ConnectRPC RPC
types (unary, server stream, client stream, bidi stream) in one place.
The methods are deliberately trivial - the example exists to show the
wire-protocol shapes (handler signatures, client invocation patterns).

middleware: server-side tower middleware composition around the connect
router. A custom AuthLayer validates a Bearer token, stamps caller
identity into request extensions, and short-circuits unauthorized
requests with 401 in the Connect-protocol JSON error shape.
TraceLayer and TimeoutLayer round out the stack via ServiceBuilder
mounted on axum::Router. The handler reads the identity from
Context::extensions() and writes a x-served-by response trailer via
Context::set_trailer(). The client demonstrates ClientConfig::default_header
for the auth header and CallOptions::with_timeout for per-call deadlines.

Both examples ship server + client binaries plus integration tests
that spin up the server in-process and exercise every code path.
@iainmcgin iainmcgin force-pushed the feat/examples-streaming-middleware branch from b2b499e to a2c412a Compare April 3, 2026 20:35
@iainmcgin
Copy link
Copy Markdown
Collaborator Author

[claude code] CI was failing because Ubuntu's apt-installed protoc (v21.x) doesn't recognize edition = "2023" syntax used by the new examples. Fixed by:

  1. Rebasing onto main (which merged Pin MSRV to Rust 1.88 and verify in CI #44 with the MSRV pin)
  2. Adding rust-version.workspace = true to the new example Cargo.tomls (matching the rest of the workspace post-Pin MSRV to Rust 1.88 and verify in CI #44)
  3. Replacing apt install protobuf-compiler with curl-from-GitHub-release for protoc 33.5 in all 7 CI jobs that need it (matching the buffa CI pattern)

Local verification: cargo test -p streaming-tour-example -p middleware-example (8/8 pass), cargo +1.88 check --workspace --all-features --all-targets (clean), workflow YAML validates.

asacamano
asacamano previously approved these changes Apr 3, 2026
// After each request, emits the running total — letting the client
// observe the cumulative sum mid-stream rather than waiting for the
// end.
rpc RunningSum(stream RunningSumRequest) returns (stream RunningSumResponse);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice example!

The generated `_with_options` doc comment used the shortcut intra-doc
link form `[connectrpc::client::ClientConfig]`, which fails to resolve
when the proto package contains `connectrpc` as a path component (e.g.
`anthropic.connectrpc.tour.v1`). The package's nested module shadows
the external crate name during rustdoc path resolution, producing
'no item named client in module connectrpc'.

Switch to the reference-style form `[ClientConfig](::connectrpc::...)`
with a leading `::` for absolute path resolution, matching the pattern
already used for UnaryResponse on lines 685 and 694.
@iainmcgin
Copy link
Copy Markdown
Collaborator Author

[claude code] CI green now (12/12 checks). The Documentation job was failing on a separate issue beyond the protoc version: a broken intra-doc link in generated client code.

The codegen template at connectrpc-codegen/src/codegen.rs:1115 emits a doc comment with the shortcut intra-doc link [connectrpc::client::ClientConfig]. When the proto package contains connectrpc as a path component (e.g. anthropic.connectrpc.tour.v1), the generated module proto::anthropic::connectrpc shadows the external crate name during rustdoc path resolution, producing 'no item named client in module connectrpc'.

Existing examples don't hit this because they ship checked-in code with use connectrpc::client::ClientConfig; at file top, so the doc comment can use the short [ClientConfig] form. Build.rs-generated code (no top-level use statements) needs the absolute-path form.

Fixed in the codegen by switching to the reference-style [ClientConfig](::connectrpc::client::ClientConfig) with a leading ::, matching the pattern already used for UnaryResponse references on lines 685 and 694.

@iainmcgin iainmcgin marked this pull request as ready for review April 3, 2026 20:50
@iainmcgin iainmcgin enabled auto-merge (squash) April 3, 2026 20:50
@iainmcgin iainmcgin merged commit b49ebdd into main Apr 3, 2026
12 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Apr 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add streaming-tour and middleware examples

2 participants