pub struct PreEncoded<M> { /* private fields */ }Expand description
Pre-encoded protobuf response body for message type M.
Use when the handler builds and encodes a borrowing view internally —
e.g. a FooView<'a> borrowing from a local snapshot — rather than
returning the view itself. The 'static bound on Handler::Body (and
on streaming items, see the use<Self> note in the
StreamingHandler docs) 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 for which RPC output
type the bytes encode. Three construction paths, in decreasing order
of compile-time guarantee:
from_message(&m)— encodes an ownedM; the receiver type is the witness.from_view(&view)— encodes a borrowing view;MessageView::Owned = Mis the witness.from_bytes_unchecked(bytes)— wraps already-encoded bytes from elsewhere (a cache, storage, another service). No witness; you’re asserting the bytes decode asM.
from_message and from_view produce the same PreEncoded<M> type,
so a stream can mix items built either way (e.g. a cache-hit path
returning the cached owned M, a cache-miss path building a view from
a snapshot) — the same role MaybeBorrowed fills for unary
handlers, but with the encode happening eagerly inside the stream
body.
§Streaming example
The motivating shape — a server-streaming handler that builds and encodes per-item views borrowing from a local store snapshot, then yields the bytes:
use connectrpc::{PreEncoded, Response, RequestContext, ServiceResult, ServiceStream};
async fn watch(
&self,
_ctx: RequestContext,
req: OwnedWatchRequestView,
) -> ServiceResult<ServiceStream<PreEncoded<WatchResponse>>> {
let store = self.store.clone();
let stream = futures::stream::unfold(store, |store| async move {
let snapshot = store.load();
// `view` borrows from `snapshot`; encode while the borrow is live.
let view = build_view_from_snapshot(&snapshot);
let item = PreEncoded::from_view(&view);
Some((Ok(item), store))
});
Response::stream_ok(stream)
}For a unary handler, the same pattern applies — return
ServiceResult<PreEncoded<MyResponse>>.
§Codec behaviour
PreEncoded is optimized for the proto codec: the wrapped bytes are
passed through verbatim with no re-encoding. The motivating use case
(high-throughput fanout) is proto-only.
For the json codec, PreEncoded falls back to decoding the bytes as
M and re-serializing as JSON. This is correct but not fast — a
full proto decode plus a JSON serialize per response (or per stream
item). The fallback exists so that registering a PreEncoded handler
on a JSON-capable router degrades gracefully instead of returning a
runtime error. If your service serves a meaningful JSON traffic share,
build and return the owned message (or MaybeBorrowed::Owned)
instead — that lets the codec layer pick the right encoding without
the proto round-trip.
If the wrapped bytes don’t decode as M (e.g. you passed mismatched
bytes to from_bytes_unchecked),
the JSON path returns an internal
error at the server; the proto path passes the bytes through and the
client sees a decode error.
§Codec-dependent fidelity
The proto path is byte-exact; the JSON path is only as faithful as
decoding the bytes to an owned M and re-serializing. The two
diverge when the wrapped bytes carry information not representable in
M itself:
- Unknown fields (proto bytes encoded against a newer schema
than the server’s
M) are preserved on the proto path and dropped on the JSON path. This matters only forfrom_bytes_uncheckedbytes sourced externally; bytes produced byfrom_message/from_viewcannot carry unknown fields. - Non-canonical proto encodings (out-of-order fields, redundant
length prefixes, repeated non-
repeatedfields) are passed through verbatim on the proto path and normalized by the decode on the JSON path.
If byte-exact fidelity across codecs matters (e.g. signature
verification, content-addressed storage), do not use PreEncoded with
JSON-capable routes.
§Cost is selected by the client
The codec is chosen per-request by the client’s Content-Type header.
For a service that adopted PreEncoded for proto throughput, a client
sending JSON requests (intentionally, by misconfiguration, or
adversarially) shifts those requests onto the slow decode-reserialize
path. The marginal cost is bounded by the response size and is usually
small relative to the handler’s own work, but a streaming RPC pays it
per item. A service that wants to enforce proto-only should reject
non-proto Content-Type at the middleware layer (e.g. an axum
middleware that returns 415 Unsupported Media Type) rather than rely
on the body type — that keeps the policy outside the handler and
applies before the request body is read.
§Contract
PreEncoded is a transparent byte container — it does not validate
the wrapped bytes on the proto path. PreEncoded::from_view gives a
compile-time witness via MessageView::Owned = M;
PreEncoded::from_bytes_unchecked trusts the caller. Returning bytes
that don’t decode as M will produce decode errors on the client (or,
for JSON clients, an internal error from the server-side fallback
decode).
Implementations§
Source§impl<M: Message> PreEncoded<M>
impl<M: Message> PreEncoded<M>
Sourcepub fn from_message(msg: &M) -> Self
pub fn from_message(msg: &M) -> Self
Encode an owned M to protobuf bytes.
The receiver type is the compile-time witness — there’s no way to
produce a PreEncoded<M> from a &Other. This is the right
constructor when the handler builds an owned M and wants to
share the encoding (e.g. encode once, clone the
Bytes-backed PreEncoded for N readers in a fanout) or when a
stream needs to mix owned-message and view-built items under a
single type Item = PreEncoded<M>.
Equivalent to PreEncoded::from_bytes_unchecked(m.encode_to_bytes()),
but with M enforced by the type system rather than asserted by
the caller.
Sourcepub fn from_view<'a, V>(view: &V) -> Selfwhere
V: ViewEncode<'a> + MessageView<'a, Owned = M>,
pub fn from_view<'a, V>(view: &V) -> Selfwhere
V: ViewEncode<'a> + MessageView<'a, Owned = M>,
Encode a ViewEncode view to protobuf bytes.
The MessageView<'a, Owned = M> bound is the compile-time witness
that the bytes decode as M — passing OtherView<'a> won’t
type-check unless OtherView::Owned == M.
Sourcepub fn from_bytes_unchecked(bytes: impl Into<Bytes>) -> Self
pub fn from_bytes_unchecked(bytes: impl Into<Bytes>) -> Self
Wrap already-encoded protobuf bytes without validating them.
Use when the bytes come from somewhere with no structural type
guarantee — a byte cache, a blob store, a sidecar service. You are
asserting the bytes decode as M; the proto path does not
validate this. In debug builds, the bytes are decoded once as a
debug_assert! to surface mismatches early.
Prefer from_message when you have an
owned M in hand and from_view when
you have a view — both enforce M at compile time.
Zero-copy for Bytes and Vec<u8>; passing &[u8] allocates and
copies.
Trait Implementations§
Source§impl<M> Clone for PreEncoded<M>
impl<M> Clone for PreEncoded<M>
Source§impl<M> Debug for PreEncoded<M>
impl<M> Debug for PreEncoded<M>
Source§impl<M: Message + JsonSerialize> Encodable<M> for PreEncoded<M>
impl<M: Message + JsonSerialize> Encodable<M> for PreEncoded<M>
Source§fn encode(&self, codec: CodecFormat) -> Result<Bytes, ConnectError>
fn encode(&self, codec: CodecFormat) -> Result<Bytes, ConnectError>
self as wire bytes for M in the requested format.Source§fn encode_segments(
&self,
codec: CodecFormat,
) -> Result<EncodedBody, ConnectError>
fn encode_segments( &self, codec: CodecFormat, ) -> Result<EncodedBody, ConnectError>
self as wire bytes that may arrive in several reference-counted
segments rather than one contiguous buffer. Read moreSource§impl<M: Message> From<&M> for PreEncoded<M>
Encode an owned M to a PreEncoded<M>.
impl<M: Message> From<&M> for PreEncoded<M>
Encode an owned M to a PreEncoded<M>.
Equivalent to PreEncoded::from_message; provided for .into()
ergonomics.