NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
crates.io · #3873 most downloaded on crates.io
A Tower-based Rust implementation of the ConnectRPC protocol
Last release 16 days ago
21 Sep 2026
Ships unpredictably
gaps range from 8 days to 5 months
Nearly every release is documented
notes for 18 of 18 stable releases
2 versions withdrawn
withdrawn after publishing
11 months old
20 releases · first in 2025
One column per month.
release: v0.9.1 by @iainmcgin in #314
A client- or bidi-streaming call that ends before its request body no longer
lets a stalled client hold that body indefinitely. When the handler stopped
reading its request stream (it returned, an interceptor rejected the call, or
the request timeout fired), the request-body reader kept the partial message
it had buffered, its task and the HTTP/2 stream until the client ended the
stream or the connection closed. The reader now frees the partial message once
it sees the handler is gone. It discards the rest of the body for at most 5
seconds and 1 MiB (1 MiB only, on wasm32), also after the END_STREAM
envelope or a decode error, and then drops it. Dropping the body resets an
HTTP/2 stream once the response is done and closes an HTTP/1.x connection, so
a client still uploading when the limit is reached loses the stream or the
connection. A bidi call whose handler stops reading requests but keeps its
response open past the limit can still draw a connection-wide GOAWAY from h2
if the client keeps sending small frames.
The header-read timeout now closes a connection that sends nothing, or
only part of the HTTP/2 connection preface. Such a peer was held open
until it hung up, or until shutdown or a configured retirement trigger
ended it: the timeout started only after enough bytes had arrived to
pick HTTP/1.1 or HTTP/2. It now ends when the timeout expires. This
applies to Server and connectrpc::axum::serve_tls. A client or proxy
that opens connections ahead of use and sends nothing on them now has
them closed after the timeout (30 seconds by default). An HTTP/2
connection that has sent its preface is still not bound by the timeout.
with_header_read_timeout(None) disables both bounds. A zero timeout now
disables them too; before, it ended every HTTP/1.1 connection after at
most one request. A timeout too long to add to the current time also
disables them, instead of panicking the connection.
serve_tls now wraps the TLS stream in a private type, so
hyper_util::server::conn::auto::upgrade::downcast no longer recovers it
from an upgraded connection; use the upgraded connection's own Read /
Write.
docs: BSR remote plugin is now published by @iainmcgin in #223
Full Changelog: v0.8.1...v0.9.0
BidiStream::into_split splits a bidirectional stream into
independently owned BidiSendHalf and BidiRecvHalf, so the two sides
can be driven from separate tasks (true full duplex). The split is a plain
move of the stream's two sides — no locking is added. Dropping the send
half (or calling close_send) ends the request body cleanly while the
RPC continues; dropping the receive half cancels the RPC, matching the
behavior of dropping a whole BidiStream.
protoc-gen-connect-rust now advertises proto edition 2024 as its
maximum supported edition (#229). Previously protoc refused to run the
generator against an edition = "2024" file at all. Generated stubs are
unchanged: the features edition 2024 introduced, enforce_naming_style
and default_symbol_visibility, are enforced by protoc while compiling
and do not affect the service descriptors the generator reads.
The decode-time element-memory budget is now configurable. buffa 0.9 introduced a 32 MiB budget on the memory a single decode may commit to repeated, map, string and bytes elements — an amplification defence, charged on element footprint rather than on contents, so a few bytes on the wire cannot ask the decoder to materialize a very large number of small elements. A single large payload is unaffected however big it grows.
Until now that budget applied to every received message with no way to
change it, so a legitimate message carrying very many small elements that
0.8 accepted would be rejected with no recourse. Servers set it through the
existing Limits:
ConnectRpcService::new(dispatcher)
.with_limits(Limits::default().with_element_memory_limit(128 * 1024 * 1024))
Limits::unlimited() lifts it along with the other limits.
It applies to every server receive path — unary, server-streaming,
client-streaming and bidi, on both the generated dispatch and hand-registered
handlers, view-based and owned-message alike. On a proto wire it also governs
an interceptor's own decode of the inbound request body, through both
Payload::message and Payload::view, on all four of those shapes — so an
interceptor is held to the same budget as the handler behind it. JSON bodies
are decoded without it, on the interceptor and handler paths alike. A
rejection now names the limit to raise, since it is the one decode failure an
operator can fix without the peer changing anything.
Clients get the same control over responses. A client decoding a
response of very many small elements hit the same 32 MiB wall with no
override. ClientConfig::with_default_element_memory_limit sets a default
and CallOptions::with_element_memory_limit overrides it per call, the same
config-default/per-call pair max_message_size already uses, and it covers
unary, server-streaming, client-streaming and bidi responses. It is set on
both codecs, but defends less on JSON: serde_json materializes the owned
message before the budget is consulted, so there the budget bounds the
view decode rather than the parse.
An over-budget response is resource_exhausted, matching the neighbouring
max_message_size overflow rather than reporting a limit as an internal
error, and it names the setter that raises it. Neither knob is a breaking
change: both types are already #[non_exhaustive], and leaving them unset
keeps buffa's default.
Breaking, for generated code only. decode_borrowed_request_view and
decode_message_request_stream now take the decode limits, and Limits is
#[non_exhaustive] so further limits can be added without another break.
Both functions are #[doc(hidden)] and called only from generated dispatch,
so regenerating with the matching protoc-gen-connect-rust is the whole
migration — which 0.9.0 already requires for buffa 0.9.
ConnectError now preserves the underlying cause of transport
failures. A new ConnectError::with_source builder attaches an
underlying error, surfaced through Error::source() and, as a
SharedSource handle that can be moved into another error type, through
ConnectError::source_arc() (with with_shared_source to re-attach one);
ConnectError stays Clone since the source is held in an Arc. The
built-in client transports now retain the cause wherever a failure they
classify was previously stringified into message and discarded: the HTTP
and HTTPS HttpClient transports, Http2Connection (unix-socket connect,
eager/lazy connect and its establishment timeout, TLS handshake, h2 send),
and From<std::io::Error> / From<http::Error> for ConnectError.
ConnectError::unavailable_from_transport exposes the same convention to
custom ClientTransport implementations. Errors the call path synthesises
itself (the call deadline, request build/encode, response decode, a
body-read reset) do not carry one. The wire format and Display output are
unchanged; the derived Debug output now additionally includes the source
when one is attached. This is purely additive (#237).
Http2ConnectionBuilder::local_address(IpAddr) binds the built-in TCP
connector's socket to a local address before connecting, so every connection
the transport opens (including reconnects) originates from that address. For
multi-homed hosts where the peer keys on the source address it observes, or
where egress must leave a specific interface:
let conn = Http2Connection::builder()
.local_address("10.1.2.3".parse()?)
.lazy_tls(uri, tls_config);
Applied through hyper's HttpConnector::set_local_address; the resolved
peer addresses are filtered to the bound address's family, so a peer with no
address of that family fails to connect rather than connecting from a
kernel-chosen source. Like tcp_connect_timeout, it has no effect on the
custom-connector and Unix-socket terminals.
Interceptor plumbing for a re-runnable (retry-style) chain and for interceptors that synthesize a response. All additive; no dispatch behavior changes:
RequestContext::headers_mut() — mutable access to request headers,
the accessor an interceptor uses to inject auth or trace metadata before
next.run(req). (extensions_mut() already existed; the docs claimed
headers were mutable but no public accessor allowed it.)Payload::from_message(msg, format) — the lazily-encoded
counterpart of Payload::new: wraps a typed message and encodes only when
encoded() runs, memoizing the result (a replacement set with
set_message is now also encoded at most once). Lets an interceptor
short-circuit with a synthesized response; the dispatch path re-encodes
it in the negotiated format if the two differ (Payload::encoded_as).
bytes() is empty for such a payload; Payload's Debug output now
labels that field wire_len.Payload::try_clone() and UnaryRequest::try_clone() — explicit
copies (encoded bytes, cloned context, empty decode cache) for an
interceptor that needs a second request.UnaryRequest::from_parts(ctx, payload) — constructor for the
#[non_exhaustive] request from an existing Payload; StreamRequest
is now Clone.Next is now Clone, so a unary interceptor can run the rest of
the chain more than once (next.clone().run(first) then
next.run(second)); the consuming run(self) keeps the single-call
case explicit.Per-route request limits. Router::with_route_limits(procedure, limits)
gives one method its own Limits, replacing the service-wide ones from
ConnectRpcService::with_limits for requests to that route, so request
body size, message size and decode budget can be sized per RPC rather than
only globally — tighter for a method whose requests are always small,
looser for an upload-shaped one, without moving the ceiling for every other
route. The route's limits are surfaced on MethodDescriptor::limits (with
a with_limits builder for custom dispatchers) and apply on every dispatch
path, including the body drain of a request refused before dispatch.
Router::has_method now accepts the path with or without its leading
slash, matching with_spec and with_route_limits. Limits is now
Copy, PartialEq and Eq, so an existing limits.clone() trips
clippy::clone_on_copy; drop the .clone().
Updated to buffa 0.9, which requires regenerating your generated code.
The floor is hard rather than a preference: buffa 0.9 widens its size
arithmetic to u64, so code emitted by a 0.8.x codegen no longer compiles
against the 0.9 runtime. Regenerate through connectrpc-build,
buffa-build, or your buf generate pipeline after upgrading. The
extern_path targets a generated crate points at must likewise be buffa
0.9 output.
One behaviour change is worth knowing before you upgrade: buffa 0.9 applies a decode-time element-memory budget, 32 MiB by default, bounding the footprint a decode may materialize in repeated, map, string and bytes elements. It is an amplification defence, and it charges element footprint rather than element contents — so a large payload does not trip it however big it grows, but a message carrying very many small elements can, and such a message is now rejected where buffa 0.8 accepted it.
StreamMessage::to_owned_message and UnaryResponse::into_owned_parts
ride on buffa's now-infallible OwnedView::to_owned_message. Neither
signature changes.
Breaking: client-streaming calls take an async Stream of requests
instead of a synchronous iterator. call_client_stream and the generated
client methods accept impl ClientRequestStream<Req> — a sealed trait
implemented automatically for every Stream<Item = Req> + Send + 'static,
carrying tailored compiler diagnostics — so request messages can be
produced as they become available, without buffering the upload or
blocking a thread. The stream backs the HTTP request body directly: the
transport polls it as it is able to send, so backpressure is HTTP/2 flow
control and a server that ends the RPC mid-upload no longer goes
unnoticed while the stream is idle. The Stream trait and a
stream_iter adapter (futures::stream::iter) are re-exported from
connectrpc::client, with stream_iter also at the crate root, so
migrating a ready collection is
client.sum(connectrpc::stream_iter(requests)) with no new
dependency. Because the stream backs the request body, it must be
Send + 'static: streams that borrow local data must be made owning.
Note that dropping a client-streaming call now cancels it: messages the
stream had not yet yielded are never delivered.
Breaking: the Limits and CompressionPolicy setters are renamed with a
with_ prefix, and the Limits fields behind them are now private
(#247), to match the with_ convention used by the other configuration
setters. The Limits fields were also pub and shared their names with the
setters, giving each value two ways to be written.
The rename is a lookup table:
| Old | New |
|---|---|
Limits::max_request_body_size(n) |
Limits::with_max_request_body_size(n) |
Limits::max_message_size(n) |
Limits::with_max_message_size(n) |
CompressionPolicy::min_size(n) |
CompressionPolicy::with_min_size(n) |
Each old setter name is now a plain accessor, so limits.max_message_size()
reads back what with_max_message_size set and policy.min_size() reads the
compression threshold, which had no read path before. Code that read a
Limits field directly (limits.max_message_size) just adds the
parentheses.
Writing a Limits field directly is what goes away. Limits also becomes
#[non_exhaustive] in 0.9.0, so both remaining forms stop compiling:
assignment to a public field, and struct-literal or functional-update
construction such as Limits { max_message_size: 512, ..Default::default() }.
Start from Limits::default() or Limits::unlimited() and chain the setters
instead. CompressionPolicy's fields were already private, so it is a rename
plus the new accessor.
The element-memory budget setter added in this same release arrives already
prefixed, as Limits::with_element_memory_limit.
Breaking (low-level client API): the connectrpc::client::call_unary,
call_unary_get, call_server_stream, call_client_stream, and
call_bidi_stream free functions now take a spec: Spec in place of the
service: &str, method: &str pair. Generated clients are the callers in
practice and are updated by regenerating (automatic for connectrpc-build
users); a hand-written caller changes
call_unary(&t, &cfg, "pkg.GreetService", "Greet", req, opts)
to
use connectrpc::{Spec, SpecOrigin, StreamType};
call_unary(&t, &cfg, GREET_SERVICE_GREET_SPEC.with_origin(SpecOrigin::Client), req, opts)
// or, without generated code:
call_unary(&t, &cfg, Spec::client("/pkg.GreetService/Greet", StreamType::Unary), req, opts)
Generated client methods now pass their method's existing
<SERVICE>_<METHOD>_SPEC constant with origin flipped via the new
Spec::with_origin(SpecOrigin::Client). This gives a client-side
interceptor, when one is registered, the method's stream type and
idempotency level, not just its path.
Notes for hand-written callers: Spec::client takes a &'static str, so a
caller whose method names arrive at runtime should intern each distinct
procedure once rather than leak per call (see the Spec::client docs); an
entry point given a spec of the wrong stream shape or a server-origin spec
now returns an Internal error before sending anything, as does a procedure
that is not a valid URI path. The value a client
interceptor sees is not == to the generated constant (its origin
differs); the new Spec::same_method compares method identity across
sides. Regenerate checked-in code with task generate:all.
Large response messages are no longer copied into the framing buffer on the server. Uncompressed (and compressed) payloads of at least 16 KiB are emitted as their own HTTP body data frame by reference count, so encoding a streaming or gRPC unary response costs one payload copy instead of two. Envelope wire bytes are unchanged — only HTTP-level frame boundaries differ (the 5-byte envelope header may arrive in a separate frame from its payload, which the protocol has always permitted).
A gRPC or gRPC-Web unary response whose body is an OwnedView no longer
copies its large fields when encoding. A view's fields are slices into the
buffer it was decoded from, so they are handed to the HTTP body by reference
count and reach the socket as their own data frames. Past the framing
threshold the encode stops growing with the payload, because only the
framing is still being written.
Encoding stays contiguous where segmenting would not pay: responses below
the framing threshold, owned-message responses (their String and Vec<u8>
fields cannot be handed over by reference), a response much smaller than the
request it borrows from (capturing would keep the whole request buffer alive
while the response flushes), Connect-protocol unary, and any call passing
through an interceptor, since an interceptor may replace the body and so
needs it whole.
Breaking, for custom dispatch only. EncodedResponse is now
Response<EncodedBody> rather than Response<Bytes>. If you implement the
Dispatcher trait, or construct or consume EncodedResponse values, build
one with Bytes::into() and recover a single buffer with
EncodedBody::into_contiguous(). Interceptors are unaffected —
UnaryResponse still carries a contiguous Payload, because an interceptor
may replace the whole body — note that this also means a call with an
interceptor configured takes the contiguous path.
Streaming responses no longer copy large fields that the encoder can hand
over by reference count. Each item of a server-streaming or bidi response,
each client-streaming response, and a unary response served over gRPC or
gRPC-Web is now encoded through Encodable::encode_segments, and the
framing layer emits every segment at or above the 16 KiB framing threshold
as its own body frame, unmoved, while the tag/length fragments between large
fields ride in the batch buffer with the envelope header. The encoded message
bytes are unchanged; a segmented message is now split across more HTTP body
frames. In practice this reaches handlers that stream OwnedView items
(or MaybeBorrowed::Borrowed wrapping one) and any custom Encodable whose
encode_segments yields segments. Owned-message items, bare views,
PreEncoded, and StreamMessage items encode contiguously as before
(though the whole message is still framed by reference count past the
threshold), as do JSON responses, Connect-protocol unary, and calls passing
through an interceptor.
The segmented encode applies only to an uncompressed response. With the
default CompressionPolicy (compress from 1 KiB) and a client that
advertises an encoding, every item large enough to segment is compressed
instead, which needs the message contiguous — so on that path the segmented
encode is flattened again and costs more than the contiguous encode it
replaced. Opt out per response with Response::compress(false), or raise
CompressionPolicy::with_min_size, when the payload is already compressed
or the copy matters more than the bytes.
Breaking, for custom dispatch only. StreamingResult's body is now
EncodedStream (ServiceStream<EncodedBody>) rather than a stream of
Bytes, mirroring EncodedResponse, and StreamResponse::from_encoded /
StreamResponse::into_encoded follow. A hand-written Dispatcher or test
double converts with
Response::stream(items.map(|r| r.map(EncodedBody::from))) and recovers a
single buffer per item with EncodedBody::into_contiguous(); EncodedBody
is also now #[non_exhaustive], so match through its accessors rather than
its variants. Generated dispatchers and handler traits are unaffected, so no
regeneration is needed.
connectrpc-health and connectrpc-reflection bound their routes to 16
KiB per request message. install_static and install now apply a
per-route Limits profile (MAX_REQUEST_BYTES, request_limits()) sized
to the small fixed shape of HealthCheckRequest and
ServerReflectionRequest; a larger message is refused with
resource_exhausted where 0.8 accepted anything up to the service-wide
limit. The profile replaces the service-wide limits on those routes in both
directions. Each crate's new apply_request_limits(router, limits) tunes
those routes specifically — pass request_limits() after another
registration path (HealthExt::register, Router::add_service, a single
reflection version), or your own Limits after any path, including a
service configured tighter than 16 KiB; the later call wins.
Removed the streaming feature and the StreamingCompressionProvider
API (#246). Streaming RPCs are unaffected — the feature name invites
the opposite conclusion, but what it gated was an incremental-compression API
that no wire path could reach, not streaming RPC support.
Breaking. Gone: the streaming feature (previously on by default), the
StreamingCompressionProvider trait, the BoxedAsyncRead and
BoxedAsyncBufRead aliases, and CompressionRegistry's register_streaming,
get_streaming, supports_streaming, compress_stream and
decompress_stream. A custom provider registered through register_streaming
moves to register and keeps working on every path that ever used it. Removed
API surfaces as a rustc error; a stale streaming entry in a
features = [...] array fails earlier still, as a Cargo resolution error —
drop it there too. The removal also drops the async-compression dependency,
which was compiled into every default build.
Initial BidiStream::message() cancellation no longer breaks the receive side.
Dropping the first receive while it is waiting for response headers or Connect
error-body parsing now leaves initialization pending for the next message()
call, while real transport, deadline, and protocol failures stay terminal and
sticky. Dropping the BidiStream (or failing the call at its deadline) now
aborts the in-flight initialization task instead of detaching it: a stalled
server can no longer pin an abandoned call's resources, and dropping the
stream cancels the call outright rather than flushing buffered request
messages in the background — callers that need the request delivered must
drive the call via message() before dropping. message() now requires
B: 'static and the view type to be 'static, which every transport and
generated view type already satisfies.
connectrpc-build now passes --include_source_info to protoc, so proto
comments are carried into the generated Rust documentation (#222). Users of
Config::descriptor_set must add --include_source_info to their own
protoc invocation to get the same benefit; buf-built sets already
include source info. The set written by Config::emit_descriptor_set now
has SourceCodeInfo stripped for the protoc and buf sources — reflection
does not need it, and stripping keeps embedded descriptor bytes lean and
proto comments out of shipped binaries (precompiled sets are still written
through unchanged). Service and method comments are now sanitized for
rustdoc the same way buffa sanitizes message and field comments: markdown
and HTML metacharacters are escaped, bare URLs are autolinked, indented
blocks are rendered as text, and unterminated fences are closed, so
arbitrary proto comments cannot break a consumer's cargo doc. Code
fences in proto comments are also made inert — an unannotated fence
becomes text and any other gains ignore (keeping its language, so a
rust fence is still syntax-highlighted) — because rustdoc would
otherwise compile a fence's contents as a doctest in the consuming crate,
where a proto comment's example has no imports and names proto types.
Paragraph breaks and continuation-line indentation in service and method
comments are also preserved instead of being flattened.
Abandoning a call_client_stream call now stops the in-flight transport
send (#224). Dropping the call future or hitting its deadline while the
transport is still waiting for response headers no longer leaves the send
polling the transport indefinitely. Note that abandonment now cancels the
underlying request: a request that was still being sent when the call was
abandoned may never reach the server, so a caller that needs the request
delivered must drive the call to completion.
A Connect or gRPC call whose deadline expires now reports
deadline_exceeded when the transport fails first. A server enforcing the
same deadline aborts the RPC independently, so its RST_STREAM can arrive
before the client's own timer fires. The in-flight body read then failed
first and the call reported internal, attributing to the client what was
really a timeout the caller asked for. Which code you got came down to timer
coarseness and scheduler delay, so the same call could report either one run
to run.
Transport errors surfacing after the deadline has elapsed are now classified
as deadline_exceeded, the rule the missing-grpc-status path already
applied and which both now share. The transport cause is kept in the
message, so a
genuine transport fault that merely happened after the deadline is still
diagnosable, and a failure before the deadline is still internal. #210
Code generation handles large schemas. buffa 0.9's element-memory budget
is charged per element on struct size rather than on encoded bytes, and
descriptor structs are wide, so the 32 MiB default rejected descriptor sets
that are entirely legitimate — ordinary schemas of a few hundred .proto
files, not pathological ones.
protoc-gen-connect-rust and connectrpc-build decode the compiler's own
output, which is not the untrusted input that budget defends against, so both
now decode under buffa 0.9.1's 1 GiB tooling budget. Both honour the same
overrides buffa's own plugins do — the element_memory_limit= plugin option
and the BUFFA_ELEMENT_MEMORY_LIMIT environment variable — so one setting
raises every plugin in a buf generate run rather than needing a
connect-specific twin. See "Very large schemas" in the guide.
Descriptor sets supplied at runtime stay bounded, because a reflection
service may be fed descriptors by a peer rather than by its own build. An
over-budget set there now reports the new ReflectionError::ElementBudget,
which says the bytes are well-formed and the schema is simply large, rather
than a decode failure that reads like corruption.
A method type missing from the descriptor set is now an error on the
build-script path instead of a silent bare-name reference (#244).
connectrpc-build used to accept a Config::descriptor_set input whose
service referenced a type absent from the set — for example a set built
without protoc --include_imports — and report success while emitting code
that named a type existing nowhere, so the first signal was rustc complaining
about a file in $OUT_DIR the user never wrote. The
protoc-gen-connect-rust plugin already rejected the same input; both paths
now fail with the same message, naming the unresolved type and pointing at
--include_imports. Sets produced by connectrpc-build's own protoc and buf
sources always carry the import closure, so only Config::descriptor_set
users are affected. A corrupt precompiled set also now names the file that
failed to decode.
A gRPC or gRPC-Web unary call now reports a trailers-only error instead of
the bare HTTP status (#241). When a proxy answers with a non-200 status
and a gRPC status in the response headers — an Envoy local reply sends 503
alongside grpc-status: 14 and grpc-message: upstream connect error — the
call surfaces the server's code, message, and metadata rather than
HTTP error 503. This matches grpc-go, which decides that a reply is gRPC
from its content-type and reads the status from it; connect-go instead
reports the HTTP status and discards the server's code, message and metadata
entirely. The header status applies only to a genuine trailers-only
response: a response carrying body data or HTTP/2 trailers takes its status
from those, never from the headers, and a success delivered on a non-200 is
still refused. The error code can change as a result, not just the message:
a proxy pairing HTTP 500 with grpc-status: 3 now yields InvalidArgument
where it previously yielded Unknown, so a caller matching on ErrorCode
may observe the server's code rather than the one derived from the HTTP
status.
A non-2xx reply that is not a usable gRPC response reports its HTTP status,
so an nginx or load-balancer error page still arrives as Unavailable on a
502 and stays recognizable as retryable. What went wrong is appended to
the message rather than replacing the code, and this now holds however the
error page fails: HTTP error 502: unexpected content-type: text/html when
the proxy labels it as HTML, and HTTP error 502: envelope decode failed: ... when the proxy keeps the gRPC content-type and the page fails framing
instead. A reply with no content-type at all is still parsed for a
grpc-status, since an absent content-type is not evidence the peer is not
speaking gRPC.
A plain gRPC unary response whose body sets the 0x80 envelope flag is
now a framing error (#242). That flag marks a gRPC-Web trailer frame
and is not defined for gRPC, so a server or intermediary that set it could
replace the trailers received in the HTTP/2 trailer section with trailers
of its own choosing, including the grpc-status. gRPC-Web is unaffected:
its trailer frames are parsed as before. The flag now has a name,
envelope::flags::GRPC_WEB_TRAILER, public alongside the existing DATA,
COMPRESSED and END_STREAM flags.
Terminal client errors now carry the response headers and trailers
consistently (#202). A failed RPC reported through
ServerStream::message() — a Connect END_STREAM carrying an error, a gRPC
or gRPC-Web grpc-status error, a decode, decompression, transport or
deadline failure — arrives with ConnectError::response_headers()
populated, and with ConnectError::trailers() populated whenever
termination metadata was received. The same holds for the sticky replay on
a second message() call and for ServerStream::error(). Previously only
a few of these paths attached metadata, so whether a caller could read the
server's headers off an error depended on which protocol was in use and on
how the stream happened to fail. The Connect unary and client-streaming
parse paths gained the same guarantee — the content-type rejection, the
bounded body read, decompression and message decode all report the
metadata their response delivered. As before, the error's trailers exclude
grpc-status, grpc-message and grpc-status-details-bin, whose content
is already the error's code, message and details. That filter now also
applies on the Connect client-streaming path, so identical wire bytes give
identical ConnectError::trailers() whatever the call shape;
ServerStream::trailers() still reports the wire trailers verbatim.
A gateway propagating an upstream error could produce a response the
client could not read, losing the handler's error code (#202). When a
handler returns a ConnectError that came back from a client call — the
ordinary gateway shape — that error carries the upstream response's
headers, and the server echoed them onto its own response verbatim.
Over Connect on HTTP/1.1 the consequence was total: the forwarded
content-length contradicted the body actually being written, hyper's
HTTP/1 encoder refused to serialize the response, and the connection
closed with zero bytes sent. Measured against connect-go v1.19.1, a
handler returning permission_denied surfaced at the caller as
unavailable with empty metadata, because a transport failure was all
the client ever saw; every handler error code collapsed the same way. A
forwarded content-encoding: gzip was a second, milder break — the
client tried to gunzip a plain-JSON error body, discarded it, and fell
back to the HTTP status. A forwarded date replaced the server's own,
since hyper emits no Date of its own once one is set, so responses
advertised the upstream's generation time.
The server now keeps its own value for any header it already set, which
is what kept two content-type values off the wire, and never forwards
names that describe the upstream rather than this response: the RFC 9110
hop-by-hop set, content-length, content-type, the content-coding
headers, date, and grpc-status-details-bin. All other header
metadata is forwarded unchanged, including every value of a multi-valued
name. Trailing metadata is unaffected by this change.
Streaming gRPC responses now validate their declared content type during response initialization.
Server-streaming calls reject a wrong gRPC / gRPC-Web protocol family or codec before returning a response stream, while bidirectional calls reject it on the first receive. Such responses previously entered normal stream processing, where they could fail later or appear to complete successfully. The check runs before a trailers-only grpc-status is read, as it already does on the unary path, so a reply in the wrong content-type family is rejected rather than believed for its status, and a non-2xx reply that is not speaking gRPC now names the offending content type after its HTTP status on streaming calls too. Existing compatibility behavior for missing and bare-family content types is unchanged.
Interceptor runs after the
request body has been read and decompressed under Limits but before the
message is decoded, so an interceptor that rejects a call never pays for
the decode — the step whose memory cost can exceed the wire size by orders
of magnitude. The guide's interceptor section and comparison table, its
examples, the Interceptor trait doc, and
ConnectRpcService::with_interceptor now state this ordering, give the
bounds an unauthenticated request can cost under the default limits, and
direct authentication to Tower middleware (which runs before any body
byte is read) and authorization to interceptors. No runtime behavior
changed.release: v0.8.2 by @iainmcgin in #315
A client- or bidi-streaming call that ends before its request body no longer
lets a stalled client hold that body indefinitely. When the handler stopped
reading its request stream (it returned, an interceptor rejected the call, or
the request timeout fired), the request-body reader kept the partial message
it had buffered, its task and the HTTP/2 stream until the client ended the
stream or the connection closed. The reader now frees the partial message once
it sees the handler is gone. It discards the rest of the body for at most 5
seconds and 1 MiB (1 MiB only, on wasm32), also after the END_STREAM
envelope or a decode error, and then drops it. Dropping the body resets an
HTTP/2 stream once the response is done and closes an HTTP/1.x connection, so
a client still uploading when the limit is reached loses the stream or the
connection. A bidi call whose handler stops reading requests but keeps its
response open past the limit can still draw a connection-wide GOAWAY from h2
if the client keeps sending small frames.
The header-read timeout now closes a connection that sends nothing, or
only part of the HTTP/2 connection preface. Such a peer was held open
until it hung up, or until shutdown or a configured retirement trigger
ended it: the timeout started only after enough bytes had arrived to
pick HTTP/1.1 or HTTP/2. It now ends when the timeout expires. This
applies to Server and connectrpc::axum::serve_tls. A client or proxy
that opens connections ahead of use and sends nothing on them now has
them closed after the timeout (30 seconds by default). An HTTP/2
connection that has sent its preface is still not bound by the timeout.
with_header_read_timeout(None) disables both bounds. A zero timeout now
disables them too; before, it ended every HTTP/1.1 connection after at
most one request. A timeout too long to add to the current time also
disables them, instead of panicking the connection.
serve_tls now wraps the TLS stream in a private type, so
hyper_util::server::conn::auto::upgrade::downcast no longer recovers it
from an upgraded connection; use the upgraded connection's own Read /
Write.
Create SECURITY.md by @iainmcgin in #213
Full Changelog: v0.8.0...v0.8.1
Client stream message() futures are Send again with concrete generated view types (#214). 0.8.0's bound shape on ServerStream::message/BidiStream::message (projecting RespView::Owned in the where-clause) tripped rustc's coroutine-witness auto-trait check on monomorphization, so a server-stream could not be consumed inside tokio::spawn; the bound is reshaped as an output type parameter (RespView: MessageView<'static, Owned = M>), which is equivalent for every caller and restores Send. Call sites are unaffected — M is inferred.
examples/bazel: update to connectrpc 0.7.0 / buffa 0.7.0 and fix codegen rules by @iainmcgin in #161
json feature for proto-only builds by @macalinao in #172Full Changelog: v0.7.0...v0.8.0
encodable_impls=all_messages codegen option (#145, #205).
protoc-gen-connect-rust can now emit the ::connectrpc::Encodable view
impl pair for every message defined in the targeted protos (not only RPC
output types), including companion files for protos that declare no
services. This is the building block for multi-crate generated-code
layouts: Rust's orphan rules require the impls to live in the crate that
owns the message types, so generating a shared proto crate with this
option lets other crates' handlers return views of those types directly
instead of falling back to owned messages or PreEncoded::from_view.
connectrpc-build exposes the same through
Config::encodable_impls(EncodableImpls::AllMessages).
Configurable codegen client feature gate name (#181, #194).
gate_client_feature=<name> lets protoc-gen-connect-rust emit generated
client items behind #[cfg(feature = "<name>")] instead of the default
client; connectrpc-build exposes the same through
Config::client_feature_name. The existing bare gate_client_feature
option and Config::gate_client_feature(true) behavior continue to use
client.
Http2ConnectionBuilder with establishment-timeout and HTTP/2 keep-alive
knobs (#137, #197). Http2Connection::builder() is now the single
configuration surface for every transport flavour (plaintext, TLS,
caller-supplied connector, Unix socket); the existing Http2Connection
constructors are shortcuts for it with default settings. The builder adds
establishment_timeout (a wall-clock bound on DNS + TCP + TLS + HTTP/2
preface as one budget) and tcp_connect_timeout (a per-address TCP bound on
the built-in connector), and proxies hyper's keep_alive_interval,
keep_alive_timeout, keep_alive_while_idle, initial_stream_window_size,
initial_connection_window_size, and adaptive_window setters directly,
with a TokioTimer pre-wired so the keep-alive setters work without hyper's
"supply a timer" panic. h2_settings(|b| ...) is the escape hatch for hyper
knobs not proxied. HttpClientBuilder gains the matching
establishment_timeout (covering DNS + TCP + TLS) and tcp_connect_timeout
(the canonical name for [#117]'s connect_timeout, which remains as an
alias). Both builders also expose no_establishment_timeout /
no_tcp_connect_timeout to opt out of the new defaults below.
Optional json cargo feature for proto-only builds (#172). The
Connect JSON codec requires serde::Serialize/Deserialize on every
message type, so the code generator derives them by default — pure cost for
crates that only speak binary proto. The new default-on json feature, when
disabled (connectrpc = { default-features = false }), relaxes the runtime's
message-type bounds to just buffa::Message via the new
JsonSerialize/JsonDeserialize marker traits, so message types
generated with the codegen no_json option (no serde derives) compile
against the runtime. A proto-only server declines JSON at content
negotiation — application/json / application/connect+json (and the
Connect GET encoding=json parameter) return HTTP 415, and
application/grpc+json / application/grpc-web+json return a gRPC error
status — with message-level encode/decode returning Unimplemented as a
backstop; the client's ClientConfig::json selector is removed from the API
in that build. The Connect error/end-stream wire format
is always JSON per spec, so serde/serde_json remain required
dependencies. See the
proto-only build guide.
Top-down service registration on Router (#164). Router::add_service
registers a generated service from the router outward
(Router::new().add_service(Arc::new(svc))), the discoverable counterpart to
the existing FooServiceExt::register extension method, which remains
available. New Router::merge / Router::merge_in_place combine routers, and
a new public ServiceRegister trait (implemented by codegen) backs
add_service. Registering or merging a method path that already exists now
fails by default so an accidental collision — such as adding the same service
twice — surfaces loudly instead of silently shadowing a route: add_service,
register, merge, merge_in_place, and merge_routers panic. Call
Router::allow_overrides to opt into last-wins replacement across all of
them. For assembling routers from dynamic input, Router::try_merge /
Router::try_merge_in_place return a RouterMergeError listing the
conflicting paths instead of panicking.
Maximum connection age for the built-in server (#151).
with_max_connection_age (on both Server and BoundServer) retires
long-lived HTTP/2 connections by sending a GOAWAY once a connection reaches
the configured age, then force-closing after a grace period
(with_max_connection_age_grace, default 5s); HTTP/1.1 connections have
keep-alive disabled instead. A symmetric ±10% jitter is applied per
connection to avoid reconnect bursts. Disabled by default; whole-server
graceful shutdown still drains in-flight requests indefinitely.
HTTP/2 adaptive flow-control window, on by default (#178). The built-in
server now enables hyper's adaptive (BDP-based) flow-control window sizing by
default, so HTTP/2 stream and connection windows grow with the measured
bandwidth-delay product instead of staying pinned at hyper's fixed 64 KiB.
This is a behaviour change: throughput improves on high-latency,
high-bandwidth links (cross-region, high-throughput streaming), at the cost
of slightly higher per-connection memory under load. It matches grpc-go and
grpc-java, which both autotune by default. New setters on Server and
BoundServer give explicit control: with_http2_adaptive_window(bool)
toggles autotuning, and with_http2_initial_stream_window_size /
with_http2_initial_connection_window_size set fixed windows (supplying a
fixed window turns adaptive sizing off, mirroring grpc-go). The new default
is exposed as the DEFAULT_HTTP2_ADAPTIVE_WINDOW constant.
Connection-level HTTP/1.1 header read timeout (#135). The built-in
Server/BoundServer and the axum serve_tls path now install a
TokioTimer on every accepted connection and apply a header read timeout,
configurable via with_header_read_timeout(Option<Duration>) (default
DEFAULT_HEADER_READ_TIMEOUT, 30 seconds; pass None to disable). The
timeout bounds how long the server waits to read a complete request header
block, measured from when hyper begins reading a new request; on a keep-alive
connection it also bounds the idle wait between requests. A peer that opens a
connection — or finishes one request — and then stalls without sending the
next request's headers is now disconnected instead of holding a task and file
descriptor open indefinitely, which mitigates slowloris-style
connection-exhaustion attacks. Previously no timer was installed, so hyper's
built-in header read timeout never took effect; the default is now active.
Applies to HTTP/1.1 only — HTTP/2 connection liveness (keep-alive pings) and
idle-connection reaping are tracked separately.
buffa 0.8 adoption (#209). The buffa runtime crate's floor
moves from 0.7 to 0.8.1 (buffa-types, buffa-codegen, and
buffa-descriptor move to 0.8 — the extra 0.8.1 patch is a
runtime-only accounting fix, so 0.8.0-generated code qualifies).
ServiceRequest::to_owned_message,
StreamMessage::to_owned_message, and the client response's
into_owned keep their infallible signatures: buffa 0.8 made owned
conversion fallible (re-materializing preserved unknown fields could
exceed the unknown-field allowance for a view that decoded fine), but
buffa 0.8.1 charges every unknown-field record against the decode-time
allowance, so a payload that would overflow is rejected at the decode
boundary (invalid_argument, like any other malformed request) and a
successfully decoded view always converts. Two further 0.8 changes are
absorbed without API impact: generated map fields are now
buffa::Map<K, V> instead of std::collections::HashMap (hand-written
code constructing owned map fields needs the type swap), and the
fast-utf8 decode path that buffa 0.8 ships as a default feature is
explicitly enabled, since connectrpc builds buffa with
default-features = false. All checked-in generated code is
regenerated against buffa 0.8.1.
Client stream items are now StreamMessage (#209).
ServerStream::message() and BidiStream::message() yield
StreamMessage<Resp> — the same wrapper server handlers receive for
inbound streams — instead of a raw buffa::OwnedView. Field access
moves from msg.reborrow().field to msg.view().field (or the
generated accessor methods), and owned conversion is the same
infallible .to_owned_message() as everywhere else on the API, so no
client-side conversion can fail. UnaryResponse::into_owned_parts()
is added for the headers-plus-owned-message-plus-trailers case that
previously required into_parts() followed by a manual conversion.
Further migration notes for code that used the item as an OwnedView:
items no longer implement PartialEq or serde Serialize (compare or
serialize through msg.view() / msg.to_owned_message()), the
consuming into_bytes() spelling becomes msg.bytes().clone() (same
cost — a Bytes refcount bump), and hand-written generic wrappers
over the stream handles need the new
RespView::Owned: HasMessageView<View<'static> = RespView> bound at
their .message() call sites (buffa-generated types always satisfy
it).
Crate-root name cleanup and ergonomics fixes from a pre-release API review (#209):
connectrpc::UnaryResponse at the crate root is now the client
response type (what generated client methods return), alongside new
root exports ServerStream and BidiStream. The wire-level
interceptor aliases previously holding those root names —
UnaryRequest, UnaryResponse, StreamRequest, StreamResponse —
are module-scoped only: import them from
connectrpc::interceptor::*.ErrorDetail is exported at the crate root, and
ErrorDetail::from_message builds a detail from a protobuf message,
handling the protocol's base64 encoding. A hand-populated
ErrorDetail::value that is not valid base64 now logs a tracing
warning when it is omitted from a gRPC status instead of vanishing
silently.connectrpc re-exports http_body, and generated client bounds
reference it through the re-export — consumers of generated code no
longer need a direct http-body dependency.StreamMessage::from_message and the un-hidden
ServiceRequest::from_parts are the supported way to construct
handler inputs in unit tests; the guide gains a "Testing handlers"
section.InboundStream<M> (= ServiceStream<StreamMessage<M>>) names the
inbound stream type in generated client-streaming/bidi handler
signatures.Client transport errors keep their original classification (#199).
The client call paths previously wrapped every transport send failure as
unavailable with a request failed: prefix, including errors that were
already classified ConnectErrors — so a local configuration mistake such
as pointing HttpClient::plaintext() at an https:// URL looked like a
retryable outage. A ConnectError found anywhere in the transport error's
source chain is now surfaced verbatim (code, message, details, and attached
metadata; the request failed: prefix is gone for the built-in
transports). Transport errors with no ConnectError in their chain are
wrapped as unavailable, unchanged. Retry classifiers keyed on
unavailable keep matching genuine transport outages, and now correctly
stop retrying non-retryable local errors; anything matching on the
request failed: message prefix should match on the error code instead.
Client connection establishment is bounded by default (#137, #197).
Http2Connection and HttpClient now bound connection establishment to
DEFAULT_ESTABLISHMENT_TIMEOUT (20s, matching grpc-go's MinConnectTimeout)
with an additional DEFAULT_TCP_CONNECT_TIMEOUT (5s) per-address TCP bound
on the built-in connector. Previously, a server that accepted the TCP
connection but stalled the TLS handshake — for example a draining pod whose
kernel backlog still accepts — would stall poll_ready indefinitely for
every caller sharing the connection. Exceeding either bound surfaces as
ErrorCode::Unavailable. This applies to every existing constructor
(they now delegate through the new builders), so this supersedes the [#117]
changelog entry's "behaviour is unchanged for callers who don't opt in"
statement under 0.7.0. To restore the unbounded pre-0.8.0 behaviour, call
.no_establishment_timeout().no_tcp_connect_timeout() on the builder. Note
that hyper divides the per-address TCP budget across the resolved address
set, so a hostname resolving to many addresses on a high-latency link may
need a larger tcp_connect_timeout than the 5s default.
Connect streaming EOF without END_STREAM now returns internal
(#168). The ServerStream Connect EOF path that 0.7.0's [#140]
introduced as unavailable now returns internal — the code
connect-go reports for this path, and the primary expected code in the
upstream conformance suite addition (connectrpc/conformance#1104).
The HTTP body completes cleanly in this case; it is the Connect
envelope sequence that is missing its terminus, so connect-go and other
gRPC stacks classify it as a wire-level error in the same family as a
failed decompression or an unparseable response, rather than the
transport flakiness [#140]'s entry described. Clients on 0.7.0 that
match Unavailable for a truncated Connect stream must match Internal
after this release, and generic retry middleware that retries on
unavailable will now treat this case as terminal — a server that omits
END_STREAM is not expected to start sending it on retry. The
client-streaming check from #163 ships with the same internal code.
(The 0.7.0 [#140] entry's "matching connect-go" parenthetical was also
inaccurate as to the code, but correct that connect-go is the reference:
it returns internal for this path.)
Connect Unary-Get query parameters are emitted in the spec-recommended
order (#167): connect, base64, compression, encoding, message.
Servers must accept any order, so this is not a wire-compatibility change;
the recommended order keeps the variable-length message last so the URL
prefix is stable for shared caches. Aligns with the order check added to
the upstream conformance suite.
Unsupported gRPC/gRPC-Web message codecs return unimplemented
(#180). A request with a valid application/grpc / application/grpc-web
prefix but a codec the server does not speak (for example
application/grpc+thrift, or application/grpc+json in a proto-only build)
now returns grpc-status unimplemented (12) instead of internal (13).
This matches the compression axis, which already returns unimplemented for
an unsupported grpc-encoding. Clients that branch on grpc-status will
observe 13 → 12 for this case on upgrade.
The built-in server now closes idle/stalled HTTP/1.1 connections by
default (#135). Because a connection timer is now installed (see the
Added entry above), the 30-second header read timeout is active by default
where previously no timer was installed and it never fired. An HTTP/1.1
keep-alive connection that sits idle for more than 30 seconds between
requests — or that opens and never finishes sending request headers — is now
closed. Connect/gRPC traffic is predominantly HTTP/2 and is unaffected, but
an HTTP/1.1 client that pools a connection across long idle gaps must
reconnect. Raise the duration with with_header_read_timeout(Some(d)) or
disable it with with_header_read_timeout(None).
Http2Connection::with_builder_plaintext / with_builder_tls (#197). Use
Http2Connection::builder() and configure via the proxied keep-alive /
window-size setters or h2_settings(|b| ...). The old names collide with the
new builder() entry point, where "builder" now means
Http2ConnectionBuilder rather than hyper's HTTP/2 builder.content-type starting with application/grpc, so a gRPC client could
accept gRPC-Web framing, and a proto-configured client could try to decode
JSON bytes as proto. Validation now mirrors connect-go's
grpcValidateResponseContentType: the exact configured subtype and the
bare family type (application/grpc / application/grpc-web, which imply
proto and are what proxies such as Envoy send on trailers-only error
replies) are accepted, with ; parameter suffixes stripped; a same-family
codec mismatch is rejected as internal and anything else as unknown,
with the expected content type named in the error message. A missing
content-type header remains accepted. This also covers gRPC / gRPC-Web
client-streaming responses, which share the unary parse path.internal (#192,
#201). A streaming Connect response whose END_STREAM envelope body was not
valid JSON was silently treated as a clean Ok(None) close (the parse error
fell through unwrap_or_default()). It now returns Err(internal) with the
serde error in the message, matching connect-go's behaviour for the same
case, and the error carries the response headers on both the
server-streaming and client-streaming paths. Well-formed END_STREAM payloads
(including the null-error, missing-code, and unknown-field conformance
shapes) are unchanged.Err(internal) (see #168); complete responses are unchanged.ci: check generated code freshness by @Yong-yuan-X in #126
connectrpc-health crate by @torkelrogstad in #128Full Changelog: v0.6.1...v0.7.0
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 offers to_owned_message() (zero-copy from the retained body
bytes), to_owned_view() (a 'static view for pass-through responses),
bytes(), and view(). The borrow may be held across .await
points; the response — and anything moved into tokio::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, is Send + 'static, Derefs
to the buffa-generated FooOwnedView wrapper 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 the OwnedView, so resp.view().field works directly on
the client (#143).
extern_path targets must be buffa ≥ 0.7.0 generated code with views
enabled (#143). Request types resolved through extern_path
(e.g. well-known types from buffa-types) use the same
ServiceRequest/StreamMessage wrappers as local types, backed by
buffa::HasMessageView impls that buffa emits with each message.
buffa-types 0.7+ qualifies; a crate generated with older buffa or with
views disabled fails to compile with a missing HasMessageView impl
(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-body Encodable impls are still not emitted for extern
output types — return the owned message for those.
The workspace requires buffa/buffa-types/buffa-codegen 0.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) made ServerStream::message() /
BidiStream::message() return Ok(None) — indistinguishable from a
clean close — with the error retrievable only via the easy-to-miss
error() accessor. A caller that treated Ok(None) as success would
silently swallow every failed streaming RPC. message() now returns
Err(...) for an errored end and reserves Ok(None) for a clean end
(gRPC status OK / error-free END_STREAM), matching tonic. Every
Err is terminal and sticky — re-polling returns the same error,
never a clean-looking Ok(None); error() and trailers() remain
populated for post-hoc inspection. Callers that only match Err need
no changes; callers doing the Ok(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-status
is now an error instead of a clean Ok(None) (#159): internal
when no trailers arrived at all, unknown when trailers arrived
without a grpc-status, and unknown for a present-but-malformed
grpc-status value — each matching grpc-go (and the conformance
suite's primary expectations). A Trailers-Only response carrying
grpc-status: 0 in 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 as unavailable —
ships in this release as #140 (see Fixed below); this entry covers
only the gRPC and gRPC-Web protocols.
connectrpc-health crate (#128) — the standard
grpc.health.v1.Health service for connectrpc routers, wire-compatible
with grpc_health_probe, kubelet grpc: probes, and gRPC-aware service
meshes (Linkerd, Istio). install_static(router, [names]) mounts a
StaticChecker-backed service in one call; implement the Checker
trait for custom logic (e.g. reporting NotServing while a dependency
is down). The generated HealthClient re-export is gated on the
default-on client feature so server-only deployments can drop the
client transport stack. See the
health checking section of the guide.ServiceRequest<'a, Req> and StreamMessage<M> (#143) — the
request wrappers described above, exported from the crate root.Heartbeat(google.protobuf.Empty) → google.protobuf.Timestamp RPC exercising well-known types as direct RPC
input and output (#143).connectrpc_build::Config::emit_descriptor_set(name) (#141) —
also write the input FileDescriptorSet (full transitive import
closure, regardless of descriptor source) to <out_dir>/<name> as
wire-format bytes, ready to include_bytes! and serve via
grpc.reflection.v1.ServerReflection (e.g. for grpcurl). The inverse
of Config::descriptor_set, which reads a precompiled set; build
scripts no longer need a second protoc --descriptor_set_out pass.connectrpc-reflection crate (#157) — gRPC server reflection,
wire-compatible with grpc.reflection.v1.ServerReflection and its
v1alpha predecessor, so grpcurl, buf curl, Postman, and grpcui
work against connectrpc servers over gRPC, gRPC-Web, and Connect alike.
Build a Reflector from emit_descriptor_set output (responses carry
the compiler's original per-file descriptor bytes) or adopt an existing
buffa_descriptor::DescriptorPool (e.g. the descriptor_pool()
accessor emitted by reflection-enabled buffa codegen); install(router, reflector) mounts both protocol versions. Reflector::with_services
curates the advertised service list, mirroring Go grpcreflect's
Namer. The service is self-describing: queries about
grpc.reflection.* fall back to the crate's own descriptors and
ListServices advertises the reflection services, matching grpc-go —
no setup needed for schema-free clients like buf curl. The
multiservice example mounts it with both sources selectable via
REFLECTION_SOURCE=fds|pool;
examples/multiservice/reflection-demo.sh walks through discovery
and schema-free calls with buf curl.message() now
returns Err with code unavailable (matching connect-go) instead of
a clean Ok(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.DeadlinePolicy::with_inter_message_timeout
armed 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 with deadline_exceeded before 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.invalid_argument
instead of internal (#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 to data_loss so callers are not told their
request was invalid. The client-side remap deliberately diverges from
connect-go, which reports invalid_argument in both directions;
data_loss is more descriptive of what actually happened. The default
CompressionProvider::decompress_with_limit implementation (used by
custom providers that only implement decompressor) follows the same
convention: read failures now map to invalid_argument instead of
internal.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.with_default_timeout / with_max
bounded only handler execution and a slow-sending client could hold the
request open indefinitely. One absolute deadline now covers body
receipt, handler execution, and (with enforce_on_streams) the response
stream; the deadline visible through RequestContext matches 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.examples/bazel: regenerate Cargo.lock against connectrpc 0.6.0 / buffa 0.6.0 by @iainmcgin in #124
Full Changelog: v0.6.0...v0.6.1
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.
"gzip decompression stalled: truncated or invalid deflate stream"
instead of never completing.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.bytes version is now 1.6 (#132).spec: add Spec, StreamType, IdempotencyLevel; thread through dispatch by @aknott-ant in #112
Full Changelog: v0.5.0...v0.6.0
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_unary takes Payload, not Bytes (#119).
The Payload carries the wire bytes plus an interceptor's lazy
decode cache; an owned-message handler calls Payload::take_message()
to reuse a decode an interceptor already paid for, instead of decoding
the same bytes twice. Generated dispatchers and Router impls follow.
Any hand-rolled impl Dispatcher must update the call_unary
signature; the streaming call_* methods are unchanged.
MethodDescriptor is now #[non_exhaustive] (#112). It gains a
spec: Option<Spec> field and from_kind / with_idempotent /
with_spec const builders. Hand-rolled impl Dispatcher blocks that
constructed MethodDescriptor via struct literal must switch to the
builders (MethodDescriptor::unary(idempotent),
MethodDescriptor::from_kind(kind), …); destructuring patterns need a
trailing ... Reads of the existing kind / idempotent pub fields
are unaffected.
AnyMessage gained a required into_any method (#119). The
blanket impl<T: Message + Serialize> AnyMessage for T covers every
generated message type, so this only affects manual AnyMessage
impls — which the trait docs already discourage.
Interceptor trait, Next continuation, and server registration
(#114). Interceptor is an async_trait with default-passthrough
intercept_unary(req, next). Next<'_> holds the rest of the chain;
next.run(req).await invokes it (consume-once, enforced by the type
system); not calling it short-circuits. Register with
ConnectRpcService::with_interceptor(...). The first-registered
interceptor runs outermost, matching connect-go::WithInterceptors.
A service with no interceptors pays one is_empty() branch and no
per-request allocation. The unary_interceptor helper turns a closure
returning a boxed future into an Interceptor for one-off use, and
#[connectrpc::async_trait] is re-exported so downstream crates don't
need an async-trait dep.
Interceptor::intercept_streaming (#121). One Stream-shaped
hook covers server-streaming, client-streaming, and bidi: interceptors
receive an inbound PayloadStream and a NextStream<'_> continuation
and return a StreamResponse carrying the outbound PayloadStream,
response headers, trailers, and a compression hint. Cross-stream
coordination is shared state captured by the inbound and outbound
adapter closures. The streaming_interceptor closure helper mirrors
the unary one. The empty-chain fast path is preserved.
with_interceptor_arc (#118). Register an already-Arc'd
Arc<dyn Interceptor> so several ConnectRpcService instances can
share one interceptor (a process-wide auth token cache, rate-limit
counter, connection pool). with_interceptor is now a thin wrapper
over it.
Payload and AnyMessage (#113). Payload holds the wire
Bytes + CodecFormat plus a lazy decode cache and an optional
replacement (set_message). Typed access via message::<M>() (decode
once, proto and JSON), view::<V>() (zero-copy proto view), and
take_message::<M>() (#119, consume the cache or decode fresh).
Most interceptors only read Spec and headers, never the body — so
the body is never decoded unless someone asks. AnyMessage is the
object-safe surface for type-erased messages, with a blanket impl over
every T: Message + Serialize (no codegen required).
Spec static method metadata (#112, refs #87). New
connectrpc::spec module with Spec, StreamType, IdempotencyLevel,
and SpecOrigin types describing a single RPC method: its
fully-qualified procedure path ("/package.Service/Method"), message
flow shape, proto-declared idempotency contract, and which generated
artifact (server or client) produced it. Spec is Copy and 'static,
with const fn constructors (Spec::server(...), Spec::client(...))
so generated Spec constants live in .rodata. Code generation emits a
pub const <SERVICE>_<METHOD>_SPEC: Spec per method that user code can
reference directly — this also closes #110, which asked for
connect-go-style procedure-path constants.
RequestContext::spec(), protocol(), and path() (#112,
#116). spec() returns the resolved Spec for 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 a Spec attached.
Router::with_spec (#120). The dynamic Router can now carry a
Spec per route, attached after registration via
.with_spec(SPEC_CONST). The generated register() chains it after
every route_* call, so handlers and interceptors see the same
RequestContext::spec() whether the host wired up the codegen
dispatcher or the dynamic Router. Routes registered without
with_spec behave exactly as before (spec() == None).
HttpClientBuilder with connect_timeout (#117).
HttpClient::builder().connect_timeout(dur).plaintext() (or
plaintext_http2_only(), with_tls(...)) bounds the TCP connect(2)
call so a silently dropped SYN fails in milliseconds instead of the
kernel's tcp_syn_retries default (~130s). The existing constructors
delegate to the builder with no timeout, so behaviour is unchanged for
current callers. (Note: [#197] in 0.8.0 changes this — the constructors
are now bounded by default.) connect_timeout covers TCP connect only, not DNS
resolution or the TLS handshake — use CallOptions::with_timeout for
an end-to-end bound.
Server::with_interceptor and Server::with_interceptor_arc
(#123). Proxies to the same methods on ConnectRpcService, completing
the proxy set started in #105. Standalone Server users can now
register interceptors without dropping down to
Server::from_service(ConnectRpcService::new(...).with_interceptor(...)).
handler: PreEncoded body type + Encodable items for streaming handlers by @rpb-ant in #98
Full Changelog: v0.4.2...v0.5.0
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.
with_* setters and MessageName impls in generated message
types. Consumers must align their direct buffa / buffa-types
dependencies to 0.6 to 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 return ServiceStream<Self::Item> instead of ServiceStream<Res> —
brings StreamingHandler, BidiStreamingHandler,
ViewStreamingHandler, and ViewBidiStreamingHandler to parity with
unary Handler::Body (added in 0.4.0). Stream items can now be
PreEncoded, MaybeBorrowed, or any Encodable<Res>; previously
they had to be the owned Res itself.
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_fn closure helpers, neither of which
is affected — codegen handles the new shape and the helpers infer
type Item from the closure return. Hand-rolled impl StreamingHandler blocks must add type 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 of ServiceStream<Out>. Existing impl <Service> blocks
that return ServiceStream<Out> compile unchanged via RPITIT
refinement (the same mechanism the unary path used since 0.4.0; the
refining_impl_trait lint suppression documented in 0.4.1 covers the
streaming case too). Handlers that want to yield PreEncoded items
must do so from 'static data — the use<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-rust output must
regenerate (the same regeneration footgun documented in 0.4.0).
connectrpc-build users (build.rs) are unaffected.
dispatcher::codegen::encode_response_stream gains a B type
parameter (#98) for the stream item type. The generated dispatcher and
route-registration code passes Res explicitly because the trait
method's stream item is the opaque impl Encodable<Out> (RPITIT),
which can't be unified against the Encodable<Res> impls. Only
consumers that call dispatcher::codegen::encode_response_stream
directly need to turbofish encode_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)).
RequestContext is now #[non_exhaustive] with pub(crate) fields
and accessor methods (#101). Direct field reads — ctx.headers,
ctx.deadline, ctx.extensions — must move to ctx.headers(),
ctx.deadline(), ctx.extensions(). Construction via
RequestContext::new(headers) and the with_* builders is unchanged.
New (additive) accessors landed alongside the change:
ctx.time_remaining() — saturating Duration until the deadline,
for budgeting downstream calls.ctx.extensions_mut() — mutable extensions, for tower middleware
that builds a RequestContext directly.ctx.peer_addr() (server feature) and ctx.peer_certs()
(server-tls feature) — typed extension lookups for the well-known
peer types. They return None when the transport didn't insert the
value, replacing the panic-prone
ctx.extensions.get::<PeerAddr>().unwrap() pattern.ClientConfig and CallOptions fields are now pub(crate) and the
ClientConfig setter methods are renamed to with_* (#100). Both
structs are #[non_exhaustive], so struct-literal and
functional-update construction was already rejected by the compiler
outside the crate (E0639); the pub fields 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, the ClientConfig setter methods were renamed to the
with_* form already used by CallOptions, and one CallOptions
setter 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 remaining CallOptions
builders (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 supplied on a ClientConfig builder call, you've hit the rename:
the same-named read accessor now occupies the old name. Prefix the
call with with_ per the table above.
Server / BoundServer / ServeTls builders renamed to with_*
(#105) — tls_handshake_timeout(...) is now with_tls_handshake_timeout(...)
(on Server, BoundServer, and axum::ServeTls) and
BoundServer::http1_keep_alive(...) is now
with_http1_keep_alive(...). This matches the with_tls(...) /
with_graceful_shutdown(...) siblings that were already with_* and
the ConnectRpcService/ClientConfig convention. Migration: prefix
the call with with_. with_tls(...) is unchanged.
PreEncoded<M> response body (#98) — wraps already-encoded protobuf
bytes and satisfies Encodable<M>. Use when the handler builds and
encodes a borrowing view internally — e.g. a *View<'a> borrowing
from a local snapshot held in Arc — rather than returning the view
itself. The 'static bound on handler bodies and stream items means a
view with a non-'static lifetime can't cross the handler boundary;
PreEncoded carries the bytes across instead.
The M type parameter is a compile-time witness. Three construction
paths, in decreasing order of guarantee:
PreEncoded::from_message(&m) (also From<&M>/.into()) encodes an
owned M and the receiver type is the witness;
PreEncoded::from_view(&view) enforces M = MView::Owned;
PreEncoded::from_bytes_unchecked(bytes) wraps already-encoded bytes
from elsewhere — a cache, a blob store, a sidecar — and takes M on
trust (in debug builds it also decodes once as a debug_assert!).
Optimized for the proto codec, where the bytes pass through verbatim.
JSON requests fall back to decoding the proto bytes as M and
re-serializing — slow, but a working response rather than a runtime
unimplemented error. Services with significant JSON traffic should
build and return the owned message (or MaybeBorrowed::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 via
Connect-Timeout-Ms / grpc-timeout; without a policy the server
trusts that value verbatim. DeadlinePolicy clamps 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) or Server::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 least with_max(...) to
bound worker lifetime and with_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 a
deadline_exceeded error 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
than d between yielded items (a stalled handler waiting on a slow
upstream). Takes effect whenever set, with or without
with_enforce_on_streams.When clamping changes a client value, a tracing::debug! event fires
on target connectrpc::deadline with the path and before/after
durations. Per-route deadline policy is a planned follow-up coupled
to the typed routing surface (#91).
Server proxies for ConnectRpcService dispatch config (#105) —
Server::with_limits, Server::with_compression, and
Server::with_compression_policy delegate to the same-named
ConnectRpcService builders, so a Server::new(router) user no longer
has to drop down to Server::from_service(...) to set request limits
or compression. (Server already held the inner ConnectRpcService;
the proxies just expose the existing surface.)
Server::with_http1_keep_alive (#105) — Server already had an
http1_keep_alive field (used by serve/serve_with_graceful_shutdown)
but no builder to set it; only BoundServer did. Adds the missing
builder so a one-step Server::new(router).serve(addr) user can disable
HTTP/1.1 keep-alive without switching to the bind/from_listener
two-step path.
ConnectError::into_http_response(headers) (#111) — renders a
ConnectError as a protocol-correct http::Response<ConnectRpcBody>
for the protocol detected from the inbound request headers. Tower
layers wrapping ConnectRpcService (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 with grpc-status trailers, Connect-streaming
clients get an EndStreamResponse envelope, 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 absent Content-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-tls transports,
server / server-tls types, gzip / zstd / streaming
compression, and axum integration. Annotations show the minimal cfg
expression after Cargo feature implications collapse (client-tls
rather than all(client, client-tls)); all(...) is used only where
neither feature implies the other. No runtime change.
connectrpc-build: .files() no longer emits cargo:rerun-if-changed
in Buf mode (#59, thanks @hobostay). When Config::files(...) is
used with Config::use_buf(), the listed entries are proto-relative
names as they appear in the buf module (e.g. "my/service.proto"), not
filesystem paths. Emitting cargo:rerun-if-changed for 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 to Precompiled mode. Manual protoc mode is unchanged.axum: serve_tls helper + mtls-identity example by @iainmcgin in #80
Full Changelog: v0.4.1...v0.4.2
connectrpc::axum::serve_tls ([#80]). Companion to serve that
hands off to the standalone Server for the TLS path — wrapping axum
with tokio_rustls::TlsAcceptor directly hangs on h2 ALPN
negotiation. Comes with an examples/mtls-identity example showing
client-cert extraction in a handler.connectrpc: server feature now enables tokio/macros ([#80]).
The accept loop in Server::serve and the new axum::serve_tls both
use tokio::select!, but the server feature only enabled
tokio/net. Crates depending on connectrpc = { features = ["server"] } only compiled when something else in the dependency closure enabled
tokio/macros for them. Our conformance suite and examples both have
tokio = { features = ["macros", …] } in dev-deps, which kept the gap
hidden in CI.
connectrpc-build: generated mod.rs #[allow(...)] is now sourced
from buffa_codegen::ALLOW_LINTS. The hardcoded list had drifted
behind buffa's: it was missing clippy::uninlined_format_args (which
buffa enum JSON deserialize errors trip), clippy::doc_lazy_continuation,
and clippy::module_inception. The pub mod <pkg> tree wraps buffa's
per-proto split output (Owned/View/Oneof/Ext/PackageMod) plus our own
__connect.rs companions, and the per-proto Owned content has no
#[allow(...)] of its own — buffa scopes package_mod_allow_attr() to
__buffa and protoc-gen-buffa-packaging covers the rest with an
inner #![allow(...)] that has no analogue in connectrpc-build's
outer-mod layout. Sourcing from ALLOW_LINTS (chained with the
connectrpc-build-specific impl_trait_redundant_captures) keeps the
two from drifting again. Bumps the buffa-codegen dependency floor to
0.5.1, where unused_qualifications landed in ALLOW_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.
ci: trigger publish-crates on tag push by @iainmcgin in #85
Full Changelog: v0.4.0...v0.4.1
connectrpc-build: generated mod.rs #[allow(...)] now suppresses
unused_qualifications and impl_trait_redundant_captures. The 0.4.0
trait method RPIT carries a use<'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 the pub mod <pkg> tree and don't touch
hand-written code.
Also documenting one related lint with no codegen-side workaround:
refining_impl_trait_internal (warn by default since rust 1.86,
rust-lang/rust#121718) fires on every handler impl, because the
generated trait declares ServiceResult<impl Encodable<Out> + ...>
while the handler returns ServiceResult<Out>. The refinement is
intentional — it is what lets handlers return either an owned
message, a borrowed view, or a MaybeBorrowed<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 set
refining_impl_trait_internal = "allow" in [lints.rust] (or
workspace lints) or #[allow(refining_impl_trait)] on each handler
impl block.
Bring graceful shutdown in line with Go's net/http.Server.Shutdown behavior by @rpb-ant in #58
Full Changelog: v0.3.3...v0.4.0
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::GeneratedFileKind is
now #[non_exhaustive]. This has no effect on connectrpc runtime
consumers; build integrations that consume connectrpc-codegen and
match GeneratedFileKind exhaustively 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
at pkg::FooView, oneof enums at pkg::msg_name::Kind, and oneof
view enums at pkg::msg_name::KindView — so the canonical
pkg::__buffa::view::FooView / pkg::__buffa::oneof::msg_name::Kind
paths 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.rs
companion 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 files
GeneratedFileKind::Companion and wires them into the per-package
stitcher with buffa_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 inspect GeneratedFileKind should now match Companion for
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.value is now
bytes::Bytes (was Vec<u8>). buffa's size cache is externalized
(buffa#22): generated structs no longer carry __buffa_cached_size,
and Message::compute_size/write_to take &mut SizeCache. The
provided encode_to_bytes() / encoded_len() are unchanged;
connectrpc itself only uses those, but direct callers of
compute_size() should switch to encoded_len().
connectrpc-codegen: Options now embeds the buffa
CodeGenConfig directly as Options::buffa instead of mirroring
individual fields (#34). The previous per-field shims
(strict_utf8_mapping, generate_json, extern_paths,
emit_register_fn) are gone; set options.buffa.<field> instead.
CodeGenConfig is re-exported from connectrpc_codegen::codegen and
connectrpc_build. connectrpc_build::Config keeps its existing
builder methods as thin shims and gains .buffa_config(cfg) for
wholesale replacement. generate_views = true is still enforced.
ConnectError shrunk from 248 to 72 bytes (#61). The
response_headers and trailers fields are now crate-private
Option<Box<http::HeaderMap>> (was pub http::HeaderMap), so
Result<_, ConnectError> no longer trips
clippy::result_large_err. New accessors replace direct field
access: response_headers() / trailers() (borrow, empty map if
unset), response_headers_mut() / trailers_mut(), and
set_response_headers() / set_trailers(). The with_headers() /
with_trailers() builders keep their signatures. Behaviour notes:
with_headers / with_trailers / set_* now normalize an empty
HeaderMap to "unset" (observationally identical via the
accessors), and the Debug output for an unset map now shows
None instead of {}.
Handler signatures redesigned (#7): the generated service
trait no longer threads a single Context in and out. Handlers
now receive a read-only RequestContext (headers, deadline,
extensions) and return ServiceResult<B> =
Result<Response<B>, ConnectError>, where Response<B> carries
the body plus optional response headers/trailers/compression hint.
Unary and client-stream methods return
ServiceResult<impl Encodable<Out>>; server-stream and bidi
return ServiceResult<ServiceStream<Out>>. Response::ok(body) is
the bare-body happy-path shorthand; for streaming bodies use
Response::stream_ok(s). Encodable<M> is the new "encodes as
M" bound on response bodies. The old Context type 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 emits impl Encodable<Out> for OutView<'_> and for OwnedView<OutView<'static>> per RPC output
type (proto via ViewEncode; JSON returns an unimplemented
error since view types lack Serialize). The new
MaybeBorrowed<M, V> enum lets a handler return either: see
benches/rpc/benches/filter_handler.rs for a redaction example
(~1.65x at the codec layer when no modification is needed).
ViewHandler/ViewClientStreamingHandler now take CodecFormat
and return the response already encoded, dropping the Res type
param.
GzipProvider defaults tuned for throughput: the default
compression level is now 1 (was 6), and flate2 is built with
the zlib-rs backend (pure-Rust port of zlib-ng) instead of
miniz_oxide. Together this is ~2.7× throughput on the
unary/large_gzip bench. Gzip wire format is unchanged; payloads
compressed at level 1 are larger than at level 6. Restore the old
ratio with GzipProvider::with_level(6). Note that Cargo feature
unification means the zlib-rs backend also applies to any other
flate2 use in the same dependency graph.GzipProvider::DEFAULT_LEVEL and ZstdProvider::DEFAULT_LEVEL are
now public constants.StreamingCompressionProvider::compress_stream (gzip and zstd) now
honors the provider's configured level; previously it ignored
self.level and used async-compression's default.connectrpc no longer pulls axum's default features (#55),
which transitively required tokio/net → mio and made the crate
impossible to use on WASM hosts that integrate with axum (e.g.
Cloudflare Workers). The axum dependency now declares
default-features = false.wasm_bindgen_futures::spawn_local
on wasm32-unknown-unknown (it was being polled inline, deadlocking
on the first .await). Native targets keep tokio::spawn.file_per_package output layout for protoc-gen-connect-rust and
connectrpc-build. When enabled (opt: file_per_package in
buf.gen.yaml, --connect-rust_opt=file_per_package with protoc, or
Config::file_per_package(true) from build.rs), the per-proto split
is collapsed to one <dotted.pkg>.rs per proto package with all
service stubs inlined and no <pkg>.mod.rs stitcher — matching the
<dotted.package>.rs filename convention protoc-gen-buffa produces
under its own file_per_package option (buffa#73) and that BSR cargo
SDK generation and tonic-style build integrations expect (module tree
synthesised from filenames). The two plugins generate disjoint content
(buffa: message types, connect-rust: service stubs); set
file_per_package on both. In the connectrpc-build path service
stubs are inlined into buffa's per-package PackageMod rather than
written as <stem>.__connect.rs siblings; the include file picks up
the new filename automatically and consumer code is unaffected. When
using the protoc plugin from buf generate, drop the
protoc-gen-buffa-packaging invocations under this layout — there
are no per-file content files or stitchers for it to wire — and keep
routing file_per_package output to its own directory: the filename
matches protoc-gen-buffa's and would silently overwrite in a shared
one. See CodeGenConfig::file_per_package for the
strategy: directory constraint.connectrpc::include_generated!(): shorthand macro for
include!(concat!(env!("OUT_DIR"), "/_connectrpc.rs")). An optional
filename argument (note: a filename including .rs, not a proto
package name as in tonic::include_proto!) supports projects that
customise the output via Config::include_file (#50).connectrpc-build: Config::emit_rerun_directives(bool) to suppress
the cargo:rerun-if-changed= lines when running outside a Cargo
build.rs context (e.g. from a Bazel genrule or standalone host tool).
Default remains true.Pin MSRV to Rust 1.88 and verify in CI by @iainmcgin in #44
Full Changelog: v0.3.2...v0.3.3
connectrpc-build no longer emits invalid
cargo:rerun-if-changed directives in Precompiled input mode
(#56). When a precompiled FileDescriptorSet was supplied instead
of .proto source files, .files() paths were still being passed
through to cargo, causing spurious rebuild triggers on paths that
don't exist in that mode.Cargo.toml and adds an
explicit CI check.Generated service code now compiles when multiple services are `include!`d into the same Rust module ([#32]). The codegen previously emitted top-level
include!d into the same Rust module (#32). The codegen previously
emitted top-level use statements 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 same mod block.*_SERVICE_NAME
const (#16) instead of repeating the fully-qualified service name
as a string literal at every call site. Matches the server-side
router.tokio feature footprint narrowed (#19). The published
connectrpc crate previously inherited the full workspace tokio
feature set (macros, net, signal, rt-multi-thread, ...) when
workspace = true was inlined at publish time. It now requests only
rt, io-util, sync, time, plus net when the client or
server feature is enabled. Downstream crates that use tokio
directly should declare their own features rather than relying on
transitive activation.wasm32-unknown-unknown target compatibility (#19) for the
connectrpc crate with default features off. A new
examples/wasm-client demonstrates a Fetch-based ClientTransport
implementation with browser-based integration tests via wasm-pack.
Currently exercises unary calls without deadlines; timeouts and
streaming require additional setup beyond the example.`emit_register_fn` option ([#35]) on connectrpc_codegen::Options and connectrpc_build::Config, plumbing through to buffa_codegen::CodeGenConfig::emit_
emit_register_fn option (#35) on connectrpc_codegen::codegen::Options
and connectrpc_build::Config, plumbing through to
buffa_codegen::CodeGenConfig::emit_register_fn. Set to false to suppress
the per-file register_types(&mut TypeRegistry) aggregator when multiple
generated files are include!d into the same module (the identically-named
functions would otherwise collide). The protoc plugin accepts a matching
no_register_fn parameter for path-compat with the unified connectrpc-build
flow.Upgraded `buffa` to 0.3.0 ([#24]). buffa 0.3 renames AnyRegistry to TypeRegistry (with JsonAnyEntry and register_json_any() replacing the old AnyTypeE
buffa to 0.3.0 (#24). buffa 0.3 renames AnyRegistry to
TypeRegistry (with JsonAnyEntry and register_json_any() replacing the
old AnyTypeEntry / register()). Generated code and the runtime crate
now use the new types; users who construct a registry manually for
google.protobuf.Any JSON encoding will need to migrate.connectrpc-build only rewrites output files when content changes
(#22). Preserves mtimes so touching one .proto no longer triggers a
full downstream recompile of every generated .rs file. Mirrors
prost-build's write_file_if_changed.Context::extensions. The built-in server inserts PeerAddr
(always) and PeerCerts (when server-tls is enabled and the client
presented a certificate chain) into every request's extensions; handlers
read them with ctx.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=false for 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 any tower::Service<Uri>
yielding a hyper::rt::Read + Write stream and the library runs the h2
handshake over it. lazy_unix / connect_unix are thin wrappers for
Unix domain sockets.to_snake_case
(#28). rpc GetFoo(...) and rpc get_foo(...) in the same service
previously emitted duplicate fn get_foo and 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_options client variant.rpc Move(...) previously emitted fn move(...)
and failed at build-script time. Method idents are now routed through
buffa's keyword escaper, producing r#move (or a _ suffix for the four
keywords that cannot be raw identifiers).service Self {} no longer generates trait Self (#27). The handler
trait is suffixed to Self_; the SelfExt / SelfClient / SelfServer
derivatives are unaffected since the suffix already de-keywords them.`BidiStream` half-duplex deadlock on `SharedHttp2Connection` ([#2], [#4]). call_bidi_stream stored the transport's send() future unpolled, so for tran
BidiStream half-duplex deadlock on SharedHttp2Connection (#2, #4).
call_bidi_stream stored the transport's send() 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 first message() call. The half-duplex pattern (send all, close,
then read) would buffer into the 32-deep ChannelBody mpsc with nobody
draining it and deadlock on the 33rd send. The send future is now
spawned so the request streams immediately.Uri::host()
returns [::1] with brackets, which rustls_pki_types::ServerName
rejected as an invalid DNS name. Brackets are now stripped so the
address parses as ServerName::IpAddress.buffa = "0.1" instead
of "0.2". The connectrpc crate bakes the workspace README via
readme = "../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 p
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
ConnectRpcService<D> — framework-agnostic, works with Axum, Hyper, etc.FooServiceServer<T> dispatcher (compile-time method dispatch, no dyn Handler vtable)Router with runtime registration for multi-service or reflection use casesCompressionProvider trait; 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
honest poll_ready, composes with tower::balance for N-connection load spreading::new() — plaintext vs TLS is an explicit choiceArc<rustls::ClientConfig>, preserving dynamic cert rotation through
Arc<dyn ResolvesClientCert>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 via with_client_cert_verifier()Generated clients
foo(req) (uses config defaults) + foo_with_options(req, opts)ClientConfig carries defaults for timeout, max message size, and headers — applied
automatically by the no-options methodDEFAULT_MAX_MESSAGE_SIZE) when no explicit limit is configured — matching
connect-go. Server: raise via Limits::max_message_size. Client: raise via
ClientConfig::default_max_message_size or CallOptions::max_message_size.DEFAULT_TLS_HANDSHAKE_TIMEOUT);
configure via Server::tls_handshake_timeout.connect-timeout-ms
is capped at 10 digits and grpc-timeout at 8 digits (matching connect-go).
Over-spec values are treated as no-timeout. Prevents a malicious client from
triggering a per-request panic via Instant + Duration overflow. Deadline
computation also uses checked_add as defense in depth.connectrpc-codegen — descriptor → Rust source libraryconnectrpc-build — build.rs integration (protoc/buf → codegen → OUT_DIR)protoc-gen-connect-rust — protoc plugin binaryGenerated code emits service traits, FooServiceServer<T> monomorphic dispatchers,
FooServiceClient<T> clients, and buffa message types via buffa-codegen.
vs tonic 0.14 (same hyper/h2 stack), Intel Xeon 8488C:
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.
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →