From 25d174fb170929621bf478e4a7c9e1f11bc723a1 Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Thu, 20 Aug 2026 08:51:57 +0000
Subject: [PATCH] sdk(local_relay): enforce max_query_results on REQ
Previously, max_query_results was not applied when a REQ contained
multiple filters. Each filter's limit is now determined individually:
use the filter's `.ids` count or its limit, bounded by max_filter_limit,
or default_filter_limit if none is set.
If the sum of all filter limits exceeds max_query_results, the total
is split among filters. For example, with max_query_results=6 and two
filters each requesting 5 events, each filter gets 3. If the number is
not divisible (e.g., 7), the filters may get 4, slightly exceeding the
max, which is acceptable.
Signed-off-by: Awiteb
---
nostr-sdk/CHANGELOG.md | 6 ++++
nostr-sdk/src/local_relay/local/inner.rs | 43 +++++++++++++++-------
nostr-sdk/src/local_relay/mod.rs | 46 ++++++++++++++++++++++++
3 files changed, 82 insertions(+), 13 deletions(-)
diff --git a/nostr-sdk/CHANGELOG.md b/nostr-sdk/CHANGELOG.md
index dc65dd5e0..09414b8b8 100644
--- a/nostr-sdk/CHANGELOG.md
+++ b/nostr-sdk/CHANGELOG.md
@@ -27,6 +27,12 @@
-->
+## Unreleased
+
+### Changed
+
+- local_relay: improve `max_query_results` handling (https://github.com/nostrdevkit/nostr/pull/1457)
+
## v0.45.2 - 2026/08/19
### Fixed
diff --git a/nostr-sdk/src/local_relay/local/inner.rs b/nostr-sdk/src/local_relay/local/inner.rs
index b2f7544b5..eaf8ddf24 100644
--- a/nostr-sdk/src/local_relay/local/inner.rs
+++ b/nostr-sdk/src/local_relay/local/inner.rs
@@ -1316,19 +1316,8 @@ impl InnerLocalRelay {
}
}
- // Cap each query and the merged result so multiple filters cannot multiply output.
- for filter in filters.iter_mut() {
- let requested_limit = filter.limit.unwrap_or_else(|| {
- filter
- .ids
- .as_ref()
- .map_or(self.default_filter_limit, |ids| ids.len())
- });
- let per_filter_limit = self
- .max_filter_limit
- .map_or(requested_limit, |max| requested_limit.min(max));
- filter.limit = Some(per_filter_limit.min(self.max_query_results));
- }
+ // set the correct limit for the filters
+ self.set_filters_limit(&mut filters);
// Check if subscription has IDs
let ids_len: Option = filters
@@ -1462,6 +1451,34 @@ impl InnerLocalRelay {
GiftWrapQueryAccess::Allowed
}
+
+ /// Ensures that each filter's effective limit respects the configured
+ /// `max_filter_limit` and that the combined limits across all filters do
+ /// not exceed `max_query_results`.
+ fn set_filters_limit(&self, filters: &mut [Filter]) {
+ let mut total_limit: usize = 0;
+ for filter in filters.iter_mut() {
+ // Determine the effective limit for this filter:
+ // - If an ID set exists, its length; otherwise the filter's own limit.
+ // - Cap that value by `max_filter_limit`.
+ // - Fall back to `default_filter_limit` if both are absent.
+ let limit = (filter.ids.as_ref().map(BTreeSet::len).or(filter.limit))
+ .min(self.max_filter_limit)
+ .unwrap_or(self.default_filter_limit);
+
+ filter.limit = Some(limit);
+ total_limit = total_limit.saturating_add(limit);
+ }
+
+ // If the sum of all limits exceeds the maximum allowed query results,
+ // redistribute the budget equally among all filters.
+ if total_limit > self.max_query_results {
+ // TODO: Set per-filter limits instead of a global one; filters
+ // matching a single id should have a limit of 1.
+ let new_limit = self.max_query_results.div_ceil(filters.len());
+ filters.iter_mut().for_each(|f| f.limit = Some(new_limit));
+ }
+ }
}
#[inline]
diff --git a/nostr-sdk/src/local_relay/mod.rs b/nostr-sdk/src/local_relay/mod.rs
index 0cc6fb9ad..b816393ed 100644
--- a/nostr-sdk/src/local_relay/mod.rs
+++ b/nostr-sdk/src/local_relay/mod.rs
@@ -232,4 +232,50 @@ mod tests {
output.failed.values().next().unwrap()
);
}
+
+ #[tokio::test]
+ async fn test_max_query_results() {
+ let relay = LocalRelay::builder()
+ .database(MemoryDatabase::unbounded())
+ .max_query_results(6)
+ .build();
+ relay.run().await.unwrap();
+
+ let keys = Keys::generate();
+ let client = Client::default();
+
+ client
+ .add_relay(relay.url().await)
+ .and_connect()
+ .await
+ .unwrap();
+
+ for i in 0..10 {
+ client
+ .send_event(
+ &EventBuilder::new(
+ if i % 2 == 1 {
+ Kind::TextNote
+ } else {
+ Kind::Comment
+ },
+ i.to_string(),
+ )
+ .finalize(&keys)
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+ }
+
+ let result = client
+ .fetch_events([
+ Filter::new().kind(Kind::TextNote),
+ Filter::new().kind(Kind::Comment),
+ ])
+ .await
+ .unwrap();
+
+ assert_eq!(6, result.len(), "max_query_results is 6");
+ }
}