Skip to main content

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 = greet_impl.register(Router::new());
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 = greet_impl.register(ConnectRouter::new());
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//! - [`router`] - Request routing and service registration
69//! - [`service`] - Tower service implementation (primary integration point)
70//! - [`client`] - Tower-based HTTP client utilities (requires `client` feature)
71//! - [`server`] - Standalone hyper-based server (requires `server` feature)
72//!
73//! # Protocol Support
74//!
75//! This implementation follows the ConnectRPC protocol specification:
76//! - Unary RPC calls (request-response)
77//! - Proto and JSON message encoding
78//! - Compression negotiation (gzip, zstd) with streaming support
79//! - Error handling with proper HTTP status mapping
80//! - Trailers via `trailer-` prefixed headers
81//! - Envelope framing for streaming messages
82//!
83//! # Client
84//!
85//! Enable the `client` feature and use generated clients with a transport.
86//!
87//! **For gRPC** (HTTP/2), use [`Http2Connection`](client::Http2Connection):
88//!
89//! ```rust,ignore
90//! use connectrpc::client::{Http2Connection, ClientConfig};
91//! use connectrpc::Protocol;
92//!
93//! let uri: http::Uri = "http://localhost:8080".parse()?;
94//! let conn = Http2Connection::connect_plaintext(uri.clone()).await?.shared(1024);
95//! let config = ClientConfig::new(uri).protocol(Protocol::Grpc);
96//!
97//! let client = GreetServiceClient::new(conn, config);
98//! let response = client.greet(request).await?;
99//! ```
100//!
101//! **For Connect over HTTP/1.1** (or unknown protocol), use
102//! [`HttpClient`](client::HttpClient):
103//!
104//! ```rust,ignore
105//! use connectrpc::client::{HttpClient, ClientConfig};
106//!
107//! let http = HttpClient::plaintext();  // cleartext http:// only
108//! let config = ClientConfig::new("http://localhost:8080".parse()?);
109//!
110//! let client = GreetServiceClient::new(http, config);
111//! ```
112//!
113//! ## Per-call options and defaults
114//!
115//! Generated clients expose both `foo(req)` and `foo_with_options(req, opts)`
116//! for each RPC. Use [`CallOptions`](client::CallOptions) for per-call timeout,
117//! headers, message-size limits, and compression overrides.
118//!
119//! For settings you want on every call, configure [`ClientConfig`](client::ClientConfig)
120//! defaults — they're applied automatically by the no-options method:
121//!
122//! ```rust,ignore
123//! let config = ClientConfig::new(uri)
124//!     .default_timeout(Duration::from_secs(30))
125//!     .default_header("authorization", "Bearer ...");
126//!
127//! let client = GreetServiceClient::new(http, config);
128//! client.greet(req).await?;  // uses 30s timeout + auth header
129//! ```
130//!
131//! Per-call `CallOptions` override config defaults.
132//!
133//! See the [`client`] module docs for connection balancing and the
134//! transport selection rationale.
135//!
136//! # Feature Flags
137//!
138//! | Feature | Default | Description |
139//! |---------|---------|-------------|
140//! | `gzip` | ✓ | Gzip compression |
141//! | `zstd` | ✓ | Zstandard compression |
142//! | `streaming` | ✓ | Streaming compression support |
143//! | `client` | ✗ | HTTP client transports (plaintext) |
144//! | `client-tls` | ✗ | TLS for client transports |
145//! | `server` | ✗ | Standalone hyper-based server |
146//! | `server-tls` | ✗ | TLS for the built-in server |
147//! | `tls` | ✗ | Convenience: `server-tls` + `client-tls` |
148//! | `axum` | ✗ | Axum framework integration |
149
150#![deny(unsafe_code)]
151#![warn(missing_docs)]
152#![cfg_attr(docsrs, feature(doc_cfg))]
153
154/// Spawn a detached background future on the ambient executor.
155///
156/// On native targets this dispatches via [`tokio::spawn`] and returns the join
157/// handle. On `wasm32` there is no tokio runtime, so the future is dispatched
158/// via [`wasm_bindgen_futures::spawn_local`] and `None` is returned (no
159/// joinable handle available).
160///
161/// The `Send` bound is required on native (`tokio::spawn`) but relaxed on
162/// wasm32 (`spawn_local` is single-threaded).
163#[cfg(not(target_arch = "wasm32"))]
164pub(crate) fn spawn_detached<F>(future: F) -> Option<tokio::task::JoinHandle<()>>
165where
166    F: std::future::Future<Output = ()> + Send + 'static,
167{
168    Some(tokio::spawn(future))
169}
170
171/// wasm32 variant — see non-wasm docs above.
172#[cfg(target_arch = "wasm32")]
173pub(crate) fn spawn_detached<F>(future: F) -> Option<tokio::task::JoinHandle<()>>
174where
175    F: std::future::Future<Output = ()> + 'static,
176{
177    wasm_bindgen_futures::spawn_local(future);
178    None
179}
180
181// Core modules (always available)
182pub mod codec;
183pub mod compression;
184pub mod dispatcher;
185pub mod envelope;
186pub mod error;
187pub(crate) mod grpc_status;
188pub mod handler;
189pub mod protocol;
190pub mod response;
191pub mod router;
192pub mod service;
193
194// Optional: HTTP client
195pub mod client;
196
197// Optional: Standalone hyper-based server
198#[cfg(feature = "server")]
199pub mod server;
200
201// Optional: TLS-aware `axum::serve` counterpart with peer-identity passthrough.
202//
203// Note: this module shadows the extern-prelude `axum` crate within the crate
204// root scope only. Don't add `use axum::...` here in `lib.rs`; use
205// `::axum::...` if a root-level reference to the external crate is ever needed.
206#[cfg(all(feature = "axum", feature = "server-tls"))]
207#[cfg_attr(docsrs, doc(cfg(all(feature = "axum", feature = "server-tls"))))]
208pub mod axum;
209
210// ============================================================================
211// Primary exports - Tower-first API
212// ============================================================================
213
214// The main entry point - a tower::Service for ConnectRPC
215pub use service::ConnectRpcBody;
216pub use service::ConnectRpcService;
217pub use service::Limits;
218pub use service::StreamingResponseBody;
219
220// Router for registering RPC handlers
221pub use router::MethodKind;
222pub use router::Router;
223
224// Dispatcher trait for monomorphic dispatch (codegen-backed alternative to Router)
225pub use dispatcher::Chain;
226pub use dispatcher::Dispatcher;
227pub use dispatcher::MethodDescriptor;
228
229// Handler traits and request/response types
230pub use handler::BidiStreamingHandler;
231pub use handler::ClientStreamingHandler;
232pub use handler::Handler;
233pub use handler::StreamingHandler;
234pub use handler::ViewBidiStreamingHandler;
235pub use handler::ViewClientStreamingHandler;
236pub use handler::ViewHandler;
237pub use handler::ViewStreamingHandler;
238pub use handler::bidi_streaming_handler_fn;
239pub use handler::client_streaming_handler_fn;
240pub use handler::handler_fn;
241pub use handler::streaming_handler_fn;
242pub use handler::view_bidi_streaming_handler_fn;
243pub use handler::view_client_streaming_handler_fn;
244pub use handler::view_handler_fn;
245pub use handler::view_streaming_handler_fn;
246pub use response::Encodable;
247pub use response::EncodedResponse;
248pub use response::MaybeBorrowed;
249pub use response::RequestContext;
250pub use response::Response;
251pub use response::ServiceResult;
252pub use response::ServiceStream;
253
254/// Re-exports for generated code. Not part of the public API; subject
255/// to change without a semver bump.
256#[doc(hidden)]
257pub mod __codegen {
258    pub use crate::response::encode_view_body;
259}
260
261// Error types
262pub use error::ConnectError;
263pub use error::ErrorCode;
264
265// Protocol detection
266pub use protocol::Protocol;
267pub use protocol::RequestProtocol;
268
269// ============================================================================
270// Codec exports
271// ============================================================================
272
273pub use codec::CodecFormat;
274pub use codec::JsonCodec;
275pub use codec::ProtoCodec;
276
277// ============================================================================
278// Compression exports
279// ============================================================================
280
281pub use compression::CompressionPolicy;
282pub use compression::CompressionProvider;
283pub use compression::CompressionRegistry;
284pub use compression::DEFAULT_COMPRESSION_MIN_SIZE;
285
286#[cfg(feature = "gzip")]
287pub use compression::GzipProvider;
288
289#[cfg(feature = "zstd")]
290pub use compression::ZstdProvider;
291
292#[cfg(feature = "streaming")]
293pub use compression::BoxedAsyncBufRead;
294
295#[cfg(feature = "streaming")]
296pub use compression::BoxedAsyncRead;
297
298#[cfg(feature = "streaming")]
299pub use compression::StreamingCompressionProvider;
300
301// ============================================================================
302// Optional: Standalone server
303// ============================================================================
304
305#[cfg(feature = "server")]
306pub use server::BoundServer;
307
308#[cfg(feature = "server")]
309pub use server::Server;
310
311#[cfg(feature = "server")]
312pub use server::PeerAddr;
313#[cfg(feature = "server-tls")]
314pub use server::PeerCerts;
315
316/// Re-export of `rustls` for TLS configuration.
317///
318/// Use this to construct a [`rustls::ServerConfig`] for [`Server::with_tls`]
319/// or a [`rustls::ClientConfig`] for [`HttpClient::with_tls`](client::HttpClient::with_tls)
320/// / [`Http2Connection::connect_tls`](client::Http2Connection::connect_tls).
321#[cfg(any(feature = "server-tls", feature = "client-tls"))]
322pub use rustls;
323
324/// Include the generated ConnectRPC file from `$OUT_DIR`.
325///
326/// Shorthand for `include!(concat!(env!("OUT_DIR"), "/_connectrpc.rs"))`.
327///
328/// Requires `Config::include_file` in `build.rs` (the no-arg form assumes
329/// the filename `"_connectrpc.rs"`):
330///
331/// ```rust,ignore
332/// // build.rs
333/// connectrpc_build::Config::new()
334///     .files(&["proto/my_service.proto"])
335///     .includes(&["proto/"])
336///     .include_file("_connectrpc.rs")
337///     .compile()
338///     .unwrap();
339/// ```
340///
341/// ```rust,ignore
342/// // src/lib.rs
343/// pub mod proto {
344///     connectrpc::include_generated!();
345/// }
346/// ```
347///
348/// `OUT_DIR` is resolved in the **calling crate's** compilation context.
349///
350/// If you customised the output filename via `Config::include_file`, pass the
351/// **filename** (including the `.rs` extension) as a string literal. Unlike
352/// `tonic::include_proto!`, this argument is a filename, not a proto package
353/// name:
354///
355/// ```rust,ignore
356/// pub mod proto {
357///     connectrpc::include_generated!("my_output.rs");
358/// }
359/// ```
360///
361/// # Notes
362///
363/// - This macro is only for the `build.rs`/`OUT_DIR` workflow. If you use
364///   `buf generate` to write files into `src/generated/`, use `#[path]`:
365///
366///   ```rust,ignore
367///   #[path = "generated/proto/mod.rs"]
368///   pub mod proto;
369///   ```
370///
371/// - If `Config::out_dir` was used to redirect output away from `$OUT_DIR`,
372///   this macro does not apply; use `#[path]` or raw `include!` instead.
373///
374/// - If your proto package hierarchy contains a module named `connectrpc`,
375///   the crate name may be shadowed in scope. Use the absolute path to avoid
376///   the ambiguity:
377///
378///   ```rust,ignore
379///   mod proto {
380///       ::connectrpc::include_generated!();
381///   }
382///   ```
383///
384/// # Compile errors
385///
386/// This macro produces a compile error (not a runtime panic) if:
387///
388/// - `OUT_DIR` is not set — the crate is not being built by Cargo.
389/// - The generated file does not exist — `Config::include_file` was not
390///   called in `build.rs`, or the filename passed to the one-arg form does
391///   not match what was passed to `Config::include_file`.
392#[macro_export]
393macro_rules! include_generated {
394    () => {
395        include!(concat!(env!("OUT_DIR"), "/_connectrpc.rs"));
396    };
397    ($file:literal) => {
398        include!(concat!(env!("OUT_DIR"), "/", $file));
399    };
400}