commit 95bcf4cc2a0aec19fc323620d71d8c517f87e2f8
parent 5d069ecec1e4045be2a51cf8ed27b31552384b34
Author: triesap <tyson@radroots.org>
Date: Wed, 1 Jul 2026 22:42:13 +0000
relay_transport: reject zero fetch limits
Diffstat:
3 files changed, 68 insertions(+), 6 deletions(-)
diff --git a/crates/relay_transport/src/error.rs b/crates/relay_transport/src/error.rs
@@ -38,6 +38,9 @@ pub enum RadrootsRelayTransportError {
#[error("Relay fetch filters must not be empty")]
EmptyFetchFilters,
+ #[error("Relay fetch {field} must be greater than zero")]
+ InvalidFetchLimit { field: &'static str },
+
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
diff --git a/crates/relay_transport/src/fetch.rs b/crates/relay_transport/src/fetch.rs
@@ -104,6 +104,7 @@ impl RadrootsRelayFetchRequest {
where
I: IntoIterator<Item = RadrootsNostrFilter>,
{
+ ensure_positive_limit("max_events", max_events)?;
Ok(Self {
mode,
observed_at_ms,
@@ -124,14 +125,19 @@ impl RadrootsRelayFetchRequest {
self
}
- pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
+ pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Result<Self, RadrootsRelayTransportError> {
+ ensure_positive_timeout("timeout_ms", timeout_ms)?;
self.timeout_ms = timeout_ms;
- self
+ Ok(self)
}
- pub fn with_raw_event_scan_limit(mut self, max_raw_events: usize) -> Self {
+ pub fn with_raw_event_scan_limit(
+ mut self,
+ max_raw_events: usize,
+ ) -> Result<Self, RadrootsRelayTransportError> {
+ ensure_positive_limit("max_raw_events", max_raw_events)?;
self.max_raw_events = max_raw_events;
- self
+ Ok(self)
}
pub fn mode(&self) -> RadrootsRelayFetchMode {
@@ -167,7 +173,26 @@ fn default_raw_event_scan_limit(max_events: usize) -> usize {
max_events
.saturating_mul(DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER)
.max(max_events)
- .max(1)
+}
+
+fn ensure_positive_limit(
+ field: &'static str,
+ value: usize,
+) -> Result<(), RadrootsRelayTransportError> {
+ if value == 0 {
+ return Err(RadrootsRelayTransportError::InvalidFetchLimit { field });
+ }
+ Ok(())
+}
+
+fn ensure_positive_timeout(
+ field: &'static str,
+ value: u64,
+) -> Result<(), RadrootsRelayTransportError> {
+ if value == 0 {
+ return Err(RadrootsRelayTransportError::InvalidFetchLimit { field });
+ }
+ Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
diff --git a/crates/relay_transport/tests/transport.rs b/crates/relay_transport/tests/transport.rs
@@ -488,6 +488,38 @@ fn fetch_requests_reject_empty_filter_sets() {
));
}
+#[test]
+fn fetch_requests_reject_zero_limits_and_timeouts() {
+ let filter = post_relay_fetch_filter(1);
+ assert!(matches!(
+ RadrootsRelayFetchRequest::fetch(1_000, 0, [filter.clone()]),
+ Err(RadrootsRelayTransportError::InvalidFetchLimit { field }) if field == "max_events"
+ ));
+ assert!(matches!(
+ RadrootsRelayFetchRequest::subscription(1_000, 0, [filter.clone()]),
+ Err(RadrootsRelayTransportError::InvalidFetchLimit { field }) if field == "max_events"
+ ));
+
+ let request =
+ RadrootsRelayFetchRequest::fetch(1_000, 1, [filter]).expect("valid fetch request");
+ assert!(matches!(
+ request.clone().with_timeout_ms(0),
+ Err(RadrootsRelayTransportError::InvalidFetchLimit { field }) if field == "timeout_ms"
+ ));
+ assert!(matches!(
+ request.clone().with_raw_event_scan_limit(0),
+ Err(RadrootsRelayTransportError::InvalidFetchLimit { field }) if field == "max_raw_events"
+ ));
+
+ let request = request
+ .with_timeout_ms(1)
+ .expect("minimum timeout")
+ .with_raw_event_scan_limit(1)
+ .expect("minimum raw scan limit");
+ assert_eq!(request.timeout_ms(), 1);
+ assert_eq!(request.max_raw_events(), 1);
+}
+
#[tokio::test]
async fn fetch_ingests_events_and_records_relay_observations() {
let signed = signed_post("hello");
@@ -896,7 +928,9 @@ async fn fetch_raw_scan_limit_bounds_noisy_adapter_output() {
let receipt = fetch_and_ingest_relay_events(
&adapter,
&store,
- post_relay_fetch_request(1_130, 1).with_raw_event_scan_limit(2),
+ post_relay_fetch_request(1_130, 1)
+ .with_raw_event_scan_limit(2)
+ .expect("raw scan limit"),
)
.await
.expect("fetch ingest");