Skip to main content

grpc_webnext/
framing.rs

1//! Fetch (unary) response framing.
2//!
3//! Browsers cannot read HTTP trailers, so a unary response body is written as
4//! two 4-byte-big-endian length-prefixed blocks:
5//!
6//! ```text
7//! [ u32 len | message bytes ]
8//! [ u32 len | Trailer bytes ]
9//! ```
10//!
11//! Initial metadata still travels in HTTP response headers; the `Trailer` block
12//! carries the gRPC status and trailing metadata.
13
14use crate::pb::Trailer;
15use bytes::{Buf, BufMut, Bytes, BytesMut};
16use prost::Message;
17
18pub const LEN_PREFIX: usize = 4;
19
20#[derive(Debug, thiserror::Error)]
21pub enum FetchError {
22    #[error("response body exceeds configured size limit ({limit} bytes)")]
23    TooLarge { limit: usize },
24    #[error("response body truncated: expected {expected} more bytes, had {had}")]
25    Truncated { expected: usize, had: usize },
26    #[error("failed to decode Trailer: {0}")]
27    Decode(#[from] prost::DecodeError),
28}
29
30/// Encode a unary response body from the message bytes and its trailer.
31pub fn encode_response_body(message: &[u8], trailer: &Trailer) -> Bytes {
32    let trailer_len = trailer.encoded_len();
33    let mut buf = BytesMut::with_capacity(LEN_PREFIX + message.len() + LEN_PREFIX + trailer_len);
34    buf.put_u32(message.len() as u32);
35    buf.put_slice(message);
36    buf.put_u32(trailer_len as u32);
37    trailer.encode(&mut buf).expect("BytesMut has capacity");
38    buf.freeze()
39}
40
41/// The empty message block (`[u32 len = 0]`): a trailers-only response (an error
42/// with no message) still needs the leading message block before the trailer block.
43pub const EMPTY_MESSAGE_BLOCK: [u8; LEN_PREFIX] = [0; LEN_PREFIX];
44
45/// Encode a `+proto` **unary request** body: a single `[u32 len | message]` block,
46/// mirroring the response's message block. The client prepends the length it already
47/// knows (protobuf is serialized whole), so the server/proxy can turn it into a gRPC
48/// frame — `[1-byte flag]` + this — and stream it upstream without buffering to measure.
49pub fn encode_request_body(message: &[u8]) -> Bytes {
50    let mut buf = BytesMut::with_capacity(LEN_PREFIX + message.len());
51    buf.put_u32(message.len() as u32);
52    buf.put_slice(message);
53    buf.freeze()
54}
55
56/// Encode just the trailing `[u32 len | Trailer bytes]` block. Used by the streaming
57/// response path, which forwards the message block straight from the inner gRPC frame
58/// (its `[u32 len | message]` layout already matches ours once the 1-byte compression
59/// flag is dropped) and only needs to append this at the end.
60pub fn encode_trailer_block(trailer: &Trailer) -> Bytes {
61    let trailer_len = trailer.encoded_len();
62    let mut buf = BytesMut::with_capacity(LEN_PREFIX + trailer_len);
63    buf.put_u32(trailer_len as u32);
64    trailer.encode(&mut buf).expect("BytesMut has capacity");
65    buf.freeze()
66}
67
68/// Decode a buffered unary response body into `(message bytes, Trailer)`.
69///
70/// `limit` bounds the total body size the caller is willing to buffer.
71pub fn decode_response_body(mut body: Bytes, limit: usize) -> Result<(Bytes, Trailer), FetchError> {
72    if body.len() > limit {
73        return Err(FetchError::TooLarge { limit });
74    }
75
76    let message = take_block(&mut body)?;
77    let trailer_bytes = take_block(&mut body)?;
78    let trailer = Trailer::decode(trailer_bytes)?;
79    Ok((message, trailer))
80}
81
82/// Read one `[u32 len | bytes]` block, advancing `body`.
83fn take_block(body: &mut Bytes) -> Result<Bytes, FetchError> {
84    if body.remaining() < LEN_PREFIX {
85        return Err(FetchError::Truncated {
86            expected: LEN_PREFIX,
87            had: body.remaining(),
88        });
89    }
90    let len = body.get_u32() as usize;
91    if body.remaining() < len {
92        return Err(FetchError::Truncated {
93            expected: len,
94            had: body.remaining(),
95        });
96    }
97    Ok(body.split_to(len))
98}