All notable changes to connectrpc will be documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning with the Rust 0.x convention: breaking changes increment the minor version (0.2 → 0.3), additive changes increment the patch version.
- Generated service code now compiles when multiple services are
include!d into the same Rust module (#32). The codegen previously emitted top-levelusestatements that collided with E0252 when buffa-packaging's flat-output strategy concatenated several service files into one module. Bindings now use fully-qualified paths throughout (::connectrpc::Context,::buffa::view::OwnedView,::http_body::Body, etc.), so multiple service files can coexist in the samemodblock.
- Generated client methods reference the per-service
*_SERVICE_NAMEconst (#16) instead of repeating the fully-qualified service name as a string literal at every call site. Matches the server-side router. - Workspace
tokiofeature footprint narrowed (#19). The publishedconnectrpccrate previously inherited the full workspace tokio feature set (macros,net,signal,rt-multi-thread, ...) whenworkspace = truewas inlined at publish time. It now requests onlyrt,io-util,sync,time, plusnetwhen theclientorserverfeature is enabled. Downstream crates that usetokiodirectly should declare their own features rather than relying on transitive activation. - Workspace dependency updates (#37).
wasm32-unknown-unknowntarget compatibility (#19) for theconnectrpccrate with default features off. A newexamples/wasm-clientdemonstrates a Fetch-basedClientTransportimplementation with browser-based integration tests viawasm-pack. Currently exercises unary calls without deadlines; timeouts and streaming require additional setup beyond the example.
emit_register_fnoption (#35) onconnectrpc_codegen::codegen::Optionsandconnectrpc_build::Config, plumbing through tobuffa_codegen::CodeGenConfig::emit_register_fn. Set tofalseto suppress the per-fileregister_types(&mut TypeRegistry)aggregator when multiple generated files areinclude!d into the same module (the identically-named functions would otherwise collide). The protoc plugin accepts a matchingno_register_fnparameter for path-compat with the unifiedconnectrpc-buildflow.
- Upgraded
buffato 0.3.0 (#24). buffa 0.3 renamesAnyRegistrytoTypeRegistry(withJsonAnyEntryandregister_json_any()replacing the oldAnyTypeEntry/register()). Generated code and the runtime crate now use the new types; users who construct a registry manually forgoogle.protobuf.AnyJSON encoding will need to migrate. connectrpc-buildonly rewrites output files when content changes (#22). Preserves mtimes so touching one.protono longer triggers a full downstream recompile of every generated.rsfile. Mirrors prost-build'swrite_file_if_changed.
- mTLS peer credentials and remote address are now available to handlers
(#31) via
Context::extensions. The built-in server insertsPeerAddr(always) andPeerCerts(whenserver-tlsis enabled and the client presented a certificate chain) into every request's extensions; handlers read them withctx.extensions.get::<PeerAddr>()/ctx.extensions.get::<PeerCerts>(). Custom HTTP stacks (axum, raw hyper) can insert the same types from a tower layer so handler code stays transport-agnostic. Server::from_listener(TcpListener)(#31) wraps a pre-bound listener, allowing socket options (IPV6_V6ONLY=falsefor dual-stack,SO_REUSEPORT, inherited file descriptors) to be configured before handing the listener to connectrpc.Http2Connection::lazy_with_connector/connect_with_connector(#15) as the generic transport escape hatch — supply anytower::Service<Uri>yielding ahyper::rt::Read + Writestream and the library runs the h2 handshake over it.lazy_unix/connect_unixare thin wrappers for Unix domain sockets.- Codegen now rejects RPC method names that collide after
to_snake_case(#28).rpc GetFoo(...)andrpc get_foo(...)in the same service previously emitted duplicatefn get_fooand failed with a rustc error pointing at generated code; the build script now fails with a clear error naming both proto methods. Also catches a method whose name collides with another's_with_optionsclient variant.
- RPC methods whose snake_case names are Rust keywords now generate valid
code (#23, #26).
rpc Move(...)previously emittedfn move(...)and failed at build-script time. Method idents are now routed through buffa's keyword escaper, producingr#move(or a_suffix for the four keywords that cannot be raw identifiers). service Self {}no longer generatestrait Self(#27). The handler trait is suffixed toSelf_; theSelfExt/SelfClient/SelfServerderivatives are unaffected since the suffix already de-keywords them.
BidiStreamhalf-duplex deadlock onSharedHttp2Connection(#2, #4).call_bidi_streamstored the transport'ssend()future unpolled, so for transports where that future contains the connect/handshake/stream work (i.e. not hyper's pooled client), the HTTP request never initiated until the firstmessage()call. The half-duplex pattern (send all, close, then read) would buffer into the 32-deepChannelBodympsc with nobody draining it and deadlock on the 33rd send. The send future is now spawned so the request streams immediately.- TLS connections to IPv6 literal URIs failed (#1, #3).
Uri::host()returns[::1]with brackets, whichrustls_pki_types::ServerNamerejected as an invalid DNS name. Brackets are now stripped so the address parses asServerName::IpAddress. - README required-dependencies example showed
buffa = "0.1"instead of"0.2". Theconnectrpccrate bakes the workspace README viareadme = "../README.md", so the crates.io page for 0.2.0 shows the stale version; this release updates it.
First release from the anthropics/connect-rust
repository. This is a complete from-scratch implementation — not a continuation
of the 0.1.x releases previously published under the connectrpc crate name,
which have been superseded.
| Protocol | Server | Client |
|---|---|---|
| Connect (unary + streaming) | ✅ | ✅ |
| Connect GET (idempotent unary via query string) | ✅ | ✅ |
| gRPC over HTTP/2 | ✅ | ✅ |
| gRPC-Web | ✅ | ✅ |
| RPC type | Server | Client |
|---|---|---|
| Unary | ✅ | ✅ |
| Server streaming | ✅ | ✅ |
| Client streaming | ✅ | ✅ |
| Bidirectional streaming (full-duplex on h2, half-duplex on h1/h2) | ✅ | ✅ |
All applicable ConnectRPC conformance features are enabled. Test counts:
| Suite | Tests |
|---|---|
| Server (default) | 3600 |
| Server Connect+TLS (incl. mTLS) | 2396 |
| Client Connect (incl. GET, bidi, zstd, mTLS, h1 half-duplex) | 2580 |
| Client gRPC | 1454 |
| Client gRPC-Web | 2838 |
Runtime
- Tower-based
ConnectRpcService<D>— framework-agnostic, works with Axum, Hyper, etc. - Monomorphic
FooServiceServer<T>dispatcher (compile-time method dispatch, nodyn Handlervtable) - Dynamic
Routerwith runtime registration for multi-service or reflection use cases - Pluggable compression via
CompressionProvidertrait; gzip + zstd built-in #![deny(unsafe_code)],#![warn(missing_docs)]
Client transports (feature = client)
HttpClient::plaintext()/::with_tls()— pooled hyper client, HTTP/1.1 + HTTP/2 via ALPNHttp2Connection::connect_plaintext()/::connect_tls()— single raw h2 connection with honestpoll_ready, composes withtower::balancefor N-connection load spreading- Security-first naming: no bare
::new()— plaintext vs TLS is an explicit choice - TLS accepts
Arc<rustls::ClientConfig>, preserving dynamic cert rotation throughArc<dyn ResolvesClientCert> - Whole-call deadline enforcement via
tokio::time::timeout_at(gRPC semantics: deadline applies to the entire call, not per-message)
Server (feature = server)
Server::with_tls(Arc<rustls::ServerConfig>)— mTLS viawith_client_cert_verifier()- Graceful shutdown with connection draining
Generated clients
- Dual methods per RPC:
foo(req)(uses config defaults) +foo_with_options(req, opts) ClientConfigcarries defaults for timeout, max message size, and headers — applied automatically by the no-options method
- Message size limits enforced on both sides. Request body collection, response body collection, envelope decoding, and decompression all apply configurable size limits, preventing either a malicious client or server from forcing unbounded memory allocation via oversized payloads or compression bombs.
- Both client and server default to 4 MiB per message
(
DEFAULT_MAX_MESSAGE_SIZE) when no explicit limit is configured — matching connect-go. Server: raise viaLimits::max_message_size. Client: raise viaClientConfig::default_max_message_sizeorCallOptions::max_message_size. - TLS handshake timeout. The server disconnects clients that open a TCP
connection but stall the TLS handshake, preventing slowloris-style connection
exhaustion. Defaults to 10 seconds (
DEFAULT_TLS_HANDSHAKE_TIMEOUT); configure viaServer::tls_handshake_timeout. - Timeout header digit-limit enforcement. Per spec,
connect-timeout-msis capped at 10 digits andgrpc-timeoutat 8 digits (matching connect-go). Over-spec values are treated as no-timeout. Prevents a malicious client from triggering a per-request panic viaInstant + Durationoverflow. Deadline computation also useschecked_addas defense in depth.
connectrpc-codegen— descriptor → Rust source libraryconnectrpc-build—build.rsintegration (protoc/buf → codegen →OUT_DIR)protoc-gen-connect-rust— protoc plugin binary
Generated code emits service traits, FooServiceServer<T> monomorphic dispatchers,
FooServiceClient<T> clients, and buffa message types via buffa-codegen.
- gRPC server reflection
- HTTP/3 (blocked on hyper support)
vs tonic 0.14 (same hyper/h2 stack), Intel Xeon 8488C:
- 1.95× faster on small unary (single-request latency, no contention)
- 1.74× faster on decode-heavy log ingest (50 records, ~15 KB)
- ~4% ahead on realistic fortune+valkey workload (c=256)
The advantage comes from buffa's zero-copy view types (borrowed string fields
directly from the request buffer, no per-string alloc; MapView as flat
Vec<(K,V)> with no hashing) and compile-time dispatch via the generated
FooServiceServer<T>. See README for the full CPU breakdown.