connectrpc/response.rs
1//! Handler request/response types.
2//!
3//! This module splits the old `Context` struct into a read-only
4//! [`RequestContext`] (passed *into* handlers) and a [`Response<B>`]
5//! wrapper (returned *from* handlers). The body type `B` is bounded by
6//! [`Encodable<M>`] in the generated trait so handlers can return either
7//! the owned message `M`, a borrowing `MView<'_>` /
8//! [`OwnedView<MView<'static>>`](buffa::view::OwnedView), or
9//! [`MaybeBorrowed`] for the conditional case.
10
11use std::marker::PhantomData;
12use std::pin::Pin;
13use std::time::{Duration, Instant};
14
15use buffa::Message;
16use buffa::view::{MessageView, ViewEncode};
17use bytes::Bytes;
18use bytes::BytesMut;
19use futures::Stream;
20use http::HeaderMap;
21use http::header::{HeaderName, HeaderValue};
22
23use crate::codec::CodecFormat;
24use crate::codec::JsonSerialize;
25use crate::codec::encode_json;
26use crate::error::ConnectError;
27
28// ---------------------------------------------------------------------------
29// RequestContext
30// ---------------------------------------------------------------------------
31
32/// Read-only request context passed to RPC handlers.
33///
34/// Carries the request headers, parsed deadline, and any
35/// connection-scoped extensions (peer address, TLS certs, auth context)
36/// inserted by a tower layer in front of the service. Handlers do *not*
37/// return this; response-side metadata lives on [`Response`].
38///
39/// `RequestContext` is `#[non_exhaustive]`: construct it with
40/// [`RequestContext::new`] and the `with_*` builders, and read fields
41/// through the accessor methods (`headers()`, `deadline()`,
42/// `extensions()`, …). New request-scoped metadata can be added in minor
43/// releases without breaking downstream code.
44#[derive(Debug, Clone, Default)]
45#[non_exhaustive]
46pub struct RequestContext {
47 /// Request headers (after protocol-prefix stripping).
48 pub(crate) headers: HeaderMap,
49 /// Absolute request deadline parsed from the protocol's timeout header,
50 /// if any. Propagate to downstream calls.
51 ///
52 /// If a [`DeadlinePolicy`](crate::DeadlinePolicy) is configured on the
53 /// service, this is the *moderated* value — clamped to the policy's
54 /// `[min, max]` range, or the policy default when the client asserted
55 /// nothing — not the raw client header.
56 pub(crate) deadline: Option<Instant>,
57 /// Request extensions carried from the underlying `http::Request`.
58 pub(crate) extensions: http::Extensions,
59 /// Static metadata for the dispatched RPC method, when known.
60 pub(crate) spec: Option<crate::spec::Spec>,
61 /// The wire protocol negotiated for this request, when known.
62 pub(crate) protocol: Option<crate::Protocol>,
63 /// The procedure path the client requested, with a leading slash.
64 pub(crate) path: Option<String>,
65 /// Decode limits for this request's message, from the service's
66 /// [`Limits`](crate::Limits). Default when the context was built outside
67 /// the service (a hand-rolled test, a tower layer), which is why this is
68 /// the service's value rather than a global.
69 pub(crate) decode_options: buffa::DecodeOptions,
70}
71
72impl RequestContext {
73 /// Create a new context with the given request headers.
74 pub fn new(headers: HeaderMap) -> Self {
75 Self {
76 headers,
77 deadline: None,
78 extensions: http::Extensions::new(),
79 spec: None,
80 protocol: None,
81 path: None,
82 decode_options: buffa::DecodeOptions::new(),
83 }
84 }
85
86 /// Set the decode limits applied to this request's message.
87 #[doc(hidden)] // set by the service from its configured `Limits`
88 #[must_use]
89 pub fn with_decode_options(mut self, options: buffa::DecodeOptions) -> Self {
90 self.decode_options = options;
91 self
92 }
93
94 /// The decode limits applied to this request's message.
95 #[doc(hidden)] // read by generated dispatch
96 #[must_use]
97 pub fn decode_options(&self) -> &buffa::DecodeOptions {
98 &self.decode_options
99 }
100
101 /// Set the request deadline (absolute `Instant`).
102 #[must_use]
103 pub fn with_deadline(mut self, deadline: Option<Instant>) -> Self {
104 self.deadline = deadline;
105 self
106 }
107
108 /// Attach request extensions captured from the underlying `http::Request`.
109 #[must_use]
110 pub fn with_extensions(mut self, extensions: http::Extensions) -> Self {
111 self.extensions = extensions;
112 self
113 }
114
115 /// Attach the static method metadata for the dispatched RPC.
116 #[must_use]
117 pub fn with_spec(mut self, spec: Option<crate::spec::Spec>) -> Self {
118 self.spec = spec;
119 self
120 }
121
122 /// Attach the negotiated wire protocol.
123 #[must_use]
124 pub fn with_protocol(mut self, protocol: Option<crate::Protocol>) -> Self {
125 self.protocol = protocol;
126 self
127 }
128
129 /// Attach the procedure path the client requested. The dispatch path
130 /// always supplies the leading-slash form (`"/package.Service/Method"`),
131 /// matching [`Spec::procedure`](crate::Spec::procedure); custom
132 /// dispatch shims and test fixtures should do the same so consumers
133 /// of [`path()`](Self::path) see a consistent shape.
134 #[must_use]
135 pub fn with_path(mut self, path: impl Into<String>) -> Self {
136 self.path = Some(path.into());
137 self
138 }
139
140 /// Request headers (after protocol-prefix stripping).
141 ///
142 /// For a single header lookup, [`header`](Self::header) is simpler.
143 pub fn headers(&self) -> &HeaderMap {
144 &self.headers
145 }
146
147 /// Get a request header value.
148 pub fn header(&self, key: impl http::header::AsHeaderName) -> Option<&HeaderValue> {
149 self.headers.get(key)
150 }
151
152 /// Absolute request deadline parsed from the protocol's timeout header
153 /// (`Connect-Timeout-Ms` or `grpc-timeout`), if the client asserted one.
154 ///
155 /// Propagate this to downstream calls so the whole call chain shares a
156 /// single budget. For the remaining budget as a `Duration`, see
157 /// [`time_remaining`](Self::time_remaining).
158 ///
159 /// If a [`DeadlinePolicy`](crate::DeadlinePolicy) is configured on the
160 /// service, this is the *moderated* value — clamped to the policy's
161 /// `[min, max]` range, or the policy default when the client asserted
162 /// nothing — not the raw client header.
163 pub fn deadline(&self) -> Option<Instant> {
164 self.deadline
165 }
166
167 /// Time remaining until the request deadline, saturating at zero.
168 ///
169 /// `None` if the client did not assert a timeout. Use this to budget
170 /// downstream calls — for example, subtract a margin before passing the
171 /// remainder as a downstream RPC's per-call timeout. See also issue
172 /// [#92](https://github.com/anthropics/connect-rust/issues/92) for
173 /// server-side deadline enforcement.
174 pub fn time_remaining(&self) -> Option<Duration> {
175 self.deadline
176 .map(|d| d.saturating_duration_since(Instant::now()))
177 }
178
179 /// Request extensions carried from the underlying `http::Request`.
180 ///
181 /// This is the passthrough for connection-scoped metadata that a tower
182 /// layer in front of the service can attach — TLS peer certificates,
183 /// remote socket address, auth context, etc. The dispatch path moves
184 /// `parts.extensions` here verbatim; handlers read it with
185 /// `ctx.extensions().get::<T>()`. For the well-known peer types, prefer
186 /// the typed accessors `peer_addr()` and `peer_certs()` (gated on the
187 /// `server` and `server-tls` features respectively) — they return
188 /// `None` instead of panicking when the transport didn't insert the
189 /// extension.
190 pub fn extensions(&self) -> &http::Extensions {
191 &self.extensions
192 }
193
194 /// Mutable access to the request extensions.
195 ///
196 /// Useful for code that constructs a `RequestContext` directly — e.g.
197 /// a custom dispatch shim or test fixture — and needs to insert
198 /// connection-scoped values before calling a handler.
199 ///
200 /// # Note
201 ///
202 /// Handlers receive `RequestContext` **by value**, so calling
203 /// `ctx.extensions_mut().insert(...)` inside a handler mutates a local
204 /// copy that the framework never sees again — it has no effect on the
205 /// dispatch path or on downstream layers. To pass values *into* a
206 /// handler from middleware, mutate `http::Request::extensions_mut()`
207 /// in the layer instead; the dispatcher moves request extensions into
208 /// `RequestContext` automatically before dispatch.
209 pub fn extensions_mut(&mut self) -> &mut http::Extensions {
210 &mut self.extensions
211 }
212
213 /// Static metadata for the dispatched RPC method, when known.
214 ///
215 /// Populated by code-generated `FooServiceServer<T>` dispatchers and
216 /// by the dynamic [`Router`](crate::Router) when registered through
217 /// the generated `register()` (which chains
218 /// [`Router::with_spec`](crate::Router::with_spec) per route).
219 /// `None` only for low-level manual registrations that do not attach a
220 /// [`Spec`](crate::Spec). See [`path`](Self::path) for the always-present
221 /// procedure path.
222 pub fn spec(&self) -> Option<crate::spec::Spec> {
223 self.spec
224 }
225
226 /// The wire protocol negotiated for this request, when known.
227 ///
228 /// `None` if the runtime constructed the context outside the dispatch
229 /// path (e.g. unit tests calling handlers directly).
230 pub fn protocol(&self) -> Option<crate::Protocol> {
231 self.protocol
232 }
233
234 /// The procedure path the client requested, `"/package.Service/Method"`.
235 ///
236 /// Always present when constructed by the dispatch path: it is taken
237 /// from the request URI, so it is populated whenever a handler is
238 /// dispatched — including dispatch through the dynamic
239 /// [`Router`](crate::Router), which does not supply a
240 /// [`Spec`](crate::Spec). `None` only for hand-built contexts (unit
241 /// tests calling handlers directly, custom dispatch shims). Code that
242 /// must label or gate every request — auth interceptors, span
243 /// builders, rate limiters — should read `path()`, not `spec()`, and
244 /// treat `None` as a misconfigured or synthetic context rather than a
245 /// real RPC.
246 ///
247 /// Compare [`spec()`](Self::spec): that is the registered method's
248 /// *static* metadata, populated when a generated
249 /// `FooServiceServer<T>` dispatcher — or a `register()`-built
250 /// [`Router`](crate::Router) — resolved the route, and
251 /// [`Spec::procedure`](crate::Spec::procedure) is its `&'static str`
252 /// procedure name. When both are present they are identical strings;
253 /// `path()` exists for the cases where `spec()` cannot be.
254 ///
255 /// The leading slash is included to match `Spec::procedure`, the
256 /// `connect-go` `Spec.Procedure` convention, and `http::Uri::path()`
257 /// for any HTTP request that reached the dispatch layer. To compare
258 /// against [`Dispatcher::lookup`](crate::Dispatcher::lookup) keys
259 /// (which omit it), use `path.strip_prefix('/').unwrap_or(path)`.
260 pub fn path(&self) -> Option<&str> {
261 self.path.as_deref()
262 }
263
264 /// Remote peer socket address, if the transport recorded one.
265 ///
266 /// Present when the request arrived through
267 /// [`Server::serve`](crate::server::Server::serve) (plain) or
268 /// `Server::with_tls(...)` (TLS), or any integration that inserts
269 /// [`PeerAddr`](crate::server::PeerAddr) into the request extensions
270 /// (`connectrpc::axum::serve_tls` does).
271 /// Returns `None` otherwise (e.g. an axum app without a layer that
272 /// captures the connect info), so prefer this over
273 /// `ctx.extensions().get::<PeerAddr>().unwrap()` — the latter compiles,
274 /// passes in unit tests, and panics in production behind a transport
275 /// that didn't insert it.
276 #[cfg(feature = "server")]
277 #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
278 pub fn peer_addr(&self) -> Option<std::net::SocketAddr> {
279 self.extensions
280 .get::<crate::server::PeerAddr>()
281 .map(|p| p.0)
282 }
283
284 /// TLS client certificate chain presented by the peer (leaf first), if any.
285 ///
286 /// Present only when the request arrived over a TLS listener that
287 /// requested a client certificate and the client presented one — see
288 /// [`Server::with_tls`](crate::server::Server::with_tls) and
289 /// `connectrpc::axum::serve_tls`. Returns `None` for plaintext
290 /// transports, for TLS without mutual auth, and for integrations that
291 /// don't insert [`PeerCerts`](crate::server::PeerCerts) into the
292 /// request extensions. Like [`peer_addr`](Self::peer_addr), prefer
293 /// this over a raw `extensions().get()` + `unwrap()`.
294 #[cfg(feature = "server-tls")]
295 #[cfg_attr(docsrs, doc(cfg(feature = "server-tls")))]
296 pub fn peer_certs(&self) -> Option<&[rustls::pki_types::CertificateDer<'static>]> {
297 self.extensions
298 .get::<crate::server::PeerCerts>()
299 .map(|p| &p.0[..])
300 }
301}
302
303// ---------------------------------------------------------------------------
304// Response<B>
305// ---------------------------------------------------------------------------
306
307/// Handler response wrapper: a body plus optional response headers,
308/// trailers, and compression hint.
309///
310/// `B` is bounded by [`Encodable<M>`] in the generated service trait so
311/// handlers can return the owned message `M` (the common case), or any
312/// type that encodes to the same wire bytes.
313///
314/// # Happy path
315///
316/// [`Response::ok`] is the bare-body shorthand:
317///
318/// ```rust,ignore
319/// async fn say(&self, _ctx: RequestContext, req: OwnedSayRequestView)
320/// -> ServiceResult<SayResponse>
321/// {
322/// Response::ok(SayResponse { sentence: reply, ..Default::default() })
323/// }
324/// ```
325///
326/// # With metadata
327///
328/// ```rust,ignore
329/// Ok(Response::new(reply)
330/// .with_header("x-request-id", id)
331/// .with_trailer("x-timing", elapsed))
332/// ```
333#[derive(Debug, Clone)]
334pub struct Response<B> {
335 /// The response body.
336 pub body: B,
337 /// Response headers to send before the body.
338 pub headers: HeaderMap,
339 /// Trailers to send after the body. Sent as HTTP/2 trailing
340 /// HEADERS for gRPC, or as `trailer-`-prefixed headers / the
341 /// EndStreamResponse JSON for Connect.
342 pub trailers: HeaderMap,
343 /// Whether to compress the response. `None` uses the server's
344 /// compression policy; `Some(false)` disables compression for this
345 /// response, `Some(true)` forces it.
346 pub compress: Option<bool>,
347}
348
349impl<B> Response<B> {
350 /// Shorthand for `Ok(Response::from(body))` — the bare-body happy
351 /// path.
352 ///
353 /// Use `Ok(Response::new(body).with_header(...))` when setting
354 /// response metadata; this constructor is for the common case of
355 /// "just the body".
356 pub fn ok(body: B) -> ServiceResult<B> {
357 Ok(Self::from(body))
358 }
359
360 /// Wrap a body with empty response metadata.
361 pub fn new(body: B) -> Self {
362 Self {
363 body,
364 headers: HeaderMap::new(),
365 trailers: HeaderMap::new(),
366 compress: None,
367 }
368 }
369
370 /// Append a response header.
371 ///
372 /// Uses [`HeaderMap::append`], so calling twice with the same name
373 /// accumulates values rather than replacing.
374 ///
375 /// # Panics
376 ///
377 /// Panics if `name` or `value` cannot be converted into the
378 /// corresponding header type (invalid characters, non-ASCII name,
379 /// etc.). Use [`try_with_header`](Self::try_with_header) for
380 /// dynamic values, or the `headers` field directly for full
381 /// control.
382 #[must_use]
383 pub fn with_header<K, V>(mut self, name: K, value: V) -> Self
384 where
385 K: TryInto<HeaderName>,
386 K::Error: std::fmt::Debug,
387 V: TryInto<HeaderValue>,
388 V::Error: std::fmt::Debug,
389 {
390 self.headers
391 .append(name.try_into().unwrap(), value.try_into().unwrap());
392 self
393 }
394
395 /// Append a response header, returning an error if `name` or
396 /// `value` is invalid.
397 ///
398 /// Non-panicking sibling of [`with_header`](Self::with_header) for
399 /// dynamic values. Uses [`HeaderMap::append`], so repeated calls
400 /// accumulate.
401 pub fn try_with_header<K, V>(mut self, name: K, value: V) -> Result<Self, http::Error>
402 where
403 K: TryInto<HeaderName>,
404 K::Error: Into<http::Error>,
405 V: TryInto<HeaderValue>,
406 V::Error: Into<http::Error>,
407 {
408 self.headers.append(
409 name.try_into().map_err(Into::into)?,
410 value.try_into().map_err(Into::into)?,
411 );
412 Ok(self)
413 }
414
415 /// Append a response trailer.
416 ///
417 /// Uses [`HeaderMap::append`], so calling twice with the same name
418 /// accumulates values rather than replacing.
419 ///
420 /// # Panics
421 ///
422 /// Panics if `name` or `value` cannot be converted into the
423 /// corresponding header type. Use
424 /// [`try_with_trailer`](Self::try_with_trailer) for dynamic
425 /// values, or the `trailers` field directly for full control.
426 #[must_use]
427 pub fn with_trailer<K, V>(mut self, name: K, value: V) -> Self
428 where
429 K: TryInto<HeaderName>,
430 K::Error: std::fmt::Debug,
431 V: TryInto<HeaderValue>,
432 V::Error: std::fmt::Debug,
433 {
434 self.trailers
435 .append(name.try_into().unwrap(), value.try_into().unwrap());
436 self
437 }
438
439 /// Append a response trailer, returning an error if `name` or
440 /// `value` is invalid.
441 ///
442 /// Non-panicking sibling of [`with_trailer`](Self::with_trailer)
443 /// for dynamic values. Uses [`HeaderMap::append`], so repeated
444 /// calls accumulate.
445 pub fn try_with_trailer<K, V>(mut self, name: K, value: V) -> Result<Self, http::Error>
446 where
447 K: TryInto<HeaderName>,
448 K::Error: Into<http::Error>,
449 V: TryInto<HeaderValue>,
450 V::Error: Into<http::Error>,
451 {
452 self.trailers.append(
453 name.try_into().map_err(Into::into)?,
454 value.try_into().map_err(Into::into)?,
455 );
456 Ok(self)
457 }
458
459 /// Override the server's compression policy for this response.
460 ///
461 /// `true` forces compression, `false` disables it, `None` (or
462 /// never calling this) defers to the server's policy.
463 #[must_use]
464 pub fn compress(mut self, enabled: impl Into<Option<bool>>) -> Self {
465 self.compress = enabled.into();
466 self
467 }
468
469 /// Replace the body, preserving headers/trailers/compression.
470 pub fn map_body<C>(self, f: impl FnOnce(B) -> C) -> Response<C> {
471 Response {
472 body: f(self.body),
473 headers: self.headers,
474 trailers: self.trailers,
475 compress: self.compress,
476 }
477 }
478}
479
480impl<B> From<B> for Response<B> {
481 fn from(body: B) -> Self {
482 Self::new(body)
483 }
484}
485
486impl<T> Response<ServiceStream<T>> {
487 /// Wrap a streaming body, boxing and unsize-coercing it to
488 /// [`ServiceStream<T>`]. Handles the explicit coercion that
489 /// `Ok(Box::pin(s).into())` would otherwise need.
490 pub fn stream(s: impl Stream<Item = Result<T, ConnectError>> + Send + 'static) -> Self {
491 Self::new(Box::pin(s))
492 }
493
494 /// Shorthand for `Ok(Response::stream(s))` — the bare-stream
495 /// happy path.
496 pub fn stream_ok(
497 s: impl Stream<Item = Result<T, ConnectError>> + Send + 'static,
498 ) -> ServiceResult<ServiceStream<T>> {
499 Ok(Self::stream(s))
500 }
501}
502
503/// Result type returned by handler trait methods.
504///
505/// `B` is the body type — typically the owned response message, or any
506/// `impl Encodable<M>`.
507pub type ServiceResult<B> = Result<Response<B>, ConnectError>;
508
509/// Boxed `Send` stream of `Result<T, ConnectError>`.
510///
511/// Used as the request type for client/bidi-streaming handlers and the
512/// body type for server/bidi-streaming responses.
513///
514/// For an inbound request stream, `None` means the client finished the
515/// stream cleanly; `Some(Err(..))` means the stream ended abnormally — a
516/// decode failure or a request body that failed mid-upload (truncated or
517/// broken transport). Treat only `None` as a complete stream; propagating
518/// the error with `?` fails the RPC, which is the right default for
519/// handlers that aggregate inbound messages.
520pub type ServiceStream<T> = Pin<Box<dyn Stream<Item = Result<T, ConnectError>> + Send>>;
521
522/// The inbound request stream a client/bidi-streaming handler receives:
523/// [`ServiceStream`] of [`StreamMessage`](crate::StreamMessage) items.
524///
525/// Pure sugar for the composed type — generated handler traits spell their
526/// parameters with this alias so signatures stay readable.
527pub type InboundStream<M> = ServiceStream<crate::StreamMessage<M>>;
528
529/// Encoded message bytes, either contiguous or split into reference-counted
530/// segments.
531///
532/// Concatenating [`segments`](Self::segments) always yields the message's wire
533/// bytes; how they are divided is an artifact of how the body was encoded and
534/// carries no protocol meaning. Envelope framing has never depended on HTTP
535/// frame boundaries, so a segmented body reaches the peer as the same message.
536///
537/// The single-buffer case is kept unboxed: a small message that was never
538/// worth segmenting costs no allocation to carry.
539#[derive(Debug, Clone)]
540pub enum EncodedBody {
541 /// One contiguous buffer — what a non-segmenting encode produces.
542 Contiguous(Bytes),
543 /// Several buffers, concatenating to the message's wire bytes.
544 Segmented(Vec<Bytes>),
545}
546
547impl EncodedBody {
548 /// Build from a rope's segments, collapsing the trivial cases so callers
549 /// never see a needless `Vec` for zero or one segment.
550 #[must_use]
551 pub fn from_segments(mut segments: Vec<Bytes>) -> Self {
552 match segments.len() {
553 0 => Self::Contiguous(Bytes::new()),
554 1 => Self::Contiguous(segments.pop().unwrap_or_default()),
555 _ => Self::Segmented(segments),
556 }
557 }
558
559 /// Total encoded length across all segments.
560 #[must_use]
561 pub fn len(&self) -> usize {
562 match self {
563 Self::Contiguous(b) => b.len(),
564 Self::Segmented(v) => v.iter().map(Bytes::len).sum(),
565 }
566 }
567
568 /// Whether the encoded message is empty.
569 #[must_use]
570 pub fn is_empty(&self) -> bool {
571 self.len() == 0
572 }
573
574 /// The segments in wire order.
575 #[must_use]
576 pub fn segments(&self) -> &[Bytes] {
577 match self {
578 Self::Contiguous(b) => std::slice::from_ref(b),
579 Self::Segmented(v) => v,
580 }
581 }
582
583 /// Flatten to a single contiguous buffer, copying only when segmented.
584 ///
585 /// This is the escape hatch for paths that genuinely need one buffer
586 /// (compression, JSON, base64) — it gives back exactly what a
587 /// non-segmenting encode would have produced.
588 #[must_use]
589 pub fn into_contiguous(self) -> Bytes {
590 match self {
591 Self::Contiguous(b) => b,
592 Self::Segmented(v) => {
593 let mut out = BytesMut::with_capacity(v.iter().map(Bytes::len).sum());
594 for segment in &v {
595 out.extend_from_slice(segment);
596 }
597 out.freeze()
598 }
599 }
600 }
601}
602
603impl From<Bytes> for EncodedBody {
604 fn from(bytes: Bytes) -> Self {
605 Self::Contiguous(bytes)
606 }
607}
608
609// ---------------------------------------------------------------------------
610// Encodable<M>
611// ---------------------------------------------------------------------------
612
613/// Encodes to the same wire bytes as proto message `M`.
614///
615/// This is the bound on the response body in generated trait methods.
616/// Provided implementations:
617/// - the owned `M` itself (blanket `M: Message + JsonSerialize` below);
618/// - `MView<'_>` and [`OwnedView<MView<'static>>`](buffa::view::OwnedView),
619/// emitted by codegen per RPC output type;
620/// - [`MaybeBorrowed<M, V>`] for handlers that conditionally return
621/// either;
622/// - [`StreamMessage<M>`](crate::StreamMessage) for echoing inbound
623/// stream items back out (re-encodes from the retained wire bytes);
624/// - [`PreEncoded`] for handlers that encode a non-`'static` view
625/// internally and pass the bytes across the handler boundary.
626///
627/// # Contract
628///
629/// Implementations must produce bytes that decode as a valid `M` in
630/// the given format.
631///
632/// `encode` is fallible: the owned-message impl never errors. The
633/// view-body impls are proto-only (view types lack `Serialize`) and return
634/// [`ErrorCode::Unimplemented`](crate::ErrorCode::Unimplemented) for
635/// `CodecFormat::Json`. [`PreEncoded`] supports both codecs but the JSON
636/// path is a slow fallback (decode + re-serialize) — see its
637/// `# Codec behaviour` doc.
638pub trait Encodable<M> {
639 /// Encode `self` as wire bytes for `M` in the requested format.
640 fn encode(&self, codec: CodecFormat) -> Result<Bytes, ConnectError>;
641
642 /// Encode `self` as wire bytes that may arrive in several reference-counted
643 /// segments rather than one contiguous buffer.
644 ///
645 /// Concatenating the segments yields exactly what [`encode`](Self::encode)
646 /// would have returned, so this is an optimization and never a wire-format
647 /// difference. A payload the encoder can hand over by reference count — a
648 /// large `bytes::Bytes` field, or a view field borrowed from the buffer the
649 /// view was decoded from — becomes its own segment instead of being copied
650 /// into the output.
651 ///
652 /// The default implementation returns [`encode`](Self::encode)'s single
653 /// buffer, which is always correct. Overriding it is worthwhile only for a
654 /// body that can carry a large payload by reference; a body whose fields
655 /// are `String` or `Vec<u8>` has nothing to hand over, and segmenting it
656 /// would add the rope's cost for no saving.
657 ///
658 /// How large a payload has to be before it earns its own segment is the
659 /// framing layer's decision, not the implementation's — anything smaller
660 /// is copied into the framing buffer downstream regardless, so a smaller
661 /// threshold spends effort without saving a copy. Implementations that
662 /// need to encode a view should call
663 /// [`__codegen::encode_view_body_segments`](crate::__codegen::encode_view_body_segments),
664 /// which applies that threshold for them.
665 ///
666 /// # Errors
667 ///
668 /// Same conditions as [`encode`](Self::encode).
669 fn encode_segments(&self, codec: CodecFormat) -> Result<EncodedBody, ConnectError> {
670 self.encode(codec).map(EncodedBody::from)
671 }
672}
673
674impl<M: Message + JsonSerialize> Encodable<M> for M {
675 fn encode(&self, codec: CodecFormat) -> Result<Bytes, ConnectError> {
676 match codec {
677 CodecFormat::Proto => Ok(self.encode_to_bytes()),
678 CodecFormat::Json => encode_json(self),
679 }
680 }
681
682 // Deliberately does not override `encode_segments`. An owned message
683 // holds its `string` and `bytes` fields as `String` / `Vec<u8>` under the
684 // default codegen mapping, and neither can be handed over by reference
685 // count, so a rope here would capture nothing and only add its own cost.
686 // The win lives on the view path, which borrows its fields out of a
687 // buffer a rope can capture from.
688}
689
690/// Encode a view body via [`ViewEncode`] for [`CodecFormat::Proto`], or
691/// return [`ErrorCode::Unimplemented`](crate::ErrorCode::Unimplemented)
692/// for [`CodecFormat::Json`] (view types don't implement `Serialize`).
693///
694/// Used by codegen-emitted `impl Encodable<Foo> for FooView<'_>` /
695/// `impl Encodable<Foo> for OwnedView<FooView<'static>>` blocks. A
696/// runtime blanket on [`OwnedView`](buffa::view::OwnedView) would
697/// conflict with the `M: Message + JsonSerialize` blanket above (coherence
698/// can't rule out upstream adding `Message`/`Serialize` for
699/// `OwnedView`), so the impls are emitted per output type instead.
700#[doc(hidden)]
701pub fn encode_view_body<'a, V: ViewEncode<'a>>(
702 view: &V,
703 codec: CodecFormat,
704) -> Result<Bytes, ConnectError> {
705 match codec {
706 // Not `encode_to_bytes`, which panics past the 2 GiB protobuf limit:
707 // an oversized response is a request-shaped input reaching a server,
708 // and the segmented sibling already reports it as an error. The work
709 // is the same either way — `encode_to_bytes` runs these two passes
710 // internally.
711 CodecFormat::Proto => {
712 let mut cache = buffa::SizeCache::new();
713 let size = checked_response_size(view.compute_size(&mut cache))?;
714 let mut buf = BytesMut::with_capacity(size);
715 view.write_to(&mut cache, &mut buf);
716 Ok(buf.freeze())
717 }
718 CodecFormat::Json => Err(ConnectError::unimplemented(
719 "view-body responses do not support the JSON codec; return the owned message type for JSON-serving handlers",
720 )),
721 }
722}
723
724/// Whether a response of `size` bytes should be encoded through a rope, given
725/// that its captures would alias a `backing` buffer of `backing_len` bytes.
726///
727/// Two ways a rope loses. A response below one segment has nothing large
728/// enough to capture, so the rope is pure overhead. And a small response
729/// derived from a large request would capture slices of that request's buffer,
730/// keeping the whole allocation alive until the response finishes flushing —
731/// a handler that answers a 64 MiB upload with a 32 KiB summary would hold
732/// 64 MiB per in-flight response where it used to hold 32 KiB. Copying is
733/// cheaper than that. When the response is at least half the buffer it borrows from,
734/// the buffer was going to stay alive anyway and the capture is free.
735fn worth_segmenting(size: usize, backing_len: usize, min_segment: usize) -> bool {
736 size >= min_segment && size.saturating_mul(2) >= backing_len
737}
738
739/// Merge each *run* of consecutive sub-`min_segment` segments into one.
740///
741/// A rope flushes its pending tail before each capture, so a view with several
742/// large fields yields alternating tag/length fragments and captured payloads.
743/// Emitted as-is those fragments become their own HTTP data frames, each a
744/// handful of bytes behind a 9-byte HTTP/2 frame header. Merging a run costs a
745/// copy proportional to the fragments, not the payload.
746///
747/// An isolated fragment therefore stays its own segment: it has no small
748/// neighbour to join, and folding it into an adjacent capture would mean
749/// allocating and copying that capture, which is the one cost this whole path
750/// exists to avoid. It is still re-copied into a run of its own — a handful of
751/// bytes — so what survives untouched is the capture, not the fragment. The
752/// alternating shape above thus keeps one 9-byte frame header per captured
753/// field, a fixed price per field paid to leave the payloads themselves
754/// un-copied.
755fn coalesce_small_runs(segments: Vec<Bytes>, min_segment: usize) -> Vec<Bytes> {
756 if segments.len() < 2 {
757 return segments;
758 }
759 let mut out: Vec<Bytes> = Vec::with_capacity(segments.len());
760 let mut pending = BytesMut::new();
761 for segment in segments {
762 if segment.len() >= min_segment {
763 if !pending.is_empty() {
764 out.push(std::mem::take(&mut pending).freeze());
765 }
766 out.push(segment);
767 } else {
768 pending.extend_from_slice(&segment);
769 }
770 }
771 if !pending.is_empty() {
772 out.push(pending.freeze());
773 }
774 out
775}
776
777/// Reject a response larger than protobuf can encode, as an error rather than
778/// a panic — this runs on a server, where the size is a function of what a
779/// caller asked for.
780fn checked_response_size(size: u32) -> Result<usize, ConnectError> {
781 buffa::checked_encode_size(size)
782 .map(|size| size as usize)
783 .map_err(|_| {
784 ConnectError::internal("response message exceeds the 2 GiB protobuf size limit")
785 })
786}
787
788/// Encode a view body, capturing its large borrowed fields by reference count
789/// instead of copying them.
790///
791/// This is where buffa 0.9's rope pays. A view's fields are slices into the
792/// buffer it was decoded from, so a rope told about that buffer can take a
793/// large field by reference, and the encode then costs the same whatever the
794/// payload weighs. The `view_rope_encode` benchmark in `benches/rpc` measures the
795/// curve; above the threshold the encode goes flat, because only the framing
796/// is still being written.
797///
798/// `backing` must be the buffer this view was decoded from. A rope pointed
799/// anywhere else captures nothing and is slower than a contiguous encode — it
800/// still produces correct bytes, so the cost of getting this wrong is silent.
801/// A caller with no buffer to give should use [`encode_view_body`].
802///
803/// The threshold below which a payload is not worth its own segment is the
804/// framing layer's, applied here so callers cannot pick a worse one: anything
805/// smaller is copied into the framing buffer downstream regardless, and a
806/// message can clear a smaller gate while none of its individual fields do,
807/// which spends the rope's cost and captures nothing. Matching the framing
808/// threshold also makes every segment map to exactly one body frame.
809///
810/// # Errors
811///
812/// [`ErrorCode::Unimplemented`](crate::ErrorCode::Unimplemented) for
813/// [`CodecFormat::Json`], as [`encode_view_body`].
814#[doc(hidden)]
815pub fn encode_view_body_segments<'a, V: ViewEncode<'a>>(
816 view: &V,
817 backing: &Bytes,
818 codec: CodecFormat,
819) -> Result<EncodedBody, ConnectError> {
820 encode_view_body_with_min_segment(view, backing, codec, crate::envelope::MIN_CHAIN_SIZE)
821}
822
823/// [`encode_view_body_segments`] with the segment threshold spelled out, so
824/// tests and benchmarks can sweep it. Production callers take the framing
825/// layer's threshold via [`encode_view_body_segments`].
826///
827/// # Errors
828///
829/// As [`encode_view_body_segments`].
830#[doc(hidden)]
831pub fn encode_view_body_with_min_segment<'a, V: ViewEncode<'a>>(
832 view: &V,
833 backing: &Bytes,
834 codec: CodecFormat,
835 min_segment: usize,
836) -> Result<EncodedBody, ConnectError> {
837 match codec {
838 CodecFormat::Json => Err(ConnectError::unimplemented(
839 "view-body responses do not support the JSON codec; return the owned message type for JSON-serving handlers",
840 )),
841 CodecFormat::Proto => {
842 let mut cache = buffa::SizeCache::new();
843 let size = checked_response_size(view.compute_size(&mut cache))?;
844
845 if !worth_segmenting(size, backing.len(), min_segment) {
846 let mut buf = BytesMut::with_capacity(size);
847 view.write_to(&mut cache, &mut buf);
848 return Ok(EncodedBody::Contiguous(buf.freeze()));
849 }
850
851 // Known cost: a rope's tail starts empty and grows by doubling,
852 // and every field too small to capture lands in it. A message that
853 // clears the gate while none of its fields do therefore copies
854 // itself roughly twice over instead of once into a sized buffer.
855 // buffa 0.9 exposes no way to pre-size the tail; until it does,
856 // that shape pays for a rope that captures nothing.
857 let mut rope = buffa::Rope::with_min_segment(min_segment).with_backing(backing.clone());
858 view.write_to(&mut cache, &mut rope);
859 Ok(EncodedBody::from_segments(coalesce_small_runs(
860 rope.into_segments(),
861 min_segment,
862 )))
863 }
864 }
865}
866
867// ---------------------------------------------------------------------------
868// MaybeBorrowed
869// ---------------------------------------------------------------------------
870
871/// Either an owned message `M` or a borrowing view `V`, both
872/// [`Encodable<M>`].
873///
874/// Use this when a handler conditionally passes the request through
875/// unchanged (return the view, zero allocations) versus modifying it
876/// (clone to owned, mutate, return owned). The single concrete return
877/// type satisfies the `impl Encodable<M>` bound on the generated trait.
878///
879/// This is not [`std::borrow::Cow`]: `V` is a separate
880/// [`Encodable<M>`] type (e.g. `MView<'a>` or `OwnedView<MView>`),
881/// not a `&M`, and there is no `ToOwned` relationship between the
882/// arms — each encodes independently.
883///
884/// ```rust,ignore
885/// async fn redact(&self, _ctx: RequestContext, req: ServiceRequest<'_, Record>)
886/// -> ServiceResult<MaybeBorrowed<Record, OwnedRecordView>>
887/// {
888/// if req.email.is_empty() && req.ssn.is_empty() {
889/// // pass-through: rebuild a 'static view from the request bytes
890/// return Response::ok(MaybeBorrowed::Borrowed(req.to_owned_view()));
891/// }
892/// let mut owned = req.to_owned_message();
893/// owned.email.clear();
894/// owned.ssn.clear();
895/// Response::ok(MaybeBorrowed::Owned(owned))
896/// }
897/// ```
898///
899/// # Codec compatibility
900///
901/// The `Borrowed` arm only encodes for [`CodecFormat::Proto`]. JSON
902/// clients receive an `unimplemented` error; if your service must
903/// support JSON, return `Owned` (or just the owned message) on every
904/// path.
905#[derive(Debug, Clone)]
906pub enum MaybeBorrowed<M, V> {
907 /// An owned message body.
908 Owned(M),
909 /// A borrowing body that encodes to the same wire bytes as `M`.
910 Borrowed(V),
911}
912
913impl<M, V> Encodable<M> for MaybeBorrowed<M, V>
914where
915 // satisfied via the blanket impl for M: Message + JsonSerialize
916 M: Encodable<M>,
917 V: Encodable<M>,
918{
919 fn encode(&self, codec: CodecFormat) -> Result<Bytes, ConnectError> {
920 match self {
921 Self::Owned(m) => m.encode(codec),
922 Self::Borrowed(v) => v.encode(codec),
923 }
924 }
925
926 /// Forwards to the wrapped body rather than taking the contiguous
927 /// default. `Borrowed` is the arm handlers reach for to avoid copying, so
928 /// it is exactly the arm that must not lose the segmented encode by being
929 /// wrapped — the wrapper would otherwise quietly undo the reason it was
930 /// chosen.
931 fn encode_segments(&self, codec: CodecFormat) -> Result<EncodedBody, ConnectError> {
932 match self {
933 Self::Owned(m) => m.encode_segments(codec),
934 Self::Borrowed(v) => v.encode_segments(codec),
935 }
936 }
937}
938
939// ---------------------------------------------------------------------------
940// PreEncoded
941// ---------------------------------------------------------------------------
942
943/// Pre-encoded protobuf response body for message type `M`.
944///
945/// Use when the handler builds and encodes a borrowing view internally —
946/// e.g. a `FooView<'a>` borrowing from a local snapshot — rather than
947/// returning the view itself. The `'static` bound on `Handler::Body` (and
948/// on streaming items, see the `use<Self>` note in the
949/// [`StreamingHandler`](crate::StreamingHandler) docs) means a view with a
950/// non-`'static` lifetime can't cross the handler
951/// boundary; `PreEncoded` carries the bytes across instead.
952///
953/// The `M` type parameter is a compile-time witness for which RPC output
954/// type the bytes encode. Three construction paths, in decreasing order
955/// of compile-time guarantee:
956///
957/// - [`from_message(&m)`](PreEncoded::from_message) — encodes an owned
958/// `M`; the receiver type *is* the witness.
959/// - [`from_view(&view)`](PreEncoded::from_view) — encodes a borrowing
960/// view; `MessageView::Owned = M` is the witness.
961/// - [`from_bytes_unchecked(bytes)`](PreEncoded::from_bytes_unchecked) —
962/// wraps already-encoded bytes from elsewhere (a cache, storage,
963/// another service). No witness; you're asserting the bytes decode as
964/// `M`.
965///
966/// `from_message` and `from_view` produce the same `PreEncoded<M>` type,
967/// so a stream can mix items built either way (e.g. a cache-hit path
968/// returning the cached owned `M`, a cache-miss path building a view from
969/// a snapshot) — the same role [`MaybeBorrowed`] fills for unary
970/// handlers, but with the encode happening eagerly inside the stream
971/// body.
972///
973/// # Streaming example
974///
975/// The motivating shape — a server-streaming handler that builds and
976/// encodes per-item views borrowing from a local store snapshot, then
977/// yields the bytes:
978///
979/// ```rust,ignore
980/// use connectrpc::{PreEncoded, Response, RequestContext, ServiceResult, ServiceStream};
981///
982/// async fn watch(
983/// &self,
984/// _ctx: RequestContext,
985/// req: OwnedWatchRequestView,
986/// ) -> ServiceResult<ServiceStream<PreEncoded<WatchResponse>>> {
987/// let store = self.store.clone();
988/// let stream = futures::stream::unfold(store, |store| async move {
989/// let snapshot = store.load();
990/// // `view` borrows from `snapshot`; encode while the borrow is live.
991/// let view = build_view_from_snapshot(&snapshot);
992/// let item = PreEncoded::from_view(&view);
993/// Some((Ok(item), store))
994/// });
995/// Response::stream_ok(stream)
996/// }
997/// ```
998///
999/// For a unary handler, the same pattern applies — return
1000/// `ServiceResult<PreEncoded<MyResponse>>`.
1001///
1002/// # Codec behaviour
1003///
1004/// `PreEncoded` is optimized for the `proto` codec: the wrapped bytes are
1005/// passed through verbatim with no re-encoding. The motivating use case
1006/// (high-throughput fanout) is proto-only.
1007///
1008/// For the `json` codec, `PreEncoded` falls back to decoding the bytes as
1009/// `M` and re-serializing as JSON. **This is correct but not fast** — a
1010/// full proto decode plus a JSON serialize per response (or per stream
1011/// item). The fallback exists so that registering a `PreEncoded` handler
1012/// on a JSON-capable router degrades gracefully instead of returning a
1013/// runtime error. If your service serves a meaningful JSON traffic share,
1014/// build and return the owned message (or [`MaybeBorrowed::Owned`])
1015/// instead — that lets the codec layer pick the right encoding without
1016/// the proto round-trip.
1017///
1018/// If the wrapped bytes don't decode as `M` (e.g. you passed mismatched
1019/// bytes to [`from_bytes_unchecked`](PreEncoded::from_bytes_unchecked)),
1020/// the JSON path returns an [`internal`](crate::ErrorCode::Internal)
1021/// error at the server; the proto path passes the bytes through and the
1022/// client sees a decode error.
1023///
1024/// ## Codec-dependent fidelity
1025///
1026/// The proto path is byte-exact; the JSON path is **only as faithful as
1027/// decoding the bytes to an owned `M` and re-serializing**. The two
1028/// diverge when the wrapped bytes carry information not representable in
1029/// `M` itself:
1030///
1031/// - **Unknown fields** (proto bytes encoded against a *newer* schema
1032/// than the server's `M`) are preserved on the proto path and dropped
1033/// on the JSON path. This matters only for
1034/// [`from_bytes_unchecked`](PreEncoded::from_bytes_unchecked) bytes
1035/// sourced externally; bytes produced by
1036/// [`from_message`](PreEncoded::from_message) /
1037/// [`from_view`](PreEncoded::from_view) cannot carry unknown fields.
1038/// - **Non-canonical proto encodings** (out-of-order fields, redundant
1039/// length prefixes, repeated non-`repeated` fields) are passed through
1040/// verbatim on the proto path and normalized by the decode on the JSON
1041/// path.
1042///
1043/// If byte-exact fidelity across codecs matters (e.g. signature
1044/// verification, content-addressed storage), do not use `PreEncoded` with
1045/// JSON-capable routes.
1046///
1047/// ## Cost is selected by the client
1048///
1049/// The codec is chosen per-request by the client's `Content-Type` header.
1050/// For a service that adopted `PreEncoded` for proto throughput, a client
1051/// sending JSON requests (intentionally, by misconfiguration, or
1052/// adversarially) shifts those requests onto the slow decode-reserialize
1053/// path. The marginal cost is bounded by the response size and is usually
1054/// small relative to the handler's own work, but a streaming RPC pays it
1055/// per item. A service that wants to *enforce* proto-only should reject
1056/// non-proto `Content-Type` at the middleware layer (e.g. an axum
1057/// middleware that returns `415 Unsupported Media Type`) rather than rely
1058/// on the body type — that keeps the policy outside the handler and
1059/// applies before the request body is read.
1060///
1061/// # Contract
1062///
1063/// `PreEncoded` is a transparent byte container — it does not validate
1064/// the wrapped bytes on the proto path. [`PreEncoded::from_view`] gives a
1065/// compile-time witness via `MessageView::Owned = M`;
1066/// [`PreEncoded::from_bytes_unchecked`] trusts the caller. Returning bytes
1067/// that don't decode as `M` will produce decode errors on the client (or,
1068/// for JSON clients, an `internal` error from the server-side fallback
1069/// decode).
1070#[must_use = "PreEncoded must be returned from a handler to take effect"]
1071pub struct PreEncoded<M> {
1072 bytes: Bytes,
1073 // `fn() -> M` keeps `PreEncoded<M>` `Send + Sync` regardless of `M`'s
1074 // auto-trait surface (the bytes are owned; `M` is only a type witness).
1075 _marker: PhantomData<fn() -> M>,
1076}
1077
1078// Manual derives: `#[derive(Debug, Clone)]` would add a spurious `M: Debug` /
1079// `M: Clone` bound (PhantomData carries it through to the where-clause).
1080impl<M> std::fmt::Debug for PreEncoded<M> {
1081 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082 f.debug_tuple("PreEncoded").field(&self.bytes).finish()
1083 }
1084}
1085
1086impl<M> Clone for PreEncoded<M> {
1087 fn clone(&self) -> Self {
1088 Self {
1089 bytes: self.bytes.clone(),
1090 _marker: PhantomData,
1091 }
1092 }
1093}
1094
1095impl<M: Message> PreEncoded<M> {
1096 /// Encode an owned `M` to protobuf bytes.
1097 ///
1098 /// The receiver type is the compile-time witness — there's no way to
1099 /// produce a `PreEncoded<M>` from a `&Other`. This is the right
1100 /// constructor when the handler builds an owned `M` and wants to
1101 /// share the encoding (e.g. encode once, clone the
1102 /// [`Bytes`]-backed `PreEncoded` for N readers in a fanout) or when a
1103 /// stream needs to mix owned-message and view-built items under a
1104 /// single `type Item = PreEncoded<M>`.
1105 ///
1106 /// Equivalent to `PreEncoded::from_bytes_unchecked(m.encode_to_bytes())`,
1107 /// but with `M` enforced by the type system rather than asserted by
1108 /// the caller.
1109 pub fn from_message(msg: &M) -> Self {
1110 Self {
1111 bytes: msg.encode_to_bytes(),
1112 _marker: PhantomData,
1113 }
1114 }
1115
1116 /// Encode a [`ViewEncode`] view to protobuf bytes.
1117 ///
1118 /// The `MessageView<'a, Owned = M>` bound is the compile-time witness
1119 /// that the bytes decode as `M` — passing `OtherView<'a>` won't
1120 /// type-check unless `OtherView::Owned == M`.
1121 pub fn from_view<'a, V>(view: &V) -> Self
1122 where
1123 V: ViewEncode<'a> + MessageView<'a, Owned = M>,
1124 {
1125 Self {
1126 bytes: view.encode_to_bytes(),
1127 _marker: PhantomData,
1128 }
1129 }
1130
1131 /// Wrap already-encoded protobuf bytes without validating them.
1132 ///
1133 /// Use when the bytes come from somewhere with no structural type
1134 /// guarantee — a byte cache, a blob store, a sidecar service. You are
1135 /// asserting the bytes decode as `M`; the proto path does not
1136 /// validate this. In debug builds, the bytes are decoded once as a
1137 /// `debug_assert!` to surface mismatches early.
1138 ///
1139 /// Prefer [`from_message`](PreEncoded::from_message) when you have an
1140 /// owned `M` in hand and [`from_view`](PreEncoded::from_view) when
1141 /// you have a view — both enforce `M` at compile time.
1142 ///
1143 /// Zero-copy for `Bytes` and `Vec<u8>`; passing `&[u8]` allocates and
1144 /// copies.
1145 pub fn from_bytes_unchecked(bytes: impl Into<Bytes>) -> Self {
1146 let bytes = bytes.into();
1147 debug_assert!(
1148 M::decode_from_slice(&bytes).is_ok(),
1149 "PreEncoded::from_bytes_unchecked: bytes do not decode as {}",
1150 std::any::type_name::<M>(),
1151 );
1152 Self {
1153 bytes,
1154 _marker: PhantomData,
1155 }
1156 }
1157}
1158
1159/// Encode an owned `M` to a [`PreEncoded<M>`].
1160///
1161/// Equivalent to [`PreEncoded::from_message`]; provided for `.into()`
1162/// ergonomics.
1163impl<M: Message> From<&M> for PreEncoded<M> {
1164 fn from(msg: &M) -> Self {
1165 Self::from_message(msg)
1166 }
1167}
1168
1169// Coherence: this impl is non-overlapping with the
1170// `impl<M: Message + JsonSerialize> Encodable<M> for M` blanket above for
1171// structural reasons. For the two to overlap, some `T` would have to satisfy
1172// both `T: Encodable<T>` (blanket, with `T: Message + JsonSerialize`) and
1173// `T = PreEncoded<U>` with `T: Encodable<U>` (this impl) for the *same* trait
1174// parameter — i.e. `T = U`, i.e. `PreEncoded<U> = U`, which is infinite. So
1175// the impls cannot overlap even if a future change made `PreEncoded` a
1176// `Message` (which would only add `PreEncoded<M>: Encodable<PreEncoded<M>>` —
1177// a different trait instantiation). No invariant to maintain here.
1178//
1179// The `M: Message + JsonSerialize` bound matches the blanket so a `PreEncoded<M>`
1180// is `Encodable<M>` exactly when an owned `M` would be — and is what makes the
1181// JSON fallback path possible (decode as `M`, re-serialize).
1182impl<M: Message + JsonSerialize> Encodable<M> for PreEncoded<M> {
1183 fn encode(&self, codec: CodecFormat) -> Result<Bytes, ConnectError> {
1184 match codec {
1185 CodecFormat::Proto => Ok(self.bytes.clone()),
1186 // Slow path: decode the proto bytes back to `M`, then serialize
1187 // as JSON. This exists for correctness (JSON clients should get
1188 // a response, not `unimplemented`), not throughput; the owned
1189 // message path skips the proto round-trip and is preferable for
1190 // JSON-heavy services. See the type-level docs.
1191 CodecFormat::Json => {
1192 let msg = M::decode_from_slice(&self.bytes).map_err(|e| {
1193 ConnectError::internal(format!(
1194 "pre-encoded bytes did not decode as {}: {e}",
1195 std::any::type_name::<M>(),
1196 ))
1197 })?;
1198 encode_json(&msg)
1199 }
1200 }
1201 }
1202}
1203
1204// ---------------------------------------------------------------------------
1205// EncodedResponse (dispatcher boundary)
1206// ---------------------------------------------------------------------------
1207
1208/// A [`Response`] with the body already encoded to bytes.
1209///
1210/// This is what the [`Dispatcher`](crate::Dispatcher) returns to the
1211/// protocol layer — encoding happens inside the dispatcher so the body
1212/// type stays generic across the trait boundary.
1213pub type EncodedResponse = Response<EncodedBody>;
1214
1215impl<B> Response<B> {
1216 /// Encode the body to bytes via [`Encodable<M>`], preserving
1217 /// response metadata.
1218 #[doc(hidden)] // exposed for dispatcher::codegen (generated code)
1219 pub fn encode<M>(self, codec: CodecFormat) -> Result<EncodedResponse, ConnectError>
1220 where
1221 B: Encodable<M>,
1222 {
1223 // Bodies that can hand a large payload over by reference count say so
1224 // here; everything else takes the default and returns the same single
1225 // buffer it always did.
1226 let body = self.body.encode_segments(codec)?;
1227 Ok(Response {
1228 body,
1229 headers: self.headers,
1230 trailers: self.trailers,
1231 compress: self.compress,
1232 })
1233 }
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238 use super::*;
1239 use buffa_types::google::protobuf::__buffa::view::StringValueView;
1240 use buffa_types::google::protobuf::StringValue;
1241
1242 /// The invariant the whole segmented path rests on: however the encoder
1243 /// chose to divide the output, concatenating it must reproduce exactly
1244 /// what the contiguous encode produced. A divergence here is a wire-format
1245 /// bug, not a performance one.
1246 ///
1247 /// Swept across thresholds because each one divides the output
1248 /// differently — 1 makes almost every write its own segment, `usize::MAX`
1249 /// never segments at all, and the interesting cases sit between.
1250 #[test]
1251 fn view_segments_concatenate_to_the_contiguous_encoding() {
1252 let buffer = encoded_string_value(&"m".repeat(64 * 1024));
1253 let view = StringValueView::decode_view(&buffer).expect("decode view");
1254 let contiguous = encode_view_body(&view, CodecFormat::Proto).expect("proto encode");
1255
1256 for min_segment in [1usize, 8, 4096, 64 * 1024, usize::MAX] {
1257 let segmented =
1258 encode_view_body_with_min_segment(&view, &buffer, CodecFormat::Proto, min_segment)
1259 .expect("proto encode");
1260
1261 assert_eq!(
1262 segmented.len(),
1263 contiguous.len(),
1264 "min_segment={min_segment}: length must match"
1265 );
1266 assert_eq!(
1267 segmented.into_contiguous(),
1268 contiguous,
1269 "min_segment={min_segment}: bytes must match"
1270 );
1271 }
1272 }
1273
1274 /// Wire bytes for a `StringValue`, to decode a borrowing view from.
1275 fn encoded_string_value(value: &str) -> Bytes {
1276 Bytes::from(buffa::Message::encode_to_vec(&StringValue::from(value)))
1277 }
1278
1279 #[test]
1280 fn small_views_skip_the_rope() {
1281 // A rope costs more than it saves on a message too small to contain a
1282 // capturable field, so the gate must send those down the contiguous
1283 // path. Pinning it because the regression would be invisible: the
1284 // bytes stay correct and only the encode gets slower.
1285 let buffer = encoded_string_value("small");
1286 let view = StringValueView::decode_view(&buffer).expect("decode view");
1287
1288 let body =
1289 encode_view_body_segments(&view, &buffer, CodecFormat::Proto).expect("proto encode");
1290 assert!(
1291 matches!(body, EncodedBody::Contiguous(_)),
1292 "a message under one segment must not pay for a rope"
1293 );
1294 }
1295
1296 #[test]
1297 fn large_view_fields_are_captured_as_segments() {
1298 // The whole point of the exercise: a field larger than one segment,
1299 // borrowed from the buffer the rope is backed by, is handed over by
1300 // reference instead of copied. If this stops splitting, the encode has
1301 // silently gone back to copying the payload.
1302 let buffer = encoded_string_value(&"x".repeat(64 * 1024));
1303 let view = StringValueView::decode_view(&buffer).expect("decode view");
1304
1305 let body =
1306 encode_view_body_segments(&view, &buffer, CodecFormat::Proto).expect("proto encode");
1307 assert!(
1308 matches!(body, EncodedBody::Segmented(_)),
1309 "a 64 KiB borrowed field must be captured, not copied"
1310 );
1311 assert_eq!(
1312 body.into_contiguous(),
1313 encode_view_body(&view, CodecFormat::Proto).expect("proto encode"),
1314 "segmented output must equal what the contiguous encoder produced"
1315 );
1316 }
1317
1318 #[test]
1319 fn a_small_response_does_not_pin_a_large_request_buffer() {
1320 // Capturing means the response's segments alias the request's buffer,
1321 // which keeps the whole allocation alive until the response finishes
1322 // flushing. For a summary of a large upload that trades a copy for
1323 // holding orders of magnitude more memory per in-flight response, so
1324 // the encoder copies instead.
1325 let big_request = 4 * 1024 * 1024;
1326 let small_response = 64 * 1024;
1327 assert!(
1328 !worth_segmenting(small_response, big_request, 16 * 1024),
1329 "a response this much smaller than its request must not capture"
1330 );
1331
1332 // Returning most of what arrived is the case capture is for: the
1333 // buffer stays alive regardless, so aliasing it costs nothing.
1334 assert!(worth_segmenting(64 * 1024, 66 * 1024, 16 * 1024));
1335
1336 // Still gated on the segment threshold.
1337 assert!(!worth_segmenting(1024, 1024, 16 * 1024));
1338 }
1339
1340 #[test]
1341 fn isolated_fragments_stay_their_own_segments() {
1342 // A rope flushes its tail before each capture, so a multi-field view
1343 // yields alternating small tag/length fragments and large payloads.
1344 // Each fragment's only neighbours are captures, and folding it into
1345 // one would mean copying that capture — the cost this path exists to
1346 // avoid — so it stays its own small frame.
1347 let big = Bytes::from(vec![1u8; 32 * 1024]);
1348 let segments = vec![
1349 Bytes::from_static(b"ab"),
1350 big.clone(),
1351 Bytes::from_static(b"cd"),
1352 big.clone(),
1353 Bytes::from_static(b"ef"),
1354 ];
1355 let merged = coalesce_small_runs(segments, 16 * 1024);
1356
1357 assert_eq!(
1358 merged.len(),
1359 5,
1360 "nothing merges: no fragment is adjacent to another"
1361 );
1362 let total: usize = merged.iter().map(Bytes::len).sum();
1363 assert_eq!(total, 2 + 32 * 1024 + 2 + 32 * 1024 + 2);
1364
1365 // The large payloads must still be the original allocations. This is
1366 // the property the non-merging buys.
1367 assert!(std::ptr::eq(merged[1].as_ptr(), big.as_ptr()));
1368 assert!(std::ptr::eq(merged[3].as_ptr(), big.as_ptr()));
1369 }
1370
1371 #[test]
1372 fn consecutive_fragments_merge_into_one_segment() {
1373 // The case the function does handle: a run of adjacent sub-threshold
1374 // fragments collapses to a single segment, so a rope tail that came
1375 // out in pieces costs one frame rather than one per piece.
1376 let big = Bytes::from(vec![1u8; 32 * 1024]);
1377 let segments = vec![
1378 Bytes::from_static(b"ab"),
1379 Bytes::from_static(b"cd"),
1380 Bytes::from_static(b"ef"),
1381 big.clone(),
1382 ];
1383 let merged = coalesce_small_runs(segments, 16 * 1024);
1384
1385 assert_eq!(merged.len(), 2, "the three fragments become one segment");
1386 assert_eq!(&merged[0][..], b"abcdef");
1387 assert!(
1388 std::ptr::eq(merged[1].as_ptr(), big.as_ptr()),
1389 "merging a run must not copy the capture that follows it"
1390 );
1391 }
1392
1393 #[test]
1394 fn view_segments_without_backing_are_still_correct() {
1395 // A rope pointed at the wrong buffer captures nothing, which costs
1396 // speed but must never cost correctness.
1397 let buffer = encoded_string_value(&"y".repeat(64 * 1024));
1398 let view = StringValueView::decode_view(&buffer).expect("decode view");
1399 let unrelated = Bytes::from_static(b"not the buffer this view came from");
1400
1401 let body =
1402 encode_view_body_segments(&view, &unrelated, CodecFormat::Proto).expect("proto encode");
1403 assert_eq!(
1404 body.into_contiguous(),
1405 encode_view_body(&view, CodecFormat::Proto).expect("proto encode")
1406 );
1407 }
1408
1409 #[test]
1410 #[cfg(feature = "json")]
1411 fn json_encoding_stays_contiguous() {
1412 // JSON is serialized whole, so there is nothing to hand over by
1413 // reference and the segmented call must not pretend otherwise.
1414 let msg = StringValue::from("json");
1415 let body =
1416 Encodable::<StringValue>::encode_segments(&msg, CodecFormat::Json).expect("json");
1417 assert!(matches!(body, EncodedBody::Contiguous(_)));
1418 }
1419
1420 #[test]
1421 fn encoded_body_collapses_trivial_segment_counts() {
1422 assert!(matches!(
1423 EncodedBody::from_segments(vec![]),
1424 EncodedBody::Contiguous(b) if b.is_empty()
1425 ));
1426 assert!(matches!(
1427 EncodedBody::from_segments(vec![Bytes::from_static(b"one")]),
1428 EncodedBody::Contiguous(_)
1429 ));
1430 assert!(matches!(
1431 EncodedBody::from_segments(vec![
1432 Bytes::from_static(b"one"),
1433 Bytes::from_static(b"two")
1434 ]),
1435 EncodedBody::Segmented(_)
1436 ));
1437 }
1438
1439 #[tokio::test]
1440 async fn response_stream_ok_shorthand() {
1441 use futures::StreamExt;
1442 let r: ServiceResult<ServiceStream<i32>> =
1443 Response::stream_ok(futures::stream::iter([Ok(7)]));
1444 let collected: Vec<_> = r.unwrap().body.map(|x| x.unwrap()).collect().await;
1445 assert_eq!(collected, vec![7]);
1446 }
1447
1448 #[test]
1449 fn compress_tristate() {
1450 assert_eq!(Response::new(()).compress(true).compress, Some(true));
1451 assert_eq!(Response::new(()).compress(false).compress, Some(false));
1452 assert_eq!(Response::new(()).compress(None).compress, None);
1453 }
1454
1455 #[test]
1456 fn header_accepts_str() {
1457 let mut h = HeaderMap::new();
1458 h.insert("x-custom", HeaderValue::from_static("v"));
1459 let ctx = RequestContext::new(h);
1460 assert_eq!(ctx.header("x-custom").unwrap(), "v");
1461 }
1462
1463 #[test]
1464 fn response_ok_shorthand() {
1465 let r: ServiceResult<u32> = Response::ok(42);
1466 let r = r.unwrap();
1467 assert_eq!(r.body, 42);
1468 assert!(r.headers.is_empty());
1469 }
1470
1471 #[test]
1472 fn response_from_body() {
1473 let r: Response<StringValue> = StringValue::from("hi").into();
1474 assert_eq!(r.body.value, "hi");
1475 assert!(r.headers.is_empty());
1476 assert!(r.trailers.is_empty());
1477 assert_eq!(r.compress, None);
1478 }
1479
1480 #[test]
1481 fn response_builder() {
1482 let r = Response::new(StringValue::from("hi"))
1483 .with_header("x-a", "1")
1484 .with_trailer("x-b", "2")
1485 .compress(true);
1486 assert_eq!(r.headers.get("x-a").unwrap(), "1");
1487 assert_eq!(r.trailers.get("x-b").unwrap(), "2");
1488 assert_eq!(r.compress, Some(true));
1489 }
1490
1491 #[test]
1492 fn encodable_owned_proto() {
1493 let m = StringValue::from("hello");
1494 let bytes = Encodable::<StringValue>::encode(&m, CodecFormat::Proto).unwrap();
1495 assert_eq!(
1496 StringValue::decode_from_slice(&bytes).unwrap().value,
1497 "hello"
1498 );
1499 }
1500
1501 #[cfg(feature = "json")]
1502 #[test]
1503 fn encodable_owned_json() {
1504 let m = StringValue::from("hello");
1505 let bytes = Encodable::<StringValue>::encode(&m, CodecFormat::Json).unwrap();
1506 assert_eq!(&bytes[..], b"\"hello\"");
1507 }
1508
1509 #[test]
1510 fn response_encode() {
1511 let r = Response::new(StringValue::from("hi")).with_header("x-a", "1");
1512 let enc = r.encode::<StringValue>(CodecFormat::Proto).unwrap();
1513 assert_eq!(enc.headers.get("x-a").unwrap(), "1");
1514 assert_eq!(
1515 StringValue::decode_from_slice(&enc.body.into_contiguous())
1516 .unwrap()
1517 .value,
1518 "hi"
1519 );
1520 }
1521
1522 #[test]
1523 fn request_context_new() {
1524 let mut h = HeaderMap::new();
1525 h.insert("x-custom", HeaderValue::from_static("v"));
1526 let ctx = RequestContext::new(h);
1527 assert_eq!(
1528 ctx.header(HeaderName::from_static("x-custom")).unwrap(),
1529 "v"
1530 );
1531 assert_eq!(ctx.headers().get("x-custom").unwrap(), "v");
1532 assert!(ctx.deadline().is_none());
1533 assert!(ctx.time_remaining().is_none());
1534 assert!(ctx.extensions().is_empty());
1535 }
1536
1537 #[test]
1538 fn request_context_with_deadline() {
1539 let d = Instant::now();
1540 let ctx = RequestContext::new(HeaderMap::new()).with_deadline(Some(d));
1541 assert_eq!(ctx.deadline(), Some(d));
1542 }
1543
1544 #[test]
1545 fn request_context_time_remaining_saturates_at_zero() {
1546 // Deadline in the past — `time_remaining()` should clamp to zero,
1547 // not underflow.
1548 let past = Instant::now() - Duration::from_secs(60);
1549 let ctx = RequestContext::new(HeaderMap::new()).with_deadline(Some(past));
1550 assert_eq!(ctx.time_remaining(), Some(Duration::ZERO));
1551 }
1552
1553 #[test]
1554 fn request_context_time_remaining_future() {
1555 let future = Instant::now() + Duration::from_secs(60);
1556 let ctx = RequestContext::new(HeaderMap::new()).with_deadline(Some(future));
1557 let remaining = ctx.time_remaining().unwrap();
1558 // Some elapsed time between `with_deadline` and the assertion is
1559 // expected; just bound it.
1560 assert!(remaining > Duration::from_secs(55));
1561 assert!(remaining <= Duration::from_secs(60));
1562 }
1563
1564 #[test]
1565 fn request_context_extensions_mut() {
1566 #[derive(Clone, Debug, PartialEq)]
1567 struct Tag(u8);
1568 let mut ctx = RequestContext::new(HeaderMap::new());
1569 ctx.extensions_mut().insert(Tag(1));
1570 assert_eq!(ctx.extensions().get::<Tag>(), Some(&Tag(1)));
1571 }
1572
1573 #[cfg(feature = "server")]
1574 #[test]
1575 fn request_context_peer_addr_absent() {
1576 // No transport inserted `PeerAddr`; the typed accessor returns
1577 // `None` rather than panicking.
1578 let ctx = RequestContext::new(HeaderMap::new());
1579 assert_eq!(ctx.peer_addr(), None);
1580 }
1581
1582 #[cfg(feature = "server")]
1583 #[test]
1584 fn request_context_peer_addr_present() {
1585 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1586 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080);
1587 let mut ext = http::Extensions::new();
1588 ext.insert(crate::server::PeerAddr(addr));
1589 let ctx = RequestContext::new(HeaderMap::new()).with_extensions(ext);
1590 assert_eq!(ctx.peer_addr(), Some(addr));
1591 }
1592
1593 #[cfg(feature = "server-tls")]
1594 #[test]
1595 fn request_context_peer_certs_absent() {
1596 let ctx = RequestContext::new(HeaderMap::new());
1597 assert!(ctx.peer_certs().is_none());
1598 }
1599
1600 #[test]
1601 fn response_map_body_preserves_metadata() {
1602 let r = Response::new(2u32)
1603 .with_header("x-h", "1")
1604 .with_trailer("x-t", "2")
1605 .compress(true);
1606 let r = r.map_body(|n| n.to_string());
1607 assert_eq!(r.body, "2");
1608 assert_eq!(r.headers.get("x-h").unwrap(), "1");
1609 assert_eq!(r.trailers.get("x-t").unwrap(), "2");
1610 assert_eq!(r.compress, Some(true));
1611 }
1612
1613 #[tokio::test]
1614 async fn response_stream_yields_items() {
1615 use futures::StreamExt;
1616 let r: Response<ServiceStream<i32>> =
1617 Response::stream(futures::stream::iter([Ok(1), Ok(2), Ok(3)]));
1618 let collected: Vec<_> = r.body.map(|x| x.unwrap()).collect().await;
1619 assert_eq!(collected, vec![1, 2, 3]);
1620 }
1621
1622 #[test]
1623 #[should_panic]
1624 fn with_header_panics_on_invalid_name() {
1625 let _ = Response::new(()).with_header("invalid header name", "v");
1626 }
1627
1628 #[test]
1629 fn try_with_header_errors_on_invalid_name() {
1630 let err = Response::new(())
1631 .try_with_header("invalid header name", "v")
1632 .unwrap_err();
1633 assert!(err.is::<http::header::InvalidHeaderName>());
1634 }
1635
1636 #[test]
1637 fn try_with_header_ok_appends() {
1638 let r = Response::new(())
1639 .try_with_header("x-a", "1")
1640 .unwrap()
1641 .try_with_header("x-a", "2")
1642 .unwrap();
1643 let vals: Vec<_> = r.headers.get_all("x-a").iter().collect();
1644 assert_eq!(vals.len(), 2);
1645 }
1646
1647 #[test]
1648 fn try_with_trailer_errors_on_invalid_value() {
1649 // Newlines are not permitted in header values.
1650 let err = Response::new(())
1651 .try_with_trailer("x-t", "bad\nvalue")
1652 .unwrap_err();
1653 assert!(err.is::<http::header::InvalidHeaderValue>());
1654 }
1655
1656 #[test]
1657 fn encode_view_body_proto() {
1658 use buffa_types::google::protobuf::__buffa::view::StringValueView;
1659 let v = StringValueView {
1660 value: "hi",
1661 ..Default::default()
1662 };
1663 let bytes = encode_view_body(&v, CodecFormat::Proto).unwrap();
1664 assert_eq!(StringValue::decode_from_slice(&bytes).unwrap().value, "hi");
1665 }
1666
1667 #[test]
1668 fn encode_view_body_json_errors() {
1669 use buffa_types::google::protobuf::__buffa::view::StringValueView;
1670 let v = StringValueView::default();
1671 let err = encode_view_body(&v, CodecFormat::Json).unwrap_err();
1672 assert_eq!(err.code, crate::ErrorCode::Unimplemented);
1673 assert!(err.message.as_deref().unwrap().contains("JSON codec"));
1674 }
1675
1676 // Manual Encodable<StringValue> impl modelling what codegen emits
1677 // for FooView<'_>. Shared by the MaybeBorrowed tests below.
1678 struct V<'a>(buffa_types::google::protobuf::__buffa::view::StringValueView<'a>);
1679 impl Encodable<StringValue> for V<'_> {
1680 fn encode(&self, c: CodecFormat) -> Result<Bytes, ConnectError> {
1681 encode_view_body(&self.0, c)
1682 }
1683 }
1684
1685 #[test]
1686 fn maybe_borrowed_dispatch() {
1687 use buffa_types::google::protobuf::__buffa::view::StringValueView;
1688 let owned: MaybeBorrowed<StringValue, V<'_>> =
1689 MaybeBorrowed::Owned(StringValue::from("owned"));
1690 let borrowed = MaybeBorrowed::Borrowed(V(StringValueView {
1691 value: "view",
1692 ..Default::default()
1693 }));
1694 assert_eq!(
1695 StringValue::decode_from_slice(&owned.encode(CodecFormat::Proto).unwrap())
1696 .unwrap()
1697 .value,
1698 "owned"
1699 );
1700 assert_eq!(
1701 StringValue::decode_from_slice(&borrowed.encode(CodecFormat::Proto).unwrap())
1702 .unwrap()
1703 .value,
1704 "view"
1705 );
1706 }
1707
1708 #[test]
1709 fn maybe_borrowed_borrowed_json_unimplemented() {
1710 use buffa_types::google::protobuf::__buffa::view::StringValueView;
1711 let borrowed: MaybeBorrowed<StringValue, V<'_>> =
1712 MaybeBorrowed::Borrowed(V(StringValueView::default()));
1713 let err = borrowed.encode(CodecFormat::Json).unwrap_err();
1714 assert_eq!(err.code, crate::ErrorCode::Unimplemented);
1715 }
1716
1717 #[test]
1718 fn pre_encoded_proto_round_trip() {
1719 let m = StringValue::from("pre-encoded");
1720 let bytes = m.encode_to_bytes();
1721 let body = PreEncoded::<StringValue>::from_bytes_unchecked(bytes.clone());
1722 let out = Encodable::<StringValue>::encode(&body, CodecFormat::Proto).unwrap();
1723 assert_eq!(out, bytes);
1724 assert_eq!(
1725 StringValue::decode_from_slice(&out).unwrap().value,
1726 "pre-encoded"
1727 );
1728 }
1729
1730 #[cfg(feature = "json")]
1731 #[test]
1732 fn pre_encoded_json_decodes_then_serializes() {
1733 // The JSON path round-trips: proto bytes → owned `M` → JSON. Slow,
1734 // but correct — see the `# Codec behaviour` doc on `PreEncoded`.
1735 let m = StringValue::from("hi");
1736 let body = PreEncoded::<StringValue>::from_bytes_unchecked(m.encode_to_bytes());
1737 let out = Encodable::<StringValue>::encode(&body, CodecFormat::Json).unwrap();
1738 // Output should match what serializing the owned message directly
1739 // would produce.
1740 assert_eq!(out, Bytes::from(serde_json::to_vec(&m).unwrap()));
1741 }
1742
1743 #[cfg(feature = "json")]
1744 #[test]
1745 fn pre_encoded_json_decode_failure_is_internal_error() {
1746 // `from_bytes_unchecked` is unvalidated on the proto path. The JSON
1747 // fallback necessarily decodes; if that fails (the wrapped bytes
1748 // were never a valid `M`), the server-side `internal` error surfaces
1749 // closer to the construction bug than the proto path would.
1750 //
1751 // Field 1 (LEN) declares 99 bytes but only 2 follow — guaranteed
1752 // truncated for `StringValue`.
1753 let body = PreEncoded::<StringValue> {
1754 bytes: Bytes::from_static(&[0x0a, 0x63, b'h', b'i']),
1755 _marker: std::marker::PhantomData,
1756 };
1757 let err = Encodable::<StringValue>::encode(&body, CodecFormat::Json).unwrap_err();
1758 assert_eq!(err.code, crate::ErrorCode::Internal);
1759 assert!(err.message.as_deref().unwrap().contains("did not decode"));
1760 }
1761
1762 #[test]
1763 fn pre_encoded_from_view() {
1764 use buffa::view::ViewEncode;
1765 use buffa_types::google::protobuf::__buffa::view::StringValueView;
1766 let v = StringValueView {
1767 value: "from-view",
1768 ..Default::default()
1769 };
1770 // `from_view` infers `M = StringValue` from `StringValueView::Owned`.
1771 let body = PreEncoded::from_view(&v);
1772 let out = Encodable::<StringValue>::encode(&body, CodecFormat::Proto).unwrap();
1773 assert_eq!(out, v.encode_to_bytes());
1774 assert_eq!(
1775 StringValue::decode_from_slice(&out).unwrap().value,
1776 "from-view"
1777 );
1778 }
1779
1780 #[test]
1781 fn pre_encoded_from_message() {
1782 let m = StringValue::from("from-message");
1783 // `from_message` infers `M` from the receiver — no annotation.
1784 let body = PreEncoded::from_message(&m);
1785 let out = Encodable::<StringValue>::encode(&body, CodecFormat::Proto).unwrap();
1786 assert_eq!(out, m.encode_to_bytes());
1787
1788 // `From<&M>` is the same conversion via `.into()`.
1789 let body2: PreEncoded<StringValue> = (&m).into();
1790 let out2 = Encodable::<StringValue>::encode(&body2, CodecFormat::Proto).unwrap();
1791 assert_eq!(out2, out);
1792 }
1793
1794 #[cfg(feature = "json")]
1795 #[test]
1796 fn pre_encoded_codec_fidelity_diverges_on_unknown_fields() {
1797 // Documents the codec-dependent fidelity caveat: the proto path
1798 // is byte-exact (unknown fields preserved); the JSON path
1799 // round-trips through `M` (unknown fields dropped). Only relevant
1800 // for `from_bytes_unchecked` bytes sourced externally.
1801 //
1802 // Wire bytes: field 1 = "hi" (the known `StringValue.value`),
1803 // plus field 2 = varint 42 (unknown to `StringValue`).
1804 let bytes_with_unknown =
1805 Bytes::from_static(&[0x0a, 0x02, b'h', b'i', /* tag 2 varint */ 0x10, 42]);
1806 let body = PreEncoded::<StringValue> {
1807 bytes: bytes_with_unknown.clone(),
1808 _marker: std::marker::PhantomData,
1809 };
1810
1811 // Proto: byte-exact passthrough, unknown field preserved.
1812 let proto = Encodable::<StringValue>::encode(&body, CodecFormat::Proto).unwrap();
1813 assert_eq!(proto, bytes_with_unknown);
1814
1815 // JSON: round-trips through `StringValue`, which drops the
1816 // unknown field. Output equals serializing the bare known
1817 // message.
1818 let json = Encodable::<StringValue>::encode(&body, CodecFormat::Json).unwrap();
1819 assert_eq!(
1820 json,
1821 Bytes::from(serde_json::to_vec(&StringValue::from("hi")).unwrap())
1822 );
1823 }
1824
1825 // --- proto-only (json feature disabled) fallback behaviour ---
1826
1827 #[cfg(not(feature = "json"))]
1828 #[test]
1829 fn encodable_owned_json_is_unimplemented_without_feature() {
1830 let m = StringValue::from("hello");
1831 // Proto still encodes normally...
1832 assert!(Encodable::<StringValue>::encode(&m, CodecFormat::Proto).is_ok());
1833 // ...but the JSON codec is compiled out and reports it cleanly.
1834 let err = Encodable::<StringValue>::encode(&m, CodecFormat::Json).unwrap_err();
1835 assert_eq!(err.code, crate::ErrorCode::Unimplemented);
1836 }
1837
1838 #[cfg(not(feature = "json"))]
1839 #[test]
1840 fn pre_encoded_json_is_unimplemented_without_feature() {
1841 let m = StringValue::from("hi");
1842 let body = PreEncoded::<StringValue>::from_bytes_unchecked(m.encode_to_bytes());
1843 assert!(Encodable::<StringValue>::encode(&body, CodecFormat::Proto).is_ok());
1844 let err = Encodable::<StringValue>::encode(&body, CodecFormat::Json).unwrap_err();
1845 assert_eq!(err.code, crate::ErrorCode::Unimplemented);
1846 }
1847
1848 #[test]
1849 fn pre_encoded_is_typed() {
1850 // `PreEncoded<M>` only implements `Encodable<M>` — the type witness
1851 // means `PreEncoded<StringValue>` cannot be used where
1852 // `Encodable<Int32Value>` is required. Verified at compile time;
1853 // this test just exercises the happy path for both types.
1854 use buffa_types::google::protobuf::Int32Value;
1855 let s = PreEncoded::<StringValue>::from_bytes_unchecked(
1856 StringValue::from("a").encode_to_bytes(),
1857 );
1858 let i =
1859 PreEncoded::<Int32Value>::from_bytes_unchecked(Int32Value::from(1).encode_to_bytes());
1860 Encodable::<StringValue>::encode(&s, CodecFormat::Proto).unwrap();
1861 Encodable::<Int32Value>::encode(&i, CodecFormat::Proto).unwrap();
1862 // The following would not compile:
1863 // Encodable::<Int32Value>::encode(&s, CodecFormat::Proto)
1864 }
1865
1866 #[test]
1867 #[cfg(debug_assertions)]
1868 #[should_panic(expected = "do not decode as")]
1869 fn pre_encoded_from_bytes_unchecked_debug_asserts() {
1870 // In debug builds, `from_bytes_unchecked` decodes once to surface
1871 // mismatched bytes early. Field 1 (LEN) declares 99 bytes; only 2
1872 // follow.
1873 let _ = PreEncoded::<StringValue>::from_bytes_unchecked(Bytes::from_static(&[
1874 0x0a, 0x63, b'h', b'i',
1875 ]));
1876 }
1877
1878 #[test]
1879 fn request_context_with_extensions() {
1880 #[derive(Clone, Debug, PartialEq)]
1881 struct Peer(u32);
1882 let mut ext = http::Extensions::new();
1883 ext.insert(Peer(7));
1884 let ctx = RequestContext::new(HeaderMap::new()).with_extensions(ext);
1885 assert_eq!(ctx.extensions().get::<Peer>(), Some(&Peer(7)));
1886 }
1887
1888 #[test]
1889 fn request_context_with_spec_and_protocol() {
1890 use crate::spec::{Spec, StreamType};
1891
1892 // Default-constructed context has neither.
1893 let ctx = RequestContext::new(HeaderMap::new());
1894 assert_eq!(ctx.spec(), None);
1895 assert_eq!(ctx.protocol(), None);
1896
1897 // Both round-trip through the builders.
1898 const SPEC: Spec = Spec::server("/pkg.Svc/M", StreamType::Unary);
1899 let ctx = RequestContext::new(HeaderMap::new())
1900 .with_spec(Some(SPEC))
1901 .with_protocol(Some(crate::Protocol::Grpc));
1902 assert_eq!(ctx.spec(), Some(SPEC));
1903 assert_eq!(ctx.protocol(), Some(crate::Protocol::Grpc));
1904
1905 // Builders accept `None` to clear (matches `with_deadline`).
1906 let ctx = ctx.with_spec(None).with_protocol(None);
1907 assert_eq!(ctx.spec(), None);
1908 assert_eq!(ctx.protocol(), None);
1909 }
1910
1911 #[test]
1912 fn request_context_with_path() {
1913 // Hand-built contexts (tests, custom dispatchers) have no path.
1914 let ctx = RequestContext::new(HeaderMap::new());
1915 assert_eq!(ctx.path(), None);
1916
1917 // Round-trips through the builder.
1918 let ctx = RequestContext::new(HeaderMap::new()).with_path("/pkg.Svc/M");
1919 assert_eq!(ctx.path(), Some("/pkg.Svc/M"));
1920
1921 // The builder takes ownership (Into<String>) so callers can pass
1922 // borrowed or owned without an extra clone.
1923 let owned = String::from("/pkg.Svc/Other");
1924 let ctx = RequestContext::new(HeaderMap::new()).with_path(owned);
1925 assert_eq!(ctx.path(), Some("/pkg.Svc/Other"));
1926
1927 // The builder does not normalize or validate — `Some("")` is
1928 // preserved verbatim. The dispatch path always supplies a non-empty
1929 // leading-slash form; `Some("")` only reaches consumers from a
1930 // misconfigured custom dispatch shim, which is a wiring bug they
1931 // should surface rather than silently coerce to `None`.
1932 let ctx = RequestContext::new(HeaderMap::new()).with_path("");
1933 assert_eq!(ctx.path(), Some(""));
1934 }
1935}