Skip to main content

praxis_core/config/
body_limits.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Body size limit configuration.
5
6use serde::Deserialize;
7
8// -----------------------------------------------------------------------------
9// BodyLimitsConfig
10// -----------------------------------------------------------------------------
11
12/// Default body limit applied when the operator omits the field.
13pub const DEFAULT_MAX_BODY_BYTES: usize = 10_485_760; // 10 MiB
14
15/// Absolute hard ceiling for body buffering (64 MiB).
16///
17/// Applied to any unbounded stream-buffer mode, even when
18/// `insecure_options.allow_unbounded_body` is set.
19pub const ABSOLUTE_MAX_BODY_BYTES: usize = 67_108_864; // 64 MiB
20
21/// Global hard ceilings on request and response body size.
22///
23/// Both limits default to 10 MiB. Setting either to `null`
24/// in YAML removes the ceiling, but Praxis will refuse to
25/// start unless `insecure_options.allow_unbounded_body` is
26/// also `true`.
27///
28/// ```
29/// use praxis_core::config::{BodyLimitsConfig, DEFAULT_MAX_BODY_BYTES};
30///
31/// let limits = BodyLimitsConfig::default();
32/// assert_eq!(limits.max_request_bytes, Some(DEFAULT_MAX_BODY_BYTES));
33/// assert_eq!(limits.max_response_bytes, Some(DEFAULT_MAX_BODY_BYTES));
34/// ```
35///
36/// ```
37/// use praxis_core::config::BodyLimitsConfig;
38///
39/// let limits: BodyLimitsConfig = serde_yaml::from_str(
40///     r#"
41/// max_request_bytes: 5242880
42/// max_response_bytes: 2097152
43/// "#,
44/// )
45/// .unwrap();
46/// assert_eq!(limits.max_request_bytes, Some(5_242_880));
47/// assert_eq!(limits.max_response_bytes, Some(2_097_152));
48/// ```
49#[derive(Debug, Clone, Deserialize, serde::Serialize)]
50#[serde(default, deny_unknown_fields)]
51pub struct BodyLimitsConfig {
52    /// Maximum request body size in bytes.
53    ///
54    /// Defaults to 10 MiB. `None` (YAML `null`) disables the
55    /// limit and requires `insecure_options.allow_unbounded_body`.
56    #[serde(default = "default_max_body_bytes")]
57    pub max_request_bytes: Option<usize>,
58
59    /// Maximum response body size in bytes.
60    ///
61    /// Defaults to 10 MiB. `None` (YAML `null`) disables the
62    /// limit and requires `insecure_options.allow_unbounded_body`.
63    #[serde(default = "default_max_body_bytes")]
64    pub max_response_bytes: Option<usize>,
65}
66
67impl Default for BodyLimitsConfig {
68    fn default() -> Self {
69        Self {
70            max_request_bytes: default_max_body_bytes(),
71            max_response_bytes: default_max_body_bytes(),
72        }
73    }
74}
75
76/// Serde default for body limit fields.
77#[expect(clippy::unnecessary_wraps, reason = "serde default requires matching field type")]
78fn default_max_body_bytes() -> Option<usize> {
79    Some(DEFAULT_MAX_BODY_BYTES)
80}
81
82// -----------------------------------------------------------------------------
83// Tests
84// -----------------------------------------------------------------------------
85
86#[cfg(test)]
87#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
88#[allow(
89    clippy::unwrap_used,
90    clippy::expect_used,
91    clippy::indexing_slicing,
92    clippy::needless_raw_strings,
93    clippy::needless_raw_string_hashes,
94    reason = "tests use unwrap/expect/indexing/raw strings for brevity"
95)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn defaults_are_ten_mib() {
101        let limits = BodyLimitsConfig::default();
102        assert_eq!(
103            limits.max_request_bytes,
104            Some(DEFAULT_MAX_BODY_BYTES),
105            "max_request_bytes should default to 10 MiB"
106        );
107        assert_eq!(
108            limits.max_response_bytes,
109            Some(DEFAULT_MAX_BODY_BYTES),
110            "max_response_bytes should default to 10 MiB"
111        );
112    }
113
114    #[test]
115    fn parse_full_config() {
116        let limits: BodyLimitsConfig = serde_yaml::from_str(
117            r#"
118max_request_bytes: 1048576
119max_response_bytes: 524288
120"#,
121        )
122        .unwrap();
123        assert_eq!(
124            limits.max_request_bytes,
125            Some(1_048_576),
126            "max_request_bytes should be parsed"
127        );
128        assert_eq!(
129            limits.max_response_bytes,
130            Some(524_288),
131            "max_response_bytes should be parsed"
132        );
133    }
134
135    #[test]
136    fn parse_empty_yields_defaults() {
137        let limits: BodyLimitsConfig = serde_yaml::from_str("{}").unwrap();
138        assert_eq!(
139            limits.max_request_bytes,
140            Some(DEFAULT_MAX_BODY_BYTES),
141            "empty YAML should use 10 MiB defaults"
142        );
143        assert_eq!(
144            limits.max_response_bytes,
145            Some(DEFAULT_MAX_BODY_BYTES),
146            "empty YAML should use 10 MiB defaults"
147        );
148    }
149
150    #[test]
151    fn parse_explicit_null_yields_none() {
152        let limits: BodyLimitsConfig = serde_yaml::from_str(
153            r#"
154max_request_bytes: null
155max_response_bytes: null
156"#,
157        )
158        .unwrap();
159        assert!(limits.max_request_bytes.is_none(), "explicit null should be None");
160        assert!(limits.max_response_bytes.is_none(), "explicit null should be None");
161    }
162}