commit 23fb3e1ceba3fec8fadfad293ede580b1e6f91fb
parent db67342c7416e1e4b03bd20ed8f17e0cf02caa44
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 16:05:37 -0700
query: reject unsupported search filters
Diffstat:
4 files changed, 139 insertions(+), 0 deletions(-)
diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs
@@ -412,6 +412,19 @@ impl BaseRelayLimits {
}
impl BaseRelay {
+ pub(crate) fn unsupported_search_closed(
+ subscription_id: &SubscriptionId,
+ filters: &[Filter],
+ ) -> Option<RelayMessage> {
+ filters
+ .iter()
+ .any(|filter| filter.search().is_some())
+ .then(|| RelayMessage::Closed {
+ subscription_id: subscription_id.clone(),
+ message: "unsupported: search filters are not supported".to_owned(),
+ })
+ }
+
pub fn open(
config: &PocketStoreConfig,
limits: BaseRelayLimits,
@@ -770,6 +783,9 @@ impl BaseRelay {
) -> Result<BaseRelayQueryReport, BaseRelayError> {
self.limits.validate_subscription_id(&subscription_id)?;
self.limits.validate_filters(&filters)?;
+ if let Some(message) = Self::unsupported_search_closed(&subscription_id, &filters) {
+ return Ok(BaseRelayQueryReport::new(vec![message], false));
+ }
self.subscriptions
.subscribe(subscription_id.clone(), filters.clone(), auth.clone())?;
self.query_req_with_group_auth_report(subscription_id, filters, auth)
@@ -783,6 +799,9 @@ impl BaseRelay {
) -> Result<BaseRelayQueryReport, BaseRelayError> {
self.limits.validate_subscription_id(&subscription_id)?;
self.limits.validate_filters(&filters)?;
+ if let Some(message) = Self::unsupported_search_closed(&subscription_id, &filters) {
+ return Ok(BaseRelayQueryReport::new(vec![message], false));
+ }
let report = self.query_events_report(&filters, auth)?;
let group_read_denied = report.group_read_denied;
let mut messages = report
@@ -850,6 +869,9 @@ impl BaseRelay {
) -> Result<BaseRelayCountReport, BaseRelayError> {
self.limits.validate_subscription_id(&subscription_id)?;
self.limits.validate_filters(&filters)?;
+ if let Some(message) = Self::unsupported_search_closed(&subscription_id, &filters) {
+ return Ok(BaseRelayCountReport::new(message, false));
+ }
let report = self.count_events_report(&filters, auth)?;
Ok(BaseRelayCountReport::new(
RelayMessage::Count {
@@ -1145,6 +1167,38 @@ mod tests {
}
#[test]
+ fn base_relay_rejects_search_req_and_count_as_unsupported() {
+ let mut relay = test_relay("base-relay-search-unsupported", 4);
+ let req_id = SubscriptionId::new("search-req").expect("req");
+ let count_id = SubscriptionId::new("search-count").expect("count");
+ let search = filter_from_value(&serde_json::json!({
+ "search": "fresh carrots",
+ "limit": 1
+ }))
+ .expect("filter");
+
+ assert_eq!(
+ relay
+ .handle_req(req_id.clone(), vec![search.clone()])
+ .expect("req"),
+ vec![RelayMessage::Closed {
+ subscription_id: req_id,
+ message: "unsupported: search filters are not supported".to_owned()
+ }]
+ );
+ assert_eq!(relay.active_subscription_count(), 0);
+ assert_eq!(
+ relay
+ .handle_count(count_id.clone(), vec![search])
+ .expect("count"),
+ RelayMessage::Closed {
+ subscription_id: count_id,
+ message: "unsupported: search filters are not supported".to_owned()
+ }
+ );
+ }
+
+ #[test]
fn base_relay_fetches_events_by_store_offset() {
let relay = test_relay("base-relay-offset-lookup", 4);
let event = signed_public_event(7, 1, Vec::new(), "offset");
diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs
@@ -597,6 +597,14 @@ impl TangleRuntimeHandle {
.limits()
.base_relay_limits()
.validate_filters(&filters)?;
+ if let Some(message) =
+ BaseRelay::unsupported_search_closed(&subscription_id, &filters)
+ {
+ runtime
+ .metrics()
+ .record_query_latency(elapsed_micros(started_at));
+ return Ok(vec![message]);
+ }
if let Some(message) = runtime.rate_limit_req(
&subscription_id,
&filters,
@@ -635,6 +643,14 @@ impl TangleRuntimeHandle {
.limits()
.base_relay_limits()
.validate_filters(&filters)?;
+ if let Some(message) =
+ BaseRelay::unsupported_search_closed(&subscription_id, &filters)
+ {
+ runtime
+ .metrics()
+ .record_query_latency(elapsed_micros(started_at));
+ return Ok(vec![message]);
+ }
if let Some(message) = runtime.rate_limit_count(
&subscription_id,
&filters,
@@ -2536,6 +2552,57 @@ mod tests {
}
#[tokio::test]
+ async fn runtime_rejects_search_req_and_count_as_unsupported() {
+ let root = temp_root("runtime-search-unsupported");
+ let _ = std::fs::remove_dir_all(&root);
+ let handle = TangleRuntimeHandle::new(
+ TangleRuntime::open(runtime_config(&root, 8)).expect("runtime"),
+ );
+ let mut auth = handle.auth_state().await.expect("auth");
+ let req_id = SubscriptionId::new("search-req").expect("req");
+ let count_id = SubscriptionId::new("search-count").expect("count");
+ let search =
+ filter_from_value(&json!({"search": "fresh carrots", "limit": 1})).expect("filter");
+
+ assert_eq!(
+ handle
+ .handle_client_message(
+ ClientMessage::Req {
+ subscription_id: req_id.clone(),
+ filters: vec![search.clone()]
+ },
+ &mut auth,
+ UnixTimestamp::new(1_714_124_433)
+ )
+ .await
+ .expect("req"),
+ vec![RelayMessage::Closed {
+ subscription_id: req_id,
+ message: "unsupported: search filters are not supported".to_owned()
+ }]
+ );
+ assert_eq!(
+ handle
+ .handle_client_message(
+ ClientMessage::Count {
+ subscription_id: count_id.clone(),
+ filters: vec![search]
+ },
+ &mut auth,
+ UnixTimestamp::new(1_714_124_434)
+ )
+ .await
+ .expect("count"),
+ vec![RelayMessage::Closed {
+ subscription_id: count_id,
+ message: "unsupported: search filters are not supported".to_owned()
+ }]
+ );
+
+ let _ = std::fs::remove_dir_all(root);
+ }
+
+ #[tokio::test]
async fn runtime_rate_limits_count_filter_kinds() {
let root = temp_root("runtime-count-kind-rate-limit");
let _ = std::fs::remove_dir_all(&root);
diff --git a/crates/tangle_runtime/src/server.rs b/crates/tangle_runtime/src/server.rs
@@ -359,6 +359,20 @@ mod tests {
json!(["EOSE", "sub-a"])
);
+ send_client_value(
+ &mut socket,
+ json!(["REQ", "sub-search", {"search": "fresh carrots", "limit": 1}]),
+ )
+ .await;
+ assert_eq!(
+ read_relay_value(&mut socket).await,
+ json!([
+ "CLOSED",
+ "sub-search",
+ "unsupported: search filters are not supported"
+ ])
+ );
+
send_client_value(&mut socket, json!(["AUTH", event_to_value(&owner_auth)])).await;
assert_eq!(
read_relay_value(&mut socket).await,
diff --git a/crates/tangle_runtime/src/session.rs b/crates/tangle_runtime/src/session.rs
@@ -6,6 +6,7 @@ use crate::{
logging,
relay::{
auth::{BaseAuthState, generate_auth_challenge},
+ core::BaseRelay,
live::{CloseResult, LiveSubscriptionSet},
},
runtime::{
@@ -320,6 +321,9 @@ impl TangleWebSocketSession {
.base_relay_limits()
.validate_subscription_id(&subscription_id)?;
self.limits.base_relay_limits().validate_filters(&filters)?;
+ if let Some(message) = BaseRelay::unsupported_search_closed(&subscription_id, &filters) {
+ return Ok(vec![message]);
+ }
if let Some(message) = self
.runtime
.rate_limit_req(