Skip to main content

praxis_filter/body/
mode.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Body delivery mode declarations.
5
6// -----------------------------------------------------------------------------
7// BodyMode
8// -----------------------------------------------------------------------------
9
10/// Controls how body chunks are delivered to a filter.
11///
12/// ```
13/// use praxis_filter::BodyMode;
14///
15/// let mode = BodyMode::default();
16/// assert!(matches!(mode, BodyMode::Stream));
17///
18/// let buffered = BodyMode::StreamBuffer {
19///     max_bytes: Some(1024),
20/// };
21/// assert!(matches!(
22///     buffered,
23///     BodyMode::StreamBuffer {
24///         max_bytes: Some(1024)
25///     }
26/// ));
27///
28/// let stream_buf = BodyMode::StreamBuffer { max_bytes: None };
29/// assert!(matches!(
30///     stream_buf,
31///     BodyMode::StreamBuffer { max_bytes: None }
32/// ));
33///
34/// let limited = BodyMode::StreamBuffer {
35///     max_bytes: Some(1024),
36/// };
37/// assert!(matches!(
38///     limited,
39///     BodyMode::StreamBuffer {
40///         max_bytes: Some(1024)
41///     }
42/// ));
43///
44/// let size_limited = BodyMode::SizeLimit { max_bytes: 2048 };
45/// assert!(matches!(
46///     size_limited,
47///     BodyMode::SizeLimit { max_bytes: 2048 }
48/// ));
49/// ```
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51#[non_exhaustive]
52pub enum BodyMode {
53    /// Deliver chunks as they arrive. Low latency, low memory.
54    ///
55    /// ```
56    /// use praxis_filter::BodyMode;
57    ///
58    /// let mode = BodyMode::Stream;
59    /// assert_eq!(mode, BodyMode::default());
60    /// ```
61    #[default]
62    Stream,
63
64    /// Deliver chunks incrementally (like [`Stream`]) but accumulate
65    /// them and defer upstream forwarding until a filter returns
66    /// [`FilterAction::Release`] or end-of-stream is reached.
67    ///
68    /// When `max_bytes` is `Some`, requests exceeding the limit
69    /// receive 413. When `None` and no global body size ceiling is
70    /// configured, body buffering is unbounded; a warning is emitted
71    /// at pipeline build time in that case.
72    ///
73    /// ```
74    /// use praxis_filter::BodyMode;
75    ///
76    /// let unlimited = BodyMode::StreamBuffer { max_bytes: None };
77    /// assert!(matches!(
78    ///     unlimited,
79    ///     BodyMode::StreamBuffer { max_bytes: None }
80    /// ));
81    ///
82    /// let limited = BodyMode::StreamBuffer {
83    ///     max_bytes: Some(4096),
84    /// };
85    /// assert!(matches!(
86    ///     limited,
87    ///     BodyMode::StreamBuffer {
88    ///         max_bytes: Some(4096)
89    ///     }
90    /// ));
91    /// ```
92    ///
93    /// [`Stream`]: BodyMode::Stream
94    /// [`FilterAction::Release`]: crate::FilterAction::Release
95    StreamBuffer {
96        /// Optional maximum body size in bytes. `None` means
97        /// unbounded buffering (a warning is emitted at build time).
98        max_bytes: Option<usize>,
99    },
100
101    /// Stream chunks through without buffering, but enforce a byte
102    /// ceiling. Returns 413 if the running byte count exceeds
103    /// `max_bytes`.
104    ///
105    /// Used by [`apply_body_limits`] when no filter needs body access
106    /// but a global size limit is configured.
107    ///
108    /// ```
109    /// use praxis_filter::BodyMode;
110    ///
111    /// let mode = BodyMode::SizeLimit { max_bytes: 4096 };
112    /// assert!(matches!(mode, BodyMode::SizeLimit { max_bytes: 4096 }));
113    /// ```
114    ///
115    /// [`apply_body_limits`]: crate::FilterPipeline::apply_body_limits
116    SizeLimit {
117        /// Maximum body size in bytes.
118        max_bytes: usize,
119    },
120}
121
122// -----------------------------------------------------------------------------
123// Tests
124// -----------------------------------------------------------------------------
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn body_mode_default_is_stream() {
132        assert_eq!(
133            BodyMode::default(),
134            BodyMode::Stream,
135            "default BodyMode should be Stream"
136        );
137    }
138
139    #[test]
140    fn body_mode_stream_buffer_unlimited() {
141        let mode = BodyMode::StreamBuffer { max_bytes: None };
142        assert!(
143            matches!(mode, BodyMode::StreamBuffer { max_bytes: None }),
144            "StreamBuffer should support unlimited mode"
145        );
146    }
147
148    #[test]
149    fn body_mode_stream_buffer_with_limit() {
150        let mode = BodyMode::StreamBuffer { max_bytes: Some(4096) };
151        assert!(
152            matches!(mode, BodyMode::StreamBuffer { max_bytes: Some(4096) }),
153            "StreamBuffer should carry configured byte limit"
154        );
155    }
156
157    #[test]
158    fn body_mode_size_limit_carries_limit() {
159        let mode = BodyMode::SizeLimit { max_bytes: 2048 };
160        assert!(
161            matches!(mode, BodyMode::SizeLimit { max_bytes: 2048 }),
162            "SizeLimit variant should carry configured limit"
163        );
164    }
165
166    #[test]
167    fn body_mode_size_limit_is_distinct_from_stream_buffer() {
168        assert_ne!(
169            BodyMode::SizeLimit { max_bytes: 100 },
170            BodyMode::StreamBuffer { max_bytes: Some(100) },
171            "SizeLimit and StreamBuffer should be distinct even with same limit"
172        );
173    }
174
175    #[test]
176    fn body_mode_stream_buffer_is_distinct_from_stream() {
177        assert_ne!(
178            BodyMode::StreamBuffer { max_bytes: None },
179            BodyMode::Stream,
180            "StreamBuffer and Stream should be distinct variants"
181        );
182    }
183}