Skip to main content

eggserve_core/
limits.rs

1//! Resource limits for connections, streams, and request sizes.
2
3use std::fmt;
4use std::time::Duration;
5
6/// Default maximum number of entries to enumerate in a directory listing.
7pub const DEFAULT_MAX_LISTING_ENTRIES: usize = 4096;
8pub const MAX_LISTING_RESPONSE_BYTES: usize = 10 * 1024 * 1024;
9
10/// Error returned when a [`Limits`] field violates its constraint.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct LimitsError {
13    /// The field that failed validation.
14    pub field: &'static str,
15    /// The rejected value.
16    pub value: String,
17    /// Human-readable constraint description.
18    pub constraint: String,
19}
20
21impl fmt::Display for LimitsError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(
24            f,
25            "{} must be {}: got {}",
26            self.field, self.constraint, self.value
27        )
28    }
29}
30
31impl std::error::Error for LimitsError {}
32
33#[derive(Debug, Clone)]
34#[must_use]
35pub struct Limits {
36    pub max_connections: usize,
37    pub max_file_streams: usize,
38    pub(crate) max_request_body_bytes: u64,
39    pub header_read_timeout: Duration,
40    pub connection_total_timeout: Duration,
41    /// Timeout for a single handler invocation. Default: 30s.
42    pub handler_timeout: Duration,
43    /// Timeout for reading the request body (total deadline, not idle).
44    /// Default: 30s.
45    pub body_read_timeout: Duration,
46    pub graceful_shutdown_timeout: Duration,
47    /// Maximum number of entries to enumerate in a directory listing.
48    pub max_listing_entries: usize,
49    /// Maximum size in bytes for a directory listing response body.
50    pub max_listing_response_bytes: usize,
51    /// Chunk size in bytes for file streaming reads.
52    pub stream_chunk_size: usize,
53}
54
55impl Default for Limits {
56    fn default() -> Self {
57        Self {
58            max_connections: 64,
59            max_file_streams: 32,
60            max_request_body_bytes: 0,
61            header_read_timeout: Duration::from_secs(10),
62            connection_total_timeout: Duration::from_secs(60),
63            handler_timeout: Duration::from_secs(30),
64            body_read_timeout: Duration::from_secs(30),
65            graceful_shutdown_timeout: Duration::from_secs(10),
66            max_listing_entries: DEFAULT_MAX_LISTING_ENTRIES,
67            max_listing_response_bytes: 1024 * 1024, // 1 MiB
68            stream_chunk_size: 8192,
69        }
70    }
71}
72
73impl Limits {
74    /// Validate all fields and return every constraint violation.
75    ///
76    /// Returns `Ok(())` if all fields satisfy their invariants. Returns `Err`
77    /// with one [`LimitsError`] per violated field.
78    pub fn validate(&self) -> Result<(), Vec<LimitsError>> {
79        let mut errors = Vec::new();
80        let max_semaphore_permits = tokio::sync::Semaphore::MAX_PERMITS;
81        if self.max_connections == 0 {
82            errors.push(LimitsError {
83                field: "max_connections",
84                value: "0".into(),
85                constraint: "> 0".into(),
86            });
87        } else if self.max_connections > max_semaphore_permits {
88            errors.push(LimitsError {
89                field: "max_connections",
90                value: self.max_connections.to_string(),
91                constraint: format!("<= {} (Semaphore::MAX_PERMITS)", max_semaphore_permits),
92            });
93        }
94        if self.max_file_streams == 0 {
95            errors.push(LimitsError {
96                field: "max_file_streams",
97                value: "0".into(),
98                constraint: "> 0".into(),
99            });
100        } else if self.max_file_streams > max_semaphore_permits {
101            errors.push(LimitsError {
102                field: "max_file_streams",
103                value: self.max_file_streams.to_string(),
104                constraint: format!("<= {} (Semaphore::MAX_PERMITS)", max_semaphore_permits),
105            });
106        }
107        if self.header_read_timeout.is_zero() {
108            errors.push(LimitsError {
109                field: "header_read_timeout",
110                value: "0s".into(),
111                constraint: "> 0".into(),
112            });
113        }
114        if self.connection_total_timeout.is_zero() {
115            errors.push(LimitsError {
116                field: "connection_total_timeout",
117                value: "0s".into(),
118                constraint: "> 0".into(),
119            });
120        }
121        if self.header_read_timeout > self.connection_total_timeout {
122            errors.push(LimitsError {
123                field: "header_read_timeout",
124                value: format!("{}s", self.header_read_timeout.as_secs()),
125                constraint: "<= connection_total_timeout".into(),
126            });
127        }
128        if self.handler_timeout.is_zero() {
129            errors.push(LimitsError {
130                field: "handler_timeout",
131                value: "0s".into(),
132                constraint: "> 0".into(),
133            });
134        }
135        if self.body_read_timeout.is_zero() {
136            errors.push(LimitsError {
137                field: "body_read_timeout",
138                value: "0s".into(),
139                constraint: "> 0".into(),
140            });
141        }
142        if self.graceful_shutdown_timeout.is_zero() {
143            errors.push(LimitsError {
144                field: "graceful_shutdown_timeout",
145                value: "0s".into(),
146                constraint: "> 0".into(),
147            });
148        }
149        if self.stream_chunk_size < 64 {
150            errors.push(LimitsError {
151                field: "stream_chunk_size",
152                value: self.stream_chunk_size.to_string(),
153                constraint: ">= 64".into(),
154            });
155        }
156        if self.stream_chunk_size > 1024 * 1024 {
157            errors.push(LimitsError {
158                field: "stream_chunk_size",
159                value: self.stream_chunk_size.to_string(),
160                constraint: "<= 1048576 (1 MiB)".into(),
161            });
162        }
163        if self.max_listing_response_bytes == 0 {
164            errors.push(LimitsError {
165                field: "max_listing_response_bytes",
166                value: "0".into(),
167                constraint: "> 0".into(),
168            });
169        } else if self.max_listing_response_bytes > MAX_LISTING_RESPONSE_BYTES {
170            errors.push(LimitsError {
171                field: "max_listing_response_bytes",
172                value: self.max_listing_response_bytes.to_string(),
173                constraint: format!("<= {} (10 MiB)", MAX_LISTING_RESPONSE_BYTES),
174            });
175        }
176        if errors.is_empty() {
177            Ok(())
178        } else {
179            Err(errors)
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn default_limits_are_valid() {
190        let limits = Limits::default();
191        assert!(limits.validate().is_ok());
192    }
193
194    #[test]
195    fn zero_max_connections_is_invalid() {
196        let limits = Limits {
197            max_connections: 0,
198            ..Default::default()
199        };
200        let errs = limits.validate().unwrap_err();
201        assert!(errs.iter().any(|e| e.field == "max_connections"));
202    }
203
204    #[test]
205    fn zero_max_file_streams_is_invalid() {
206        let limits = Limits {
207            max_file_streams: 0,
208            ..Default::default()
209        };
210        let errs = limits.validate().unwrap_err();
211        assert!(errs.iter().any(|e| e.field == "max_file_streams"));
212    }
213
214    #[test]
215    fn zero_header_read_timeout_is_invalid() {
216        let limits = Limits {
217            header_read_timeout: Duration::ZERO,
218            ..Default::default()
219        };
220        let errs = limits.validate().unwrap_err();
221        assert!(errs.iter().any(|e| e.field == "header_read_timeout"));
222    }
223
224    #[test]
225    fn zero_connection_total_timeout_is_invalid() {
226        let limits = Limits {
227            connection_total_timeout: Duration::ZERO,
228            ..Default::default()
229        };
230        let errs = limits.validate().unwrap_err();
231        assert!(errs.iter().any(|e| e.field == "connection_total_timeout"));
232    }
233
234    #[test]
235    fn header_timeout_cannot_exceed_connection_timeout() {
236        let limits = Limits {
237            header_read_timeout: Duration::from_secs(2),
238            connection_total_timeout: Duration::from_secs(1),
239            ..Default::default()
240        };
241        let errs = limits.validate().unwrap_err();
242        assert!(errs.iter().any(|e| e.field == "header_read_timeout"));
243    }
244
245    #[test]
246    fn zero_handler_timeout_is_invalid() {
247        let limits = Limits {
248            handler_timeout: Duration::ZERO,
249            ..Default::default()
250        };
251        let errs = limits.validate().unwrap_err();
252        assert!(errs.iter().any(|e| e.field == "handler_timeout"));
253    }
254
255    #[test]
256    fn zero_body_read_timeout_is_invalid() {
257        let limits = Limits {
258            body_read_timeout: Duration::ZERO,
259            ..Default::default()
260        };
261        let errs = limits.validate().unwrap_err();
262        assert!(errs.iter().any(|e| e.field == "body_read_timeout"));
263    }
264
265    #[test]
266    fn zero_graceful_shutdown_timeout_is_invalid() {
267        let limits = Limits {
268            graceful_shutdown_timeout: Duration::ZERO,
269            ..Default::default()
270        };
271        let errs = limits.validate().unwrap_err();
272        assert!(errs.iter().any(|e| e.field == "graceful_shutdown_timeout"));
273    }
274
275    #[test]
276    fn multiple_errors_reported() {
277        let limits = Limits {
278            max_connections: 0,
279            max_file_streams: 0,
280            handler_timeout: Duration::ZERO,
281            ..Default::default()
282        };
283        let errs = limits.validate().unwrap_err();
284        assert_eq!(errs.len(), 3);
285    }
286
287    #[test]
288    fn non_default_valid_values() {
289        let limits = Limits {
290            max_connections: 1,
291            max_file_streams: 1,
292            header_read_timeout: Duration::from_millis(1),
293            connection_total_timeout: Duration::from_millis(1),
294            handler_timeout: Duration::from_millis(1),
295            body_read_timeout: Duration::from_millis(1),
296            graceful_shutdown_timeout: Duration::from_millis(1),
297            ..Default::default()
298        };
299        assert!(limits.validate().is_ok());
300    }
301
302    #[test]
303    fn limits_error_display() {
304        let err = LimitsError {
305            field: "max_connections",
306            value: "0".into(),
307            constraint: "> 0".into(),
308        };
309        let msg = err.to_string();
310        assert!(msg.contains("max_connections"));
311        assert!(msg.contains("> 0"));
312        assert!(msg.contains("0"));
313    }
314
315    #[test]
316    fn large_concurrency_values_are_valid() {
317        let max = tokio::sync::Semaphore::MAX_PERMITS;
318        let limits = Limits {
319            max_connections: max,
320            max_file_streams: max,
321            ..Default::default()
322        };
323        assert!(limits.validate().is_ok());
324    }
325
326    #[test]
327    fn exceeding_semaphore_max_permits_is_invalid() {
328        let limits = Limits {
329            max_connections: tokio::sync::Semaphore::MAX_PERMITS + 1,
330            ..Default::default()
331        };
332        let errs = limits.validate().unwrap_err();
333        assert!(errs.iter().any(|e| e.field == "max_connections"));
334    }
335
336    #[test]
337    fn usizemax_concurrency_is_invalid() {
338        let limits = Limits {
339            max_connections: usize::MAX,
340            max_file_streams: usize::MAX,
341            ..Default::default()
342        };
343        let errs = limits.validate().unwrap_err();
344        assert!(errs.iter().any(|e| e.field == "max_connections"));
345        assert!(errs.iter().any(|e| e.field == "max_file_streams"));
346    }
347
348    #[test]
349    fn large_duration_values_are_valid() {
350        let limits = Limits {
351            header_read_timeout: Duration::from_secs(u64::MAX),
352            connection_total_timeout: Duration::from_secs(u64::MAX),
353            handler_timeout: Duration::from_secs(u64::MAX),
354            body_read_timeout: Duration::from_secs(u64::MAX),
355            graceful_shutdown_timeout: Duration::from_secs(u64::MAX),
356            ..Default::default()
357        };
358        assert!(limits.validate().is_ok());
359    }
360
361    #[test]
362    fn limits_error_is_send_and_sync() {
363        fn assert_send_sync<T: Send + Sync>() {}
364        assert_send_sync::<LimitsError>();
365    }
366
367    #[test]
368    fn limits_is_clone() {
369        let limits = Limits::default();
370        let cloned = limits.clone();
371        assert_eq!(limits.max_connections, cloned.max_connections);
372    }
373
374    #[test]
375    fn zero_stream_chunk_size_is_invalid() {
376        let limits = Limits {
377            stream_chunk_size: 0,
378            ..Default::default()
379        };
380        let errs = limits.validate().unwrap_err();
381        assert!(errs.iter().any(|e| e.field == "stream_chunk_size"));
382    }
383
384    #[test]
385    fn small_stream_chunk_size_below_minimum_is_invalid() {
386        let limits = Limits {
387            stream_chunk_size: 63,
388            ..Default::default()
389        };
390        let errs = limits.validate().unwrap_err();
391        assert!(errs.iter().any(|e| e.field == "stream_chunk_size"));
392    }
393
394    #[test]
395    fn minimum_stream_chunk_size_is_valid() {
396        let limits = Limits {
397            stream_chunk_size: 64,
398            ..Default::default()
399        };
400        assert!(limits.validate().is_ok());
401    }
402
403    #[test]
404    fn excessive_stream_chunk_size_is_invalid() {
405        let limits = Limits {
406            stream_chunk_size: 1024 * 1024 + 1,
407            ..Default::default()
408        };
409        let errs = limits.validate().unwrap_err();
410        assert!(errs.iter().any(|e| e.field == "stream_chunk_size"));
411    }
412
413    #[test]
414    fn listing_response_limit_is_bounded() {
415        let limits = Limits {
416            max_listing_response_bytes: MAX_LISTING_RESPONSE_BYTES + 1,
417            ..Default::default()
418        };
419        let errs = limits.validate().unwrap_err();
420        assert!(errs.iter().any(|e| e.field == "max_listing_response_bytes"));
421    }
422
423    #[test]
424    fn maximum_stream_chunk_size_is_valid() {
425        let limits = Limits {
426            stream_chunk_size: 1024 * 1024,
427            ..Default::default()
428        };
429        assert!(limits.validate().is_ok());
430    }
431
432    #[test]
433    fn validate_all_fields_simultaneously() {
434        let limits = Limits {
435            max_connections: 0,
436            max_file_streams: 0,
437            header_read_timeout: Duration::ZERO,
438            connection_total_timeout: Duration::ZERO,
439            handler_timeout: Duration::ZERO,
440            body_read_timeout: Duration::ZERO,
441            graceful_shutdown_timeout: Duration::ZERO,
442            stream_chunk_size: 0,
443            ..Default::default()
444        };
445        let errs = limits.validate().unwrap_err();
446        assert_eq!(errs.len(), 8);
447        let fields: Vec<&str> = errs.iter().map(|e| e.field).collect();
448        assert!(fields.contains(&"max_connections"));
449        assert!(fields.contains(&"max_file_streams"));
450        assert!(fields.contains(&"header_read_timeout"));
451        assert!(fields.contains(&"connection_total_timeout"));
452        assert!(fields.contains(&"handler_timeout"));
453        assert!(fields.contains(&"body_read_timeout"));
454        assert!(fields.contains(&"graceful_shutdown_timeout"));
455        assert!(fields.contains(&"stream_chunk_size"));
456    }
457}