diff --git a/src/client.rs b/src/client.rs index a4656004..0fbc4ca6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1160,7 +1160,8 @@ impl Builder { /// /// Small DATA frames consume this budget. The budget is restored when /// buffered frames are consumed by the application, while sufficiently - /// large frames may also restore budget. + /// large frames may also restore budget. Empty DATA frames are limited + /// separately and do not consume this budget. /// /// When this budget is exhausted, the connection is closed with /// `ENHANCE_YOUR_CALM`. diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 36f1bda8..a9d3a9ac 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -37,6 +37,7 @@ pub const DEFAULT_LOCAL_RESET_COUNT_MAX: usize = 1024; // smaller than this consume more internal bookkeeping than useful data. pub const DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD: usize = 256; pub const DEFAULT_DATA_FRAME_BUDGET: usize = DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD * 100; +pub const MAX_RECV_EMPTY_DATA_FRAMES: usize = 100; // RFC 9113 suggests allowing at minimum 100 streams, it seems reasonable to // by default allow a portion of that to be remembered as reset for some time. pub const DEFAULT_RESET_STREAM_MAX: usize = 50; diff --git a/src/proto/streams/counts.rs b/src/proto/streams/counts.rs index 1980e73b..1cad3dfd 100644 --- a/src/proto/streams/counts.rs +++ b/src/proto/streams/counts.rs @@ -69,6 +69,10 @@ pub(super) struct Counts { /// connection-level budget for DATA framing overhead. data_frame_budget: Budget, + + /// Number of empty, non-final DATA frames received over the lifetime of + /// the connection. + num_recv_empty_data_frames: usize, } impl Counts { @@ -87,12 +91,22 @@ impl Counts { max_local_error_reset_streams: config.local_max_error_reset_streams, num_local_error_reset_streams: 0, data_frame_budget: Budget::new(config.data_frame_budget), + num_recv_empty_data_frames: 0, } } /// Records the framing overhead of a DATA frame. pub fn record_data_frame(&mut self, payload_len: usize) -> Result<(), BudgetExhausted> { - if payload_len < DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD { + if payload_len == 0 { + self.num_recv_empty_data_frames = self + .num_recv_empty_data_frames + .checked_add(1) + .ok_or(BudgetExhausted)?; + if self.num_recv_empty_data_frames > MAX_RECV_EMPTY_DATA_FRAMES { + return Err(BudgetExhausted); + } + Ok(()) + } else if payload_len < DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD { self.data_frame_budget .consume(DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD - payload_len) } else { @@ -105,7 +119,7 @@ impl Counts { /// Releases the framing overhead of a DATA frame that is no longer /// buffered internally. pub fn release_data_frame(&mut self, payload_len: usize) { - if payload_len < DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD { + if payload_len != 0 && payload_len < DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD { self.data_frame_budget .replenish(DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD - payload_len); } @@ -399,4 +413,32 @@ mod tests { counts.release_data_frame(1); } } + + #[test] + fn empty_data_frames_do_not_consume_data_frame_budget() { + let mut counts = counts(); + counts.data_frame_budget = Budget::new(0); + + for _ in 0..MAX_RECV_EMPTY_DATA_FRAMES { + counts.record_data_frame(0).unwrap(); + } + + // Empty frames have their own limit, while a non-empty small frame + // still consumes the independently configured DATA frame budget. + assert!(counts.record_data_frame(0).is_err()); + assert!(counts.record_data_frame(1).is_err()); + } + + #[test] + fn large_data_frames_do_not_replenish_empty_data_frame_limit() { + let mut counts = counts(); + + for _ in 0..MAX_RECV_EMPTY_DATA_FRAMES { + counts.record_data_frame(0).unwrap(); + counts + .record_data_frame(DEFAULT_DATA_FRAME_OVERHEAD_THRESHOLD * 2) + .unwrap(); + } + assert!(counts.record_data_frame(0).is_err()); + } } diff --git a/src/server.rs b/src/server.rs index c6999758..d26dcf4b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1054,7 +1054,8 @@ impl Builder { /// /// Small DATA frames consume this budget. The budget is restored when /// buffered frames are consumed by the application, while sufficiently - /// large frames may also restore budget. + /// large frames may also restore budget. Empty DATA frames are limited + /// separately and do not consume this budget. /// /// When this budget is exhausted, the connection is closed with /// `ENHANCE_YOUR_CALM`.