Skip to main content

tako_rs_core/grpc/
streaming.rs

1//! Streaming gRPC integration: server-streaming responder, client-streaming
2//! extractor, the internal frame de-framer, and the bidirectional scaffold
3//! that wires a handler's inbound and outbound streams together.
4
5use std::convert::Infallible;
6use std::pin::Pin;
7use std::task::Context;
8use std::task::Poll;
9
10use bytes::Bytes;
11use bytes::BytesMut;
12use futures_util::Stream;
13use futures_util::StreamExt;
14use http::HeaderMap;
15use http::StatusCode;
16use http_body::Frame;
17use http_body_util::StreamBody;
18use prost::Message;
19
20use super::GrpcError;
21use super::framing::MAX_GRPC_MESSAGE_SIZE;
22use super::framing::grpc_encode;
23use super::status::GrpcStatus;
24use crate::body::TakoBody;
25use crate::extractors::FromRequest;
26use crate::responder::Responder;
27use crate::types::Request;
28use crate::types::Response;
29
30/// Server-streaming gRPC response.
31///
32/// Encodes each `Ok` item with the standard length-prefix framing and emits a
33/// final HTTP/2 trailer carrying `grpc-status` (`Ok` if the stream terminates
34/// cleanly) and `grpc-message` when applicable.
35pub struct GrpcServerStream<S, T>
36where
37  S: Stream<Item = Result<T, GrpcStatus>> + Send + 'static,
38  T: Message + Send + 'static,
39{
40  pub stream: S,
41  /// Server metadata sent as response headers (initial metadata).
42  pub initial_metadata: HeaderMap,
43}
44
45impl<S, T> GrpcServerStream<S, T>
46where
47  S: Stream<Item = Result<T, GrpcStatus>> + Send + 'static,
48  T: Message + Send + 'static,
49{
50  pub fn new(stream: S) -> Self {
51    Self {
52      stream,
53      initial_metadata: HeaderMap::new(),
54    }
55  }
56
57  pub fn with_metadata(mut self, headers: HeaderMap) -> Self {
58    self.initial_metadata = headers;
59    self
60  }
61}
62
63impl<S, T> Responder for GrpcServerStream<S, T>
64where
65  S: Stream<Item = Result<T, GrpcStatus>> + Send + 'static,
66  T: Message + Send + 'static,
67{
68  fn into_response(self) -> Response {
69    use std::sync::Arc;
70    use std::sync::atomic::AtomicBool;
71    use std::sync::atomic::Ordering;
72
73    // Track whether the user stream already emitted a terminal `grpc-status`
74    // trailer (i.e. ended in `Err(status)`). Without this, the unconditional
75    // OK trailer below would double the trailer headers — RFC §8.1 / the
76    // gRPC HTTP/2 mapping disallows two `grpc-status` values per response.
77    let error_emitted = Arc::new(AtomicBool::new(false));
78    let mark_err = error_emitted.clone();
79    let stream = self.stream.map(move |item| match item {
80      Ok(msg) => {
81        let bytes = grpc_encode(&msg);
82        Ok::<_, Infallible>(Frame::data(Bytes::from(bytes)))
83      }
84      Err(status) => {
85        mark_err.store(true, Ordering::Release);
86        Ok(Frame::trailers(status.write_trailers()))
87      }
88    });
89
90    // After the user stream exhausts, append a final `grpc-status: 0`
91    // trailer — but only if no error trailer was emitted upstream.
92    let check_err = error_emitted.clone();
93    let mut once = false;
94    let trailer = futures_util::stream::iter(std::iter::from_fn(move || {
95      if once {
96        None
97      } else {
98        once = true;
99        if check_err.load(Ordering::Acquire) {
100          None
101        } else {
102          Some(Ok::<_, Infallible>(Frame::trailers(
103            GrpcStatus::ok().write_trailers(),
104          )))
105        }
106      }
107    }));
108    let combined = stream.chain(trailer);
109
110    // SAFETY of `.expect(...)`: `Response::builder().status(...).header(...).body(...)`
111    // can only return Err if a `header_name`/`header_value` fails to convert.
112    // Here both inputs are `HeaderName::from_static`/`HeaderValue::from_static`,
113    // which are pre-validated at compile time. The status code and body are
114    // infallible.
115    //
116    // If you ADD a `.header(dynamic_name, dynamic_value)` to this builder
117    // chain, the panic message becomes misleading — the failure mode is no
118    // longer impossible. In that case, switch to `.body(...)?` + propagate
119    // via `Result<Response, _>` (callers can map back via Responder), or
120    // construct `http::Response::new(...)` directly + setters.
121    let mut resp = http::Response::builder()
122      .status(StatusCode::OK)
123      .header(
124        http::header::CONTENT_TYPE,
125        http::HeaderValue::from_static("application/grpc"),
126      )
127      .body(TakoBody::new(StreamBody::new(combined)))
128      .expect("static headers + body construction is infallible");
129    let headers = resp.headers_mut();
130    for (k, v) in &self.initial_metadata {
131      headers.insert(k.clone(), v.clone());
132    }
133    resp
134  }
135}
136
137/// Client-streaming gRPC extractor.
138///
139/// Wraps the request body into a `Stream<Item = Result<T, GrpcError>>` so a
140/// handler can iterate over framed protobuf messages.
141pub struct GrpcClientStream<T: Message + Default + Send + 'static> {
142  pub stream: Pin<Box<dyn Stream<Item = Result<T, GrpcError>> + Send>>,
143}
144
145impl<'a, T> FromRequest<'a> for GrpcClientStream<T>
146where
147  T: Message + Default + Send + 'static,
148{
149  type Error = GrpcError;
150
151  fn from_request(
152    req: &'a mut Request,
153  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
154    async move {
155      let ct = req
156        .headers()
157        .get(http::header::CONTENT_TYPE)
158        .and_then(|v| v.to_str().ok())
159        .unwrap_or("");
160      if !ct.starts_with("application/grpc") {
161        return Err(GrpcError::InvalidContentType);
162      }
163
164      // Take the body out of the request — `into_body` is not directly
165      // available without owning the request; we drain incrementally instead.
166      // Collect a one-shot producer and parse multiple frames out of it.
167      let body = std::mem::take(req.body_mut());
168      let stream = GrpcFrameStream::new(body);
169      Ok(GrpcClientStream {
170        stream: Box::pin(stream),
171      })
172    }
173  }
174}
175
176struct GrpcFrameStream<T> {
177  body: TakoBody,
178  buffer: BytesMut,
179  finished: bool,
180  _marker: std::marker::PhantomData<fn() -> T>,
181}
182
183impl<T> GrpcFrameStream<T> {
184  fn new(body: TakoBody) -> Self {
185    Self {
186      body,
187      buffer: BytesMut::new(),
188      finished: false,
189      _marker: std::marker::PhantomData,
190    }
191  }
192}
193
194impl<T> Stream for GrpcFrameStream<T>
195where
196  T: Message + Default,
197{
198  type Item = Result<T, GrpcError>;
199
200  fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
201    let this = self.get_mut();
202    loop {
203      // Try to emit a frame from the buffer.
204      if this.buffer.len() >= 5 {
205        let msg_len = u32::from_be_bytes([
206          this.buffer[1],
207          this.buffer[2],
208          this.buffer[3],
209          this.buffer[4],
210        ]) as usize;
211        if msg_len > MAX_GRPC_MESSAGE_SIZE {
212          return Poll::Ready(Some(Err(GrpcError::MessageTooLarge)));
213        }
214        if this.buffer.len() >= 5 + msg_len {
215          if this.buffer[0] != 0 {
216            return Poll::Ready(Some(Err(GrpcError::CompressionUnsupported)));
217          }
218          let payload = this.buffer.split_to(5 + msg_len);
219          let msg_bytes = &payload[5..5 + msg_len];
220          return match T::decode(msg_bytes) {
221            Ok(m) => Poll::Ready(Some(Ok(m))),
222            Err(e) => Poll::Ready(Some(Err(GrpcError::DecodeError(e.to_string())))),
223          };
224        }
225      }
226
227      if this.finished {
228        return Poll::Ready(None);
229      }
230
231      // Pull more bytes off the body.
232      let mut body = Pin::new(&mut this.body);
233      match http_body::Body::poll_frame(body.as_mut(), cx) {
234        Poll::Ready(Some(Ok(frame))) => {
235          if let Some(data) = frame.data_ref() {
236            this.buffer.extend_from_slice(data);
237          }
238        }
239        Poll::Ready(Some(Err(e))) => {
240          return Poll::Ready(Some(Err(GrpcError::BodyReadError(e.to_string()))));
241        }
242        Poll::Ready(None) => {
243          this.finished = true;
244        }
245        Poll::Pending => return Poll::Pending,
246      }
247    }
248  }
249}
250
251/// Bidirectional gRPC handler scaffold.
252///
253/// Combines a [`GrpcClientStream`] (for inbound) with a `GrpcServerStream`
254/// builder (for outbound). The handler reads inbound frames as needed and
255/// drives the outbound stream as a Responder.
256pub struct GrpcBidi<Req, Resp>
257where
258  Req: Message + Default + Send + 'static,
259  Resp: Message + Send + 'static,
260{
261  pub inbound: GrpcClientStream<Req>,
262  pub _phantom: std::marker::PhantomData<Resp>,
263}
264
265impl<'a, Req, Resp> FromRequest<'a> for GrpcBidi<Req, Resp>
266where
267  Req: Message + Default + Send + 'static,
268  Resp: Message + Send + 'static,
269{
270  type Error = GrpcError;
271
272  fn from_request(
273    req: &'a mut Request,
274  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
275    async move {
276      Ok(GrpcBidi {
277        inbound: GrpcClientStream::<Req>::from_request(req).await?,
278        _phantom: std::marker::PhantomData,
279      })
280    }
281  }
282}