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.
A breaking release that reworks both ends of the message surface:
handlers take borrowed request views built on buffa 0.7.0, and client
streams surface terminal RPC errors from message() instead of hiding
them behind Ok(None). It is also the first release of two new crates
that join the lockstep versioning scheme at 0.7.0: connectrpc-health
(the standard gRPC health-checking service) and connectrpc-reflection
(gRPC server reflection).
On the server side, buffa 0.7.0 removed OwnedView's Deref impl (the
impl let safe code hold view fields past the backing buffer's lifetime),
and handlers move from owned OwnedView<FooView<'static>> parameters to
borrowed requests and owned stream items with per-field accessors.
Consumers with checked-in protoc-gen-connect-rust output must
regenerate with this release's toolchain and buffa ≥ 0.7.0.
-
Unary and server-streaming handlers take
ServiceRequest<'_, Req>(#143). The request is borrowed from the dispatcher-owned body for the duration of the call: it Derefs to the request view for zero-copy field access, and offersto_owned_message()(zero-copy from the retained body bytes),to_owned_view()(a'staticview for pass-through responses),bytes(), andview(). The borrow may be held across.awaitpoints; the response — and anything moved intotokio::spawn— cannot borrow from it (enforced by the compiler). Existing handler impls must update their signatures; bodies that already started with.to_owned_message()work unchanged:// before (0.6) async fn greet(&self, ctx: RequestContext, request: OwnedView<GreetRequestView<'static>>) -> ServiceResult<GreetResponse> // after (0.7) async fn greet(&self, ctx: RequestContext, request: ServiceRequest<'_, GreetRequest>) -> ServiceResult<GreetResponse>
-
Client-streaming and bidi inbound items are
StreamMessage<Req>(#143). Each item owns its decoded buffer, isSend + 'static, Derefs to the buffa-generatedFooOwnedViewwrapper for per-field accessor methods (item.name()), and re-encodes from the retained wire bytes when yielded back (StreamMessage<M>: Encodable<M>). -
UnaryResponse::view()returns the reborrowed view rather than a reference to theOwnedView, soresp.view().fieldworks directly on the client (#143). -
extern_pathtargets must be buffa ≥ 0.7.0 generated code with views enabled (#143). Request types resolved throughextern_path(e.g. well-known types frombuffa-types) use the sameServiceRequest/StreamMessagewrappers as local types, backed bybuffa::HasMessageViewimpls that buffa emits with each message.buffa-types0.7+ qualifies; a crate generated with older buffa or with views disabled fails to compile with a missingHasMessageViewimpl (on buffa 0.7.1+ the compile error itself explains the fix; 0.7.0 only names the missing impl). The output side is unchanged: view-bodyEncodableimpls are still not emitted for extern output types — return the owned message for those. -
The workspace requires
buffa/buffa-types/buffa-codegen0.7 (#143), which is also the regen baseline for checked-in generated code. -
Client streams return terminal server errors from
message()(#159). Previously, an RPC error carried in the stream's termination metadata (gRPC HTTP/2 trailers, gRPC-Web trailer frame, or Connect END_STREAM envelope) madeServerStream::message()/BidiStream::message()returnOk(None)— indistinguishable from a clean close — with the error retrievable only via the easy-to-misserror()accessor. A caller that treatedOk(None)as success would silently swallow every failed streaming RPC.message()now returnsErr(...)for an errored end and reservesOk(None)for a clean end (gRPC status OK / error-free END_STREAM), matchingtonic. EveryErris terminal and sticky — re-polling returns the same error, never a clean-lookingOk(None);error()andtrailers()remain populated for post-hoc inspection. Callers that only matchErrneed no changes; callers doing theOk(None)-then-error()dance can delete the dance:// Before: errors hid behind Ok(None) // After: `?` is complete while let Some(m) = s.message().await? { while let Some(m) = s.message().await? { handle(m); handle(m); } } if let Some(err) = s.error() { return Err(err.clone().into()); }
-
A gRPC/gRPC-Web stream that never delivers a usable
grpc-statusis now an error instead of a cleanOk(None)(#159):internalwhen no trailers arrived at all,unknownwhen trailers arrived without agrpc-status, andunknownfor a present-but-malformedgrpc-statusvalue — each matching grpc-go (and the conformance suite's primary expectations). A Trailers-Only response carryinggrpc-status: 0in the response headers (grpc-go's shape for an OK end with zero messages) remains a clean end. The Connect-protocol equivalent — a missing END_STREAM envelope reported asunavailable— ships in this release as #140 (see Fixed below); this entry covers only the gRPC and gRPC-Web protocols.
- New
connectrpc-healthcrate (#128) — the standardgrpc.health.v1.Healthservice for connectrpc routers, wire-compatible withgrpc_health_probe, kubeletgrpc:probes, and gRPC-aware service meshes (Linkerd, Istio).install_static(router, [names])mounts aStaticChecker-backed service in one call; implement theCheckertrait for custom logic (e.g. reportingNotServingwhile a dependency is down). The generatedHealthClientre-export is gated on the default-onclientfeature so server-only deployments can drop the client transport stack. See the health checking section of the guide. ServiceRequest<'a, Req>andStreamMessage<M>(#143) — the request wrappers described above, exported from the crate root.- The multiservice example gains a
Heartbeat(google.protobuf.Empty) → google.protobuf.TimestampRPC exercising well-known types as direct RPC input and output (#143). connectrpc_build::Config::emit_descriptor_set(name)(#141) — also write the inputFileDescriptorSet(full transitive import closure, regardless of descriptor source) to<out_dir>/<name>as wire-format bytes, ready toinclude_bytes!and serve viagrpc.reflection.v1.ServerReflection(e.g. forgrpcurl). The inverse ofConfig::descriptor_set, which reads a precompiled set; build scripts no longer need a secondprotoc --descriptor_set_outpass.- New
connectrpc-reflectioncrate (#157) — gRPC server reflection, wire-compatible withgrpc.reflection.v1.ServerReflectionand itsv1alphapredecessor, sogrpcurl,buf curl, Postman, andgrpcuiwork against connectrpc servers over gRPC, gRPC-Web, and Connect alike. Build aReflectorfromemit_descriptor_setoutput (responses carry the compiler's original per-file descriptor bytes) or adopt an existingbuffa_descriptor::DescriptorPool(e.g. thedescriptor_pool()accessor emitted by reflection-enabled buffa codegen);install(router, reflector)mounts both protocol versions.Reflector::with_servicescurates the advertised service list, mirroring Gogrpcreflect'sNamer. The service is self-describing: queries aboutgrpc.reflection.*fall back to the crate's own descriptors andListServicesadvertises the reflection services, matching grpc-go — no setup needed for schema-free clients likebuf curl. The multiservice example mounts it with both sources selectable viaREFLECTION_SOURCE=fds|pool;examples/multiservice/reflection-demo.shwalks through discovery and schema-free calls withbuf curl.
- The gRPC and gRPC-Web unary response parsers enforce the single-message rule before decompressing, and gRPC-Web parsing stops at the trailers frame (#147). A second data envelope is rejected before its payload is touched (matching the Connect client-streaming parser from #133), and a gRPC-Web response now completes as soon as a complete trailers frame is buffered instead of reading the body to EOF, so well-formed responses finish even if the server keeps writing.
- Connect streaming clients report an error when a response body ends
without the required END_STREAM envelope (#140).
message()now returnsErrwith codeunavailable(matching connect-go) instead of a cleanOk(None), so a stream cut off mid-response is no longer indistinguishable from a complete one. Streams that previously appeared to drain cleanly against a known-good server may now error — if you see this, suspect an intermediary (proxy or load balancer) stripping the trailing envelope. - The inter-message timeout no longer starts at response-stream
construction (#127).
DeadlinePolicy::with_inter_message_timeoutarmed its timer when the response stream was built, so the first measurement covered stream-setup latency (encoding, header writing, framework overhead) rather than the gap between messages, and a short timeout could fail the stream withdeadline_exceededbefore the consumer ever polled it. The timer now arms when the stream is first polled and re-arms after each yielded item, so setup latency before the first poll is excluded while a handler that stalls before its first item still times out.
- Malformed gzip and zstd compressed payloads now return
invalid_argumentinstead ofinternal(#139). For servers this attributes the failure to the sender and moves it out of 5xx metrics (the Connect HTTP status changes from 500 to 400) — update any alerting that keys on 5xx for these events. On the client, where the corrupt payload is a response, the error is remapped todata_lossso callers are not told their request was invalid. The client-side remap deliberately diverges from connect-go, which reportsinvalid_argumentin both directions;data_lossis more descriptive of what actually happened. The defaultCompressionProvider::decompress_with_limitimplementation (used by custom providers that only implementdecompressor) follows the same convention: read failures now map toinvalid_argumentinstead ofinternal. - Client-streaming and bidi handlers see request body failures as
stream errors (#150). A request body that fails mid-upload
(truncated or broken transport) now yields
Err(internal)from the handler's inbound stream instead of ending it cleanly, so partial input is no longer mistaken for a complete client stream. This refines the #130 behavior, which logged transport errors at debug level in all cases; errors after END_STREAM (or after the handler stopped reading) remain diagnostic-only. Handlers that?-propagate stream items now fail the RPC on truncated uploads — the right default for aggregation; handlers that want to tolerate truncation must match on the error. - Configured deadlines now bound request-body receipt for unary and
server-streaming RPCs (#136). Previously the body was collected
before the timeout was applied, so
with_default_timeout/with_maxbounded only handler execution and a slow-sending client could hold the request open indefinitely. One absolute deadline now covers body receipt, handler execution, and (withenforce_on_streams) the response stream; the deadline visible throughRequestContextmatches what is enforced. Services with timeouts sized only for handler CPU time may need to raise them to accommodate large uploads from slow clients. Client- and bidi-streaming RPCs are unchanged — their bodies are consumed inside the handler, already within the handler deadline.
- The client streaming receive path is restructured so the terminal contract above is enforced by construction rather than convention (#160): the terminal outcome is recorded exactly once, and ending the stream without recording why is unrepresentable. No public-API or intended behavior change beyond #159; the streaming contract tests' assertions are unchanged.
A patch release focused on the robustness of the streaming request and
response paths and of decompression. There are no API changes; the only
dependency-facing change is that the minimum supported bytes version is
now 1.6.
- The streaming request body reader treats the Connect END_STREAM envelope as terminal (#130). Request body bytes that arrive after END_STREAM are now drained (bounded) and ignored with a warning instead of being buffered, the decode buffer is released as soon as the decoder finishes, and transport-level body errors are logged at debug level instead of being silently discarded.
- Gzip decompression returns an error for truncated deflate streams
(#131). A gzip payload whose deflate stream ends without an
end-of-stream marker is rejected with
"gzip decompression stalled: truncated or invalid deflate stream"instead of never completing. - Decompression output buffers are sized from the compressed input, not
from
max_message_size(#132). Previously every decompressed message was backed by a limit-sized allocation (4 MiB by default) for as long as the message was held; the backing allocation now tracks the actual message size, with the same limits enforced as the buffer grows. - The Connect client-streaming response parser stops at END_STREAM and enforces the single-message rule before decompressing (#133). A second data envelope is rejected before its payload is touched, and bytes after the END_STREAM envelope are ignored.
- The minimum supported
bytesversion is now 1.6 (#132).
The headline feature is server-side interceptors (#114, #121) —
typed, async middleware that wraps a single RPC after envelope decoding,
decompression, and header parsing, and before the handler. Interceptors
see the resolved Spec, headers, deadline, extensions, and a lazily
decoded Payload, can rewrite the request and response, and can
short-circuit. Both unary (Interceptor::intercept_unary) and streaming
(Interceptor::intercept_streaming, covering server-, client-, and
bidi-streaming with one Stream-shaped hook) are supported. See the
interceptors section of the user guide.
The supporting types — Spec static method metadata (#112),
Payload / AnyMessage type-erased message bodies (#113),
RequestContext::path() / spec() / protocol() (#112, #116,
#120) — are useful on their own for tracing, auth, and routing layers
that need to know which RPC is in flight.
Consumers with checked-in protoc-gen-connect-rust output must
regenerate with the 0.6.0 toolchain: the generated Dispatcher::lookup
emits per-method Spec constants (#112), register() chains
.with_spec(...) (#120), and call_unary takes Payload (#119).
connectrpc-build users (build.rs integration) are unaffected — Cargo
rebuilds OUT_DIR automatically.
-
Dispatcher::call_unarytakesPayload, notBytes(#119). ThePayloadcarries the wire bytes plus an interceptor's lazy decode cache; an owned-message handler callsPayload::take_message()to reuse a decode an interceptor already paid for, instead of decoding the same bytes twice. Generated dispatchers andRouterimpls follow. Any hand-rolledimpl Dispatchermust update thecall_unarysignature; the streamingcall_*methods are unchanged. -
MethodDescriptoris now#[non_exhaustive](#112). It gains aspec: Option<Spec>field andfrom_kind/with_idempotent/with_specconst builders. Hand-rolledimpl Dispatcherblocks that constructedMethodDescriptorvia struct literal must switch to the builders (MethodDescriptor::unary(idempotent),MethodDescriptor::from_kind(kind), …); destructuring patterns need a trailing... Reads of the existingkind/idempotentpubfields are unaffected. -
AnyMessagegained a requiredinto_anymethod (#119). The blanketimpl<T: Message + Serialize> AnyMessage for Tcovers every generated message type, so this only affects manualAnyMessageimpls — which the trait docs already discourage.
-
Interceptortrait,Nextcontinuation, and server registration (#114).Interceptoris anasync_traitwith default-passthroughintercept_unary(req, next).Next<'_>holds the rest of the chain;next.run(req).awaitinvokes it (consume-once, enforced by the type system); not calling it short-circuits. Register withConnectRpcService::with_interceptor(...). The first-registered interceptor runs outermost, matchingconnect-go::WithInterceptors. A service with no interceptors pays oneis_empty()branch and no per-request allocation. Theunary_interceptorhelper turns a closure returning a boxed future into anInterceptorfor one-off use, and#[connectrpc::async_trait]is re-exported so downstream crates don't need anasync-traitdep. -
Interceptor::intercept_streaming(#121). OneStream-shaped hook covers server-streaming, client-streaming, and bidi: interceptors receive an inboundPayloadStreamand aNextStream<'_>continuation and return aStreamResponsecarrying the outboundPayloadStream, response headers, trailers, and a compression hint. Cross-stream coordination is shared state captured by the inbound and outbound adapter closures. Thestreaming_interceptorclosure helper mirrors the unary one. The empty-chain fast path is preserved. -
with_interceptor_arc(#118). Register an already-Arc'dArc<dyn Interceptor>so severalConnectRpcServiceinstances can share one interceptor (a process-wide auth token cache, rate-limit counter, connection pool).with_interceptoris now a thin wrapper over it. -
PayloadandAnyMessage(#113).Payloadholds the wireBytes+CodecFormatplus a lazy decode cache and an optional replacement (set_message). Typed access viamessage::<M>()(decode once, proto and JSON),view::<V>()(zero-copy proto view), andtake_message::<M>()(#119, consume the cache or decode fresh). Most interceptors only readSpecand headers, never the body — so the body is never decoded unless someone asks.AnyMessageis the object-safe surface for type-erased messages, with a blanket impl over everyT: Message + Serialize(no codegen required). -
Specstatic method metadata (#112, refs #87). Newconnectrpc::specmodule withSpec,StreamType,IdempotencyLevel, andSpecOrigintypes describing a single RPC method: its fully-qualifiedprocedurepath ("/package.Service/Method"), message flow shape, proto-declared idempotency contract, and which generated artifact (server or client) produced it.SpecisCopyand'static, withconst fnconstructors (Spec::server(...),Spec::client(...)) so generatedSpecconstants live in.rodata. Code generation emits apub const <SERVICE>_<METHOD>_SPEC: Specper method that user code can reference directly — this also closes #110, which asked for connect-go-style procedure-path constants. -
RequestContext::spec(),protocol(), andpath()(#112, #116).spec()returns the resolvedSpecfor the dispatched method;protocol()returns the negotiated wire protocol (Connect/Grpc/GrpcWeb);path()returns the requested procedure path taken directly from the request URI.path()is the wire truth and is populated unconditionally;spec()carries the richer static metadata but requires the route to have aSpecattached. -
Router::with_spec(#120). The dynamicRoutercan now carry aSpecper route, attached after registration via.with_spec(SPEC_CONST). The generatedregister()chains it after everyroute_*call, so handlers and interceptors see the sameRequestContext::spec()whether the host wired up the codegen dispatcher or the dynamicRouter. Routes registered withoutwith_specbehave exactly as before (spec() == None). -
HttpClientBuilderwithconnect_timeout(#117).HttpClient::builder().connect_timeout(dur).plaintext()(orplaintext_http2_only(),with_tls(...)) bounds the TCPconnect(2)call so a silently dropped SYN fails in milliseconds instead of the kernel'stcp_syn_retriesdefault (~130s). The existing constructors delegate to the builder with no timeout, so behaviour is unchanged for current callers.connect_timeoutcovers TCP connect only, not DNS resolution or the TLS handshake — useCallOptions::with_timeoutfor an end-to-end bound. -
Server::with_interceptorandServer::with_interceptor_arc(#123). Proxies to the same methods onConnectRpcService, completing the proxy set started in #105. StandaloneServerusers can now register interceptors without dropping down toServer::from_service(ConnectRpcService::new(...).with_interceptor(...)).
This release tracks buffa 0.6.0 (#108) and lands the contract-locking
breaking changes for the runtime types (RequestContext, ClientConfig,
CallOptions, the streaming handler traits) so future request- and
client-scoped metadata can ship as non-breaking additions.
Consumers with checked-in protoc-gen-connect-rust output must
regenerate with the 0.5.0 toolchain and buffa 0.6.0 plugins. The regen
picks up the buffa 0.6.0 codegen output changes — with_<field>() builder
setters on explicit-presence fields, MessageName impls on owned and view
message types, serde::Serialize impls on view types, and the removal of
empty __oneof.rs / __ext.rs / __view_oneof.rs ancillary content
files. After regenerating, delete any orphaned empty ancillary files;
the package stitcher (*.mod.rs) no longer include!s them. It also
picks up the streaming-trait ServiceStream<impl Encodable<Out>> change
described under Breaking below. connectrpc-build users (build.rs
integration) are unaffected — Cargo rebuilds OUT_DIR automatically.
- buffa dependency floor bumped to 0.6 (#108). buffa 0.6.0 has no
API-breaking changes for connect-rust's call sites — the bump is for
the codegen output baseline (see the regen note above) and to pick up
the
with_*setters andMessageNameimpls in generated message types. Consumers must align their directbuffa/buffa-typesdependencies to0.6to avoid duplicate crate versions.
The next three entries change the streaming-handler trait shape. They
are breaking under semver but the practical blast radius is narrow: the
common consumer surfaces — impl <GeneratedServiceTrait> blocks and
*_handler_fn registrations — compile unchanged. Only hand-rolled
impl StreamingHandler/impl BidiStreamingHandler blocks and direct
callers of dispatcher::codegen::encode_response_stream need a one-line
edit.
-
Streaming handler traits gain
type Item: Encodable<Res>(#98) and returnServiceStream<Self::Item>instead ofServiceStream<Res>— bringsStreamingHandler,BidiStreamingHandler,ViewStreamingHandler, andViewBidiStreamingHandlerto parity with unaryHandler::Body(added in 0.4.0). Stream items can now bePreEncoded,MaybeBorrowed, or anyEncodable<Res>; previously they had to be the ownedResitself.These are the lower-level escape-hatch traits behind
Router, not the primary handler surface. Most consumers use the codegen-generated service trait or the*_handler_fnclosure helpers, neither of which is affected — codegen handles the new shape and the helpers infertype Itemfrom the closure return. Hand-rolledimpl StreamingHandlerblocks must addtype Item = Res;. We surveyed the in-tree consumers and found two; if you have hand-rolled impls, expect a single one-line addition per impl block. -
Generated server-streaming and bidi-streaming trait methods now declare
ServiceStream<impl Encodable<Out> + Send + use<Self>>(#98) instead ofServiceStream<Out>. Existingimpl <Service>blocks that returnServiceStream<Out>compile unchanged via RPITIT refinement (the same mechanism the unary path used since 0.4.0; therefining_impl_traitlint suppression documented in 0.4.1 covers the streaming case too). Handlers that want to yieldPreEncodeditems must do so from'staticdata — theuse<Self>precise-capturing clause excludes&self's lifetime, so views built inside the stream body must encode to bytes before the borrow ends.Consumers with checked-in
protoc-gen-connect-rustoutput must regenerate (the same regeneration footgun documented in 0.4.0).connectrpc-buildusers (build.rs) are unaffected. -
dispatcher::codegen::encode_response_streamgains aBtype parameter (#98) for the stream item type. The generated dispatcher and route-registration code passesResexplicitly because the trait method's stream item is the opaqueimpl Encodable<Out>(RPITIT), which can't be unified against theEncodable<Res>impls. Only consumers that calldispatcher::codegen::encode_response_streamdirectly need to turbofishencode_response_stream::<Res, _, _>(s, format). We are not aware of any.
The next three entries lock the runtime config-type contracts behind
accessors and with_* builders so future request- and client-scoped
metadata can land as non-breaking additions. The migration is
mechanical: direct field reads become accessor calls (ctx.headers →
ctx.headers()) and bare-name setters gain a with_ prefix
(.protocol(p) → .with_protocol(p)).
-
RequestContextis now#[non_exhaustive]withpub(crate)fields and accessor methods (#101). Direct field reads —ctx.headers,ctx.deadline,ctx.extensions— must move toctx.headers(),ctx.deadline(),ctx.extensions(). Construction viaRequestContext::new(headers)and thewith_*builders is unchanged.New (additive) accessors landed alongside the change:
ctx.time_remaining()— saturatingDurationuntil the deadline, for budgeting downstream calls.ctx.extensions_mut()— mutable extensions, for tower middleware that builds aRequestContextdirectly.ctx.peer_addr()(serverfeature) andctx.peer_certs()(server-tlsfeature) — typed extension lookups for the well-known peer types. They returnNonewhen the transport didn't insert the value, replacing the panic-pronectx.extensions.get::<PeerAddr>().unwrap()pattern.
-
ClientConfigandCallOptionsfields are nowpub(crate)and theClientConfigsetter methods are renamed towith_*(#100). Both structs are#[non_exhaustive], so struct-literal and functional-update construction was already rejected by the compiler outside the crate (E0639); thepubfields just made the rustdoc suggest a path that didn't compile. Each field now has a same-named read accessor (config.protocol(),config.base_uri(),options.timeout(),options.headers(), …). To free the bare names for accessors, theClientConfigsetter methods were renamed to thewith_*form already used byCallOptions, and oneCallOptionssetter was renamed to match its field:Type Before After ClientConfig.protocol(p).with_protocol(p)ClientConfig.codec_format(f).with_codec_format(f)ClientConfig.compression(r).with_compression(r)ClientConfig.compression_policy(p).with_compression_policy(p)ClientConfig.default_timeout(t).with_default_timeout(t)ClientConfig.default_max_message_size(s).with_default_max_message_size(s)ClientConfig.default_header(n, v).with_default_header(n, v)ClientConfig.default_headers(h).with_default_headers(h)CallOptions.with_compression(b).with_compress(b)ClientConfig::new(uri),.json(),.proto(), and.compress_requests(e)are unchanged. The remainingCallOptionsbuilders (with_timeout,with_header, …) are unchanged. Migrating reads:config.protocol→config.protocol(),options.timeout→options.timeout().If you see
error[E0061]: this method takes 0 arguments but 1 argument was suppliedon aClientConfigbuilder call, you've hit the rename: the same-named read accessor now occupies the old name. Prefix the call withwith_per the table above. -
Server/BoundServer/ServeTlsbuilders renamed towith_*(#105) —tls_handshake_timeout(...)is nowwith_tls_handshake_timeout(...)(onServer,BoundServer, andaxum::ServeTls) andBoundServer::http1_keep_alive(...)is nowwith_http1_keep_alive(...). This matches thewith_tls(...)/with_graceful_shutdown(...)siblings that were alreadywith_*and theConnectRpcService/ClientConfigconvention. Migration: prefix the call withwith_.with_tls(...)is unchanged.
-
PreEncoded<M>response body (#98) — wraps already-encoded protobuf bytes and satisfiesEncodable<M>. Use when the handler builds and encodes a borrowing view internally — e.g. a*View<'a>borrowing from a local snapshot held inArc— rather than returning the view itself. The'staticbound on handler bodies and stream items means a view with a non-'staticlifetime can't cross the handler boundary;PreEncodedcarries the bytes across instead.The
Mtype parameter is a compile-time witness. Three construction paths, in decreasing order of guarantee:PreEncoded::from_message(&m)(alsoFrom<&M>/.into()) encodes an ownedMand the receiver type is the witness;PreEncoded::from_view(&view)enforcesM = MView::Owned;PreEncoded::from_bytes_unchecked(bytes)wraps already-encoded bytes from elsewhere — a cache, a blob store, a sidecar — and takesMon trust (in debug builds it also decodes once as adebug_assert!).Optimized for the proto codec, where the bytes pass through verbatim. JSON requests fall back to decoding the proto bytes as
Mand re-serializing — slow, but a working response rather than a runtimeunimplementederror. Services with significant JSON traffic should build and return the owned message (orMaybeBorrowed::Owned) so the codec layer can skip the proto round-trip. -
DeadlinePolicy(#103) — server-side moderation of client-asserted RPC deadlines. Clients control the per-request timeout viaConnect-Timeout-Ms/grpc-timeout; without a policy the server trusts that value verbatim.DeadlinePolicyclamps the asserted timeout to a server-controlled[min, max]range, applies a default when the client asserts nothing (or sends an unparseable header), and optionally extends enforcement to streaming bodies.Configure via
ConnectRpcService::with_deadline_policy(...)(axum / tower) orServer::with_deadline_policy(...).DeadlinePolicy::new()is a no-op policy that preserves the prior default behavior — no clamping, no default, streaming bodies unbounded once the handler returns. Existing services see no change without an explicit opt-in. Recommended production starting point: set at leastwith_max(...)to bound worker lifetime andwith_default_timeout(...)to your SLA.Two independent opt-in extensions, both off by default:
with_enforce_on_streams(true)wraps server- and bidi-streaming response bodies so the next item after the deadline is adeadline_exceedederror and the stream ends. Unary and client-streaming responses were already bounded by the handler future timeout; this closes the streaming-body gap.with_inter_message_timeout(d)cuts off a stream that goes longer thandbetween yielded items (a stalled handler waiting on a slow upstream). Takes effect whenever set, with or withoutwith_enforce_on_streams.
When clamping changes a client value, a
tracing::debug!event fires on targetconnectrpc::deadlinewith the path and before/after durations. Per-route deadline policy is a planned follow-up coupled to the typed routing surface (#91). -
Serverproxies forConnectRpcServicedispatch config (#105) —Server::with_limits,Server::with_compression, andServer::with_compression_policydelegate to the same-namedConnectRpcServicebuilders, so aServer::new(router)user no longer has to drop down toServer::from_service(...)to set request limits or compression. (Serveralready held the innerConnectRpcService; the proxies just expose the existing surface.) -
Server::with_http1_keep_alive(#105) —Serveralready had anhttp1_keep_alivefield (used byserve/serve_with_graceful_shutdown) but no builder to set it; onlyBoundServerdid. Adds the missing builder so a one-stepServer::new(router).serve(addr)user can disable HTTP/1.1 keep-alive without switching to thebind/from_listenertwo-step path. -
ConnectError::into_http_response(headers)(#111) — renders aConnectErroras a protocol-correcthttp::Response<ConnectRpcBody>for the protocol detected from the inbound request headers. Tower layers wrappingConnectRpcService(auth, rate limiting, validation) use this to short-circuit a request before dispatch and still produce an error the client's protocol can decode — gRPC / gRPC-Web clients get an HTTP 200 withgrpc-statustrailers, Connect-streaming clients get anEndStreamResponseenvelope, Connect-unary clients get a JSON error body. Without it, layers had to hand-roll a Connect-unary JSON body that gRPC and streaming clients see as a transport-level failure. Unrecognized or absentContent-Type(Connect GET, non-RPC traffic) falls back to Connect-unary JSON. -
#[doc(cfg(...))]annotations across the feature-gated public surface (#109, thanks @Yong-yuan-X). docs.rs now renders "Available on crate feature…only" badges on every public item that requires a non-default feature —client/client-tlstransports,server/server-tlstypes,gzip/zstd/streamingcompression, andaxumintegration. Annotations show the minimal cfg expression after Cargo feature implications collapse (client-tlsrather thanall(client, client-tls));all(...)is used only where neither feature implies the other. No runtime change.
connectrpc-build:.files()no longer emitscargo:rerun-if-changedin Buf mode (#59, thanks @hobostay). WhenConfig::files(...)is used withConfig::use_buf(), the listed entries are proto-relative names as they appear in the buf module (e.g."my/service.proto"), not filesystem paths. Emittingcargo:rerun-if-changedfor them pointed Cargo at non-existent files and forced a rebuild on every invocation. The directives are now suppressed in Buf mode, completing the fix #56 applied toPrecompiledmode. Manualprotocmode is unchanged.
connectrpc::axum::serve_tls(#80). Companion toservethat hands off to the standaloneServerfor the TLS path — wrapping axum withtokio_rustls::TlsAcceptordirectly hangs on h2 ALPN negotiation. Comes with anexamples/mtls-identityexample showing client-cert extraction in a handler.
-
connectrpc:serverfeature now enablestokio/macros(#80). The accept loop inServer::serveand the newaxum::serve_tlsboth usetokio::select!, but theserverfeature only enabledtokio/net. Crates depending onconnectrpc = { features = ["server"] }only compiled when something else in the dependency closure enabledtokio/macrosfor them. Our conformance suite and examples both havetokio = { features = ["macros", …] }in dev-deps, which kept the gap hidden in CI. -
connectrpc-build: generatedmod.rs#[allow(...)]is now sourced frombuffa_codegen::ALLOW_LINTS. The hardcoded list had drifted behind buffa's: it was missingclippy::uninlined_format_args(which buffa enum JSON deserialize errors trip),clippy::doc_lazy_continuation, andclippy::module_inception. Thepub mod <pkg>tree wraps buffa's per-proto split output (Owned/View/Oneof/Ext/PackageMod) plus our own__connect.rscompanions, and the per-proto Owned content has no#[allow(...)]of its own — buffa scopespackage_mod_allow_attr()to__buffaandprotoc-gen-buffa-packagingcovers the rest with an inner#![allow(...)]that has no analogue inconnectrpc-build's outer-mod layout. Sourcing fromALLOW_LINTS(chained with theconnectrpc-build-specificimpl_trait_redundant_captures) keeps the two from drifting again. Bumps thebuffa-codegendependency floor to0.5.1, whereunused_qualificationslanded inALLOW_LINTS. The checked-in conformance/example/bench output was regenerated against the buffa 0.5.2 toolchain (Self::in oneof serde, inlined format args in enum serde) — those are codegen output changes, not API changes, and don't affect the floor.
-
connectrpc-build: generatedmod.rs#[allow(...)]now suppressesunused_qualificationsandimpl_trait_redundant_captures. The 0.4.0 trait method RPIT carries ause<'a, Self>precise-capturing clause that is required for edition-2021 consumers but redundant under edition 2024, and buffa 0.5 codegen references sibling types through the canonical__buffa::view::*path even when a shorter natural-path re-export exists (the re-export can be shadowed by a same-named proto type). Both lints fired against the generated code in workspaces that opt them in (or build with-D warnings). The two extra entries in the#[allow(...)]block scope the suppression to thepub mod <pkg>tree and don't touch hand-written code.Also documenting one related lint with no codegen-side workaround:
refining_impl_trait_internal(warnby default since rust 1.86, rust-lang/rust#121718) fires on every handlerimpl, because the generated trait declaresServiceResult<impl Encodable<Out> + ...>while the handler returnsServiceResult<Out>. The refinement is intentional — it is what lets handlers return either an owned message, a borrowed view, or aMaybeBorrowed<M, V>— and is benign for handler impls, which are not part of the service's public API. There is no place in the generated module tree where#[allow(...)]could reach the handler impl. Consumers who deny warnings should setrefining_impl_trait_internal = "allow"in[lints.rust](or workspace lints) or#[allow(refining_impl_trait)]on each handlerimplblock.
This release tracks buffa 0.5.0. Consumers with checked-in
protoc-gen-connect-rust output must regenerate with the 0.4.0
toolchain (and buffa 0.5.0 plugins): service stubs are now emitted as
<stem>.__connect.rs (was <stem>.rs), and the new package stitcher
only include!s the new name. After regenerating, delete the stale
<stem>.rs files in the connect output directory — protoc plugins
do not delete or overwrite the old name. Regenerate before bumping
the runtime crate, not after: regenerated buffa output references
runtime symbols (ViewReborrow, decode_bytes_to_bytes,
__private::arbitrary_bytes) that don't exist in buffa 0.4.
connectrpc-build users (build.rs integration) are unaffected — Cargo
rebuilds OUT_DIR automatically.
-
buffa dependency bumped to 0.5 (buffa#97). The only codegen-facing change is that
buffa_codegen::GeneratedFileKindis now#[non_exhaustive]. This has no effect onconnectrpcruntime consumers; build integrations that consumeconnectrpc-codegenand matchGeneratedFileKindexhaustively need a wildcard arm (connect-rust itself matches only by==). See the buffa 0.5.0 release for the new natural-path re-exports — buffa 0.5.0 re-exports views atpkg::FooView, oneof enums atpkg::msg_name::Kind, and oneof view enums atpkg::msg_name::KindView— so the canonicalpkg::__buffa::view::FooView/pkg::__buffa::oneof::msg_name::Kindpaths from the buffa-0.4 layout are no longer needed in hand-written consumer code (they remain available for disambiguation if a proto type ever shadows a re-export). connect-rust's examples and tests now use the natural paths throughout. -
Generated service code is now emitted as a
<stem>.__connect.rscompanion file rather than appended to buffa's<stem>.rs(unified path) or written as a bare<stem>.rs(split / plugin path). connectrpc-codegen tags these filesGeneratedFileKind::Companionand wires them into the per-package stitcher withbuffa_codegen::apply_companions(buffa#91, designed in buffa#81). The module structure exposed to consumers is unchanged; the visible effect is the on-disk filename change for projects with checked-in generated code (see the regeneration note above). Build integrations that inspectGeneratedFileKindshould now matchCompanionfor connect-rust's service files. -
Carried over from the buffa 0.4 sync (buffa#62, buffa#55): generated code uses buffa's per-package stitcher layout, with view and oneof types canonically located under
<pkg>::__buffa::view::and<pkg>::__buffa::oneof::(buffa 0.5.0's natural-path re-exports above hide this from consumers).buffa_types::Any.valueis nowbytes::Bytes(wasVec<u8>). buffa's size cache is externalized (buffa#22): generated structs no longer carry__buffa_cached_size, andMessage::compute_size/write_totake&mut SizeCache. The providedencode_to_bytes()/encoded_len()are unchanged; connectrpc itself only uses those, but direct callers ofcompute_size()should switch toencoded_len(). -
connectrpc-codegen:Optionsnow embeds the buffaCodeGenConfigdirectly asOptions::buffainstead of mirroring individual fields (#34). The previous per-field shims (strict_utf8_mapping,generate_json,extern_paths,emit_register_fn) are gone; setoptions.buffa.<field>instead.CodeGenConfigis re-exported fromconnectrpc_codegen::codegenandconnectrpc_build.connectrpc_build::Configkeeps its existing builder methods as thin shims and gains.buffa_config(cfg)for wholesale replacement.generate_views = trueis still enforced. -
ConnectErrorshrunk from 248 to 72 bytes (#61). Theresponse_headersandtrailersfields are now crate-privateOption<Box<http::HeaderMap>>(waspub http::HeaderMap), soResult<_, ConnectError>no longer tripsclippy::result_large_err. New accessors replace direct field access:response_headers()/trailers()(borrow, empty map if unset),response_headers_mut()/trailers_mut(), andset_response_headers()/set_trailers(). Thewith_headers()/with_trailers()builders keep their signatures. Behaviour notes:with_headers/with_trailers/set_*now normalize an emptyHeaderMapto "unset" (observationally identical via the accessors), and theDebugoutput for an unset map now showsNoneinstead of{}. -
Handler signatures redesigned (#7): the generated service trait no longer threads a single
Contextin and out. Handlers now receive a read-onlyRequestContext(headers, deadline, extensions) and returnServiceResult<B>=Result<Response<B>, ConnectError>, whereResponse<B>carries the body plus optional response headers/trailers/compression hint. Unary and client-stream methods returnServiceResult<impl Encodable<Out>>; server-stream and bidi returnServiceResult<ServiceStream<Out>>.Response::ok(body)is the bare-body happy-path shorthand; for streaming bodies useResponse::stream_ok(s).Encodable<M>is the new "encodes as M" bound on response bodies. The oldContexttype is removed.// before async fn say(&self, ctx: Context, req: ...) -> Result<(SayResponse, Context), ConnectError> { Ok((SayResponse { ... }, ctx)) } // after async fn say(&self, _ctx: RequestContext, req: ...) -> ServiceResult<SayResponse> { Response::ok(SayResponse { ... }) }
-
View response bodies (#7): unary and client-stream trait methods are now
<'a>(&'a self, ...) -> ServiceResult<impl Encodable<Out> + use<'a, Self>>, so a handler can return a body that borrows from&self. Codegen emitsimpl Encodable<Out> for OutView<'_>and forOwnedView<OutView<'static>>per RPC output type (proto viaViewEncode; JSON returns anunimplementederror since view types lackSerialize). The newMaybeBorrowed<M, V>enum lets a handler return either: seebenches/rpc/benches/filter_handler.rsfor a redaction example (~1.65x at the codec layer when no modification is needed).ViewHandler/ViewClientStreamingHandlernow takeCodecFormatand return the response already encoded, dropping theRestype param.
GzipProviderdefaults tuned for throughput: the default compression level is now 1 (was 6), andflate2is built with thezlib-rsbackend (pure-Rust port of zlib-ng) instead ofminiz_oxide. Together this is ~2.7× throughput on theunary/large_gzipbench. Gzip wire format is unchanged; payloads compressed at level 1 are larger than at level 6. Restore the old ratio withGzipProvider::with_level(6). Note that Cargo feature unification means thezlib-rsbackend also applies to any otherflate2use in the same dependency graph.GzipProvider::DEFAULT_LEVELandZstdProvider::DEFAULT_LEVELare now public constants.
StreamingCompressionProvider::compress_stream(gzip and zstd) now honors the provider's configured level; previously it ignoredself.leveland usedasync-compression's default.connectrpcno longer pullsaxum's default features (#55), which transitively requiredtokio/net→mioand made the crate impossible to use on WASM hosts that integrate withaxum(e.g. Cloudflare Workers). Theaxumdependency now declaresdefault-features = false.- wasm32 client-stream and bidi RPCs no longer hang (#63). The
body-reader future is now spawned via
wasm_bindgen_futures::spawn_localonwasm32-unknown-unknown(it was being polled inline, deadlocking on the first.await). Native targets keeptokio::spawn.
file_per_packageoutput layout forprotoc-gen-connect-rustandconnectrpc-build. When enabled (opt: file_per_packageinbuf.gen.yaml,--connect-rust_opt=file_per_packagewithprotoc, orConfig::file_per_package(true)frombuild.rs), the per-proto split is collapsed to one<dotted.pkg>.rsper proto package with all service stubs inlined and no<pkg>.mod.rsstitcher — matching the<dotted.package>.rsfilename conventionprotoc-gen-buffaproduces under its ownfile_per_packageoption (buffa#73) and that BSR cargo SDK generation andtonic-style build integrations expect (module tree synthesised from filenames). The two plugins generate disjoint content (buffa: message types, connect-rust: service stubs); setfile_per_packageon both. In theconnectrpc-buildpath service stubs are inlined into buffa's per-packagePackageModrather than written as<stem>.__connect.rssiblings; the include file picks up the new filename automatically and consumer code is unaffected. When using the protoc plugin frombuf generate, drop theprotoc-gen-buffa-packaginginvocations under this layout — there are no per-file content files or stitchers for it to wire — and keep routingfile_per_packageoutput to its own directory: the filename matchesprotoc-gen-buffa's and would silently overwrite in a shared one. SeeCodeGenConfig::file_per_packagefor thestrategy: directoryconstraint.connectrpc::include_generated!(): shorthand macro forinclude!(concat!(env!("OUT_DIR"), "/_connectrpc.rs")). An optional filename argument (note: a filename including.rs, not a proto package name as intonic::include_proto!) supports projects that customise the output viaConfig::include_file(#50).connectrpc-build:Config::emit_rerun_directives(bool)to suppress thecargo:rerun-if-changed=lines when running outside a Cargobuild.rscontext (e.g. from a Bazel genrule or standalone host tool). Default remainstrue.
connectrpc-buildno longer emits invalidcargo:rerun-if-changeddirectives inPrecompiledinput mode (#56). When a precompiledFileDescriptorSetwas supplied instead of.protosource files,.files()paths were still being passed through to cargo, causing spurious rebuild triggers on paths that don't exist in that mode.
- MSRV is now declared as Rust 1.88 on the workspace and verified
in CI (#44). The code has required 1.88 since v0.3.2 (let-chains);
this commit documents the requirement in
Cargo.tomland adds an explicit CI check.
- New
examples/streaming-tourandexamples/middlewarecrates, plus a user guide underdocs/guide.md(#46, #48).
- 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.