connectrpc/lib.rs
1//! ConnectRPC implementation for Rust
2//!
3//! This crate provides a tower-based ConnectRPC runtime that can be integrated
4//! with any HTTP framework that supports tower services (axum, hyper, tonic, etc.).
5//!
6//! # Architecture
7//!
8//! The core abstraction is [`ConnectRpcService`], a [`tower::Service`] that handles
9//! ConnectRPC requests. This allows seamless integration with existing web servers:
10//!
11//! ```rust,ignore
12//! use connectrpc::{Router, ConnectRpcService};
13//! use std::sync::Arc;
14//!
15//! // Build your router with RPC handlers
16//! let greet_impl = Arc::new(MyGreetService);
17//! let router = Router::new().add_service(greet_impl);
18//!
19//! // Get a tower::Service - use with ANY compatible framework
20//! let service = ConnectRpcService::new(router);
21//! ```
22//!
23//! # Framework Integration
24//!
25//! ## With Axum (recommended)
26//!
27//! Enable the `axum` feature for convenient integration:
28//!
29//! ```rust,ignore
30//! use axum::{Router, routing::get};
31//! use connectrpc::Router as ConnectRouter;
32//! use std::sync::Arc;
33//!
34//! let greet_impl = Arc::new(MyGreetService);
35//! let connect = ConnectRouter::new().add_service(greet_impl);
36//!
37//! let app = Router::new()
38//! .route("/health", get(health))
39//! .fallback_service(connect.into_axum_service());
40//!
41//! axum::serve(listener, app).await?;
42//! ```
43//!
44//! ## With Raw Hyper
45//!
46//! Use `ConnectRpcService` directly with hyper's service machinery.
47//!
48//! ## Standalone Server
49//!
50//! For simple cases, enable the `server` feature for a built-in hyper server:
51//!
52//! ```rust,ignore
53//! use connectrpc::{Router, Server};
54//!
55//! let router = Router::new();
56//! // ... register handlers ...
57//!
58//! Server::new(router).serve(addr).await?;
59//! ```
60//!
61//! # Modules
62//!
63//! - [`codec`] - Message encoding/decoding (protobuf and JSON)
64//! - [`compression`] - Pluggable compression (gzip, zstd) with streaming support
65//! - [`envelope`] - Streaming message framing (5-byte header + payload)
66//! - [`error`] - ConnectRPC error types and HTTP status mapping
67//! - [`handler`] - Async handler traits for implementing RPC methods
68//! - [`request`] - Borrowed single-message request views ([`ServiceRequest`])
69//! - [`stream_message`] - Owned per-item streaming message wrapper ([`StreamMessage`])
70//! - [`response`] - Handler response types and [`RequestContext`]
71//! - [`router`] - Request routing and service registration
72//! - [`service`] - Tower service implementation (primary integration point)
73//! - [`dispatcher`] - Method dispatch glue between router and generated code
74//! - [`spec`] - Static per-method metadata ([`Spec`], [`StreamType`])
75//! - [`payload`] - Lazily-decoded, type-erased message bodies ([`Payload`])
76//! - [`interceptor`] - RPC-level interceptors ([`Interceptor`], [`Next`])
77//! - [`deadline`] - Server-side deadline moderation ([`DeadlinePolicy`])
78//! - [`protocol`] - Protocol detection ([`Protocol`]: Connect, gRPC, gRPC-Web)
79//! - [`client`] - Tower-based HTTP client utilities (transports require the `client` feature)
80//! - [`server`] - Standalone hyper-based server (requires `server` feature)
81//!
82//! # Protocol Support
83//!
84//! Servers speak the [Connect protocol](https://connectrpc.com/docs/protocol),
85//! gRPC, and gRPC-Web from a single registration; clients can be configured
86//! for any of the three:
87//! - All four RPC shapes: unary, server-streaming, client-streaming, bidi
88//! (full-duplex bidi requires HTTP/2; browsers additionally cannot
89//! stream request bodies, regardless of protocol)
90//! - Proto and JSON message encoding
91//! - Compression negotiation (gzip, zstd) with streaming support
92//! - Error handling with proper HTTP status mapping
93//! - Trailers via `trailer-` prefixed headers
94//! - Envelope framing for streaming messages
95//! - Deadline propagation and server-side deadline moderation
96//!
97//! # Client
98//!
99//! Enable the `client` feature and use generated clients with a transport.
100//!
101//! **For gRPC** (HTTP/2), use [`Http2Connection`](client::Http2Connection):
102//!
103//! ```rust,ignore
104//! use connectrpc::client::{Http2Connection, ClientConfig};
105//! use connectrpc::Protocol;
106//!
107//! let uri: http::Uri = "http://localhost:8080".parse()?;
108//! let conn = Http2Connection::connect_plaintext(uri.clone()).await?.shared(1024);
109//! let config = ClientConfig::new(uri).with_protocol(Protocol::Grpc);
110//!
111//! let client = GreetServiceClient::new(conn, config);
112//! let response = client.greet(request).await?;
113//! ```
114//!
115//! **For Connect over HTTP/1.1** (or unknown protocol), use
116//! [`HttpClient`](client::HttpClient):
117//!
118//! ```rust,ignore
119//! use connectrpc::client::{HttpClient, ClientConfig};
120//!
121//! let http = HttpClient::plaintext(); // cleartext http:// only
122//! let config = ClientConfig::new("http://localhost:8080".parse()?);
123//!
124//! let client = GreetServiceClient::new(http, config);
125//! ```
126//!
127//! ## Per-call options and defaults
128//!
129//! Generated clients expose both `foo(req)` and `foo_with_options(req, opts)`
130//! for each RPC. Use [`CallOptions`](client::CallOptions) for per-call timeout,
131//! headers, message-size limits, and compression overrides.
132//!
133//! For settings you want on every call, configure [`ClientConfig`](client::ClientConfig)
134//! defaults — they're applied automatically by the no-options method:
135//!
136//! ```rust,ignore
137//! let config = ClientConfig::new(uri)
138//! .with_default_timeout(Duration::from_secs(30))
139//! .with_default_header("authorization", "Bearer ...");
140//!
141//! let client = GreetServiceClient::new(http, config);
142//! client.greet(req).await?; // uses 30s timeout + auth header
143//! ```
144//!
145//! Per-call `CallOptions` override config defaults.
146//!
147//! See the [`client`] module docs for connection balancing and the
148//! transport selection rationale.
149//!
150//! # Feature Flags
151//!
152//! | Feature | Default | Description |
153//! |---------|---------|-------------|
154//! | `json` | ✓ | JSON codec for protobuf messages; disable for proto-only builds |
155//! | `gzip` | ✓ | Gzip compression |
156//! | `zstd` | ✓ | Zstandard compression |
157//! | `streaming` | ✓ | Streaming compression support |
158//! | `client` | ✗ | HTTP client transports (plaintext) |
159//! | `client-tls` | ✗ | TLS for client transports |
160//! | `server` | ✗ | Standalone hyper-based server |
161//! | `server-tls` | ✗ | TLS for the built-in server |
162//! | `tls` | ✗ | Convenience: `server-tls` + `client-tls` |
163//! | `axum` | ✗ | Axum framework integration |
164
165#![deny(unsafe_code)]
166#![warn(missing_docs)]
167#![cfg_attr(docsrs, feature(doc_cfg))]
168
169/// Spawn a detached background future on the ambient executor.
170///
171/// On native targets this dispatches via [`tokio::spawn`] and returns the join
172/// handle. On `wasm32` there is no tokio runtime, so the future is dispatched
173/// via [`wasm_bindgen_futures::spawn_local`] and `None` is returned (no
174/// joinable handle available).
175///
176/// The `Send` bound is required on native (`tokio::spawn`) but relaxed on
177/// wasm32 (`spawn_local` is single-threaded).
178#[cfg(not(target_arch = "wasm32"))]
179pub(crate) fn spawn_detached<F>(future: F) -> Option<tokio::task::JoinHandle<()>>
180where
181 F: std::future::Future<Output = ()> + Send + 'static,
182{
183 Some(tokio::spawn(future))
184}
185
186/// wasm32 variant — see non-wasm docs above.
187#[cfg(target_arch = "wasm32")]
188pub(crate) fn spawn_detached<F>(future: F) -> Option<tokio::task::JoinHandle<()>>
189where
190 F: std::future::Future<Output = ()> + 'static,
191{
192 wasm_bindgen_futures::spawn_local(future);
193 None
194}
195
196// Core modules (always available)
197pub mod codec;
198pub mod compression;
199pub mod deadline;
200pub mod dispatcher;
201pub mod envelope;
202pub mod error;
203pub(crate) mod grpc_status;
204pub mod handler;
205pub mod interceptor;
206pub mod payload;
207pub mod protocol;
208pub mod request;
209pub mod response;
210pub mod router;
211pub mod service;
212pub mod spec;
213pub mod stream_message;
214
215// Optional: HTTP client
216pub mod client;
217
218// Optional: Standalone hyper-based server
219#[cfg(feature = "server")]
220#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
221pub mod server;
222
223// Optional: TLS-aware `axum::serve` counterpart with peer-identity passthrough.
224//
225// Note: this module shadows the extern-prelude `axum` crate within the crate
226// root scope only. Don't add `use axum::...` here in `lib.rs`; use
227// `::axum::...` if a root-level reference to the external crate is ever needed.
228#[cfg(all(feature = "axum", feature = "server-tls"))]
229#[cfg_attr(docsrs, doc(cfg(all(feature = "axum", feature = "server-tls"))))]
230pub mod axum;
231
232// ============================================================================
233// Primary exports - Tower-first API
234// ============================================================================
235
236// The main entry point - a tower::Service for ConnectRPC
237pub use service::ConnectRpcBody;
238pub use service::ConnectRpcService;
239pub use service::Limits;
240pub use service::StreamingResponseBody;
241
242// Router for registering RPC handlers
243pub use router::MethodKind;
244pub use router::Router;
245pub use router::RouterMergeError;
246pub use router::ServiceRegister;
247pub use router::merge_routers;
248
249// Dispatcher trait for monomorphic dispatch (codegen-backed alternative to Router)
250pub use dispatcher::Chain;
251pub use dispatcher::Dispatcher;
252pub use dispatcher::MethodDescriptor;
253
254// Handler traits and request/response types
255pub use handler::BidiStreamingHandler;
256pub use handler::ClientStreamingHandler;
257pub use handler::Handler;
258pub use handler::StreamingHandler;
259pub use handler::ViewBidiStreamingHandler;
260pub use handler::ViewClientStreamingHandler;
261pub use handler::ViewHandler;
262pub use handler::ViewStreamingHandler;
263pub use handler::bidi_streaming_handler_fn;
264pub use handler::client_streaming_handler_fn;
265pub use handler::handler_fn;
266pub use handler::streaming_handler_fn;
267pub use handler::view_bidi_streaming_handler_fn;
268pub use handler::view_client_streaming_handler_fn;
269pub use handler::view_handler_fn;
270pub use handler::view_streaming_handler_fn;
271pub use request::HasMessageView;
272pub use request::ServiceRequest;
273pub use response::Encodable;
274pub use response::EncodedResponse;
275pub use response::InboundStream;
276pub use response::MaybeBorrowed;
277pub use response::PreEncoded;
278pub use response::RequestContext;
279pub use response::Response;
280pub use response::ServiceResult;
281pub use response::ServiceStream;
282pub use stream_message::StreamMessage;
283
284/// Re-exports for generated code. Not part of the public API; subject
285/// to change without a semver bump.
286#[doc(hidden)]
287pub mod __codegen {
288 pub use crate::response::encode_view_body;
289}
290
291// Error types
292pub use error::ConnectError;
293pub use error::ErrorCode;
294pub use error::ErrorDetail;
295
296/// Re-export of the `http-body` crate whose [`Body`](http_body::Body) trait
297/// appears in generated client bounds — so consumers don't need their own
298/// `http-body` dependency to use generated code.
299pub use http_body;
300
301// Protocol detection
302pub use protocol::Protocol;
303pub use protocol::RequestProtocol;
304
305// Static method metadata
306pub use spec::IdempotencyLevel;
307pub use spec::Spec;
308pub use spec::SpecOrigin;
309pub use spec::StreamType;
310
311// Type-erased message bodies for interceptors
312pub use interceptor::async_trait;
313pub use payload::AnyMessage;
314pub use payload::Payload;
315
316// RPC interceptors (unary and streaming). The wire-level request/response
317// aliases (interceptor::UnaryRequest and friends) stay module-scoped: at the
318// crate root those names belong to the far more common client-facing types
319// below.
320pub use interceptor::Interceptor;
321pub use interceptor::Next;
322pub use interceptor::NextStream;
323pub use interceptor::PayloadStream;
324pub use interceptor::streaming_interceptor;
325pub use interceptor::unary_interceptor;
326
327// Client response and stream handles (what generated client methods return)
328pub use client::BidiStream;
329pub use client::ServerStream;
330pub use client::UnaryResponse;
331
332// ============================================================================
333// Codec exports
334// ============================================================================
335
336pub use codec::CodecFormat;
337#[cfg(feature = "json")]
338#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
339pub use codec::JsonCodec;
340pub use codec::JsonDeserialize;
341pub use codec::JsonSerialize;
342pub use codec::ProtoCodec;
343
344// ============================================================================
345// Compression exports
346// ============================================================================
347
348pub use compression::CompressionPolicy;
349pub use compression::CompressionProvider;
350pub use compression::CompressionRegistry;
351pub use compression::DEFAULT_COMPRESSION_MIN_SIZE;
352
353// ============================================================================
354// Deadline exports
355// ============================================================================
356
357pub use deadline::DeadlinePolicy;
358
359#[cfg(feature = "gzip")]
360#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
361pub use compression::GzipProvider;
362
363#[cfg(feature = "zstd")]
364#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
365pub use compression::ZstdProvider;
366
367#[cfg(feature = "streaming")]
368#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
369pub use compression::BoxedAsyncBufRead;
370
371#[cfg(feature = "streaming")]
372#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
373pub use compression::BoxedAsyncRead;
374
375#[cfg(feature = "streaming")]
376#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
377pub use compression::StreamingCompressionProvider;
378
379// ============================================================================
380// Optional: Standalone server
381// ============================================================================
382
383#[cfg(feature = "server")]
384#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
385pub use server::BoundServer;
386
387#[cfg(feature = "server")]
388#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
389pub use server::Server;
390
391#[cfg(feature = "server")]
392#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
393pub use server::PeerAddr;
394#[cfg(feature = "server-tls")]
395#[cfg_attr(docsrs, doc(cfg(feature = "server-tls")))]
396pub use server::PeerCerts;
397
398/// Re-export of `rustls` for TLS configuration.
399///
400/// Use this to construct a [`rustls::ServerConfig`] for [`Server::with_tls`]
401/// or a [`rustls::ClientConfig`] for [`HttpClient::with_tls`](client::HttpClient::with_tls)
402/// / [`Http2Connection::connect_tls`](client::Http2Connection::connect_tls).
403#[cfg(any(feature = "server-tls", feature = "client-tls"))]
404#[cfg_attr(docsrs, doc(cfg(any(feature = "server-tls", feature = "client-tls"))))]
405pub use rustls;
406
407/// Include the generated ConnectRPC file from `$OUT_DIR`.
408///
409/// Shorthand for `include!(concat!(env!("OUT_DIR"), "/_connectrpc.rs"))`.
410///
411/// Requires `Config::include_file` in `build.rs` (the no-arg form assumes
412/// the filename `"_connectrpc.rs"`):
413///
414/// ```rust,ignore
415/// // build.rs
416/// connectrpc_build::Config::new()
417/// .files(&["proto/my_service.proto"])
418/// .includes(&["proto/"])
419/// .include_file("_connectrpc.rs")
420/// .compile()
421/// .unwrap();
422/// ```
423///
424/// ```rust,ignore
425/// // src/lib.rs
426/// pub mod proto {
427/// connectrpc::include_generated!();
428/// }
429/// ```
430///
431/// `OUT_DIR` is resolved in the **calling crate's** compilation context.
432///
433/// If you customised the output filename via `Config::include_file`, pass the
434/// **filename** (including the `.rs` extension) as a string literal. Unlike
435/// `tonic::include_proto!`, this argument is a filename, not a proto package
436/// name:
437///
438/// ```rust,ignore
439/// pub mod proto {
440/// connectrpc::include_generated!("my_output.rs");
441/// }
442/// ```
443///
444/// # Notes
445///
446/// - This macro is only for the `build.rs`/`OUT_DIR` workflow. If you use
447/// `buf generate` to write files into `src/generated/`, use `#[path]`:
448///
449/// ```rust,ignore
450/// #[path = "generated/proto/mod.rs"]
451/// pub mod proto;
452/// ```
453///
454/// - If `Config::out_dir` was used to redirect output away from `$OUT_DIR`,
455/// this macro does not apply; use `#[path]` or raw `include!` instead.
456///
457/// - If your proto package hierarchy contains a module named `connectrpc`,
458/// the crate name may be shadowed in scope. Use the absolute path to avoid
459/// the ambiguity:
460///
461/// ```rust,ignore
462/// mod proto {
463/// ::connectrpc::include_generated!();
464/// }
465/// ```
466///
467/// # Compile errors
468///
469/// This macro produces a compile error (not a runtime panic) if:
470///
471/// - `OUT_DIR` is not set — the crate is not being built by Cargo.
472/// - The generated file does not exist — `Config::include_file` was not
473/// called in `build.rs`, or the filename passed to the one-arg form does
474/// not match what was passed to `Config::include_file`.
475#[macro_export]
476macro_rules! include_generated {
477 () => {
478 include!(concat!(env!("OUT_DIR"), "/_connectrpc.rs"));
479 };
480 ($file:literal) => {
481 include!(concat!(env!("OUT_DIR"), "/", $file));
482 };
483}