`POST /query` returns events from only one channel when a filter has multiple `#h` values
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- postgresql, rust
- Domain
- backend-api-design, databases
Research direction
Start in req.rs at extract_channel_id_from_filter and trace its use from api/bridge.rs:1227 into the query builder; compare the sibling extractors in bridge.rs:235 and count.rs:17. Run the handlers::req::tests multi-value filter case and cargo fmt -p buzz-relay -- --check. Done means multi-value #h filters return events from every accessible channel without breaking single-value queries.
Written by the indexing model from the issue text.
Description
Describe the bug
On the HTTP bridge (POST /query), a filter carrying multiple #h values returns events
from only one channel — the lexicographically smallest. Under NIP-01 the tag list is an OR
set, so every listed channel should match. WS REQ handles the same filter correctly; only
the bridge is affected.
This silently empties the desktop Workflows overview, which sends exactly this filter
(desktop/src-tauri/src/commands/workflows.rs:87 → query_relay → POST /query). The
workflows exist and run on schedule, but the view shows "No workflows yet". NIP-45 COUNT
undercounts the same way, via the same query builder (handlers/count.rs:153, :225;
api/bridge.rs:1468, :1536).
Steps to reproduce
- Join two channels, A and B, where only B contains a
kind:30620workflow, and A's channel
UUID sorts lexicographically before B's. - Open the Workflows view.
- The list is empty.
Directly against the bridge, with a NIP-98-signed POST /query:
[{"kinds":[30620],"#h":["<chA>","<chB>"]}] // returns only chA's events
[{"kinds":[30620],"#h":["<chB>"]}] // returns chB's events
Expected behavior
The multi-value filter returns events from both channels, matching NIP-01 OR semantics and
the existing WS REQ behavior. B's workflow appears in the Workflows view.
Version and platform
- Buzz version: 0.5.2 (desktop); relay self-hosted from
ghcr.io/block/buzz, Postgres 17 - OS: macOS 26.6
Logs / additional context
Two #h extractors disagree inside the same request, at api/bridge.rs:1227:
let mut query = req::build_event_query_from_filter(filter, ...).await;
req::apply_access_scope_to_query(
&mut query,
extract_channel_from_filter(filter),
&accessible_channels,
);
build_event_query_from_filteruses the singularextract_channel_id_from_filter
(req.rs:851), which returns the first#hUUID →EventQuery.channel_id = Some(chA).apply_access_scope_to_queryis passedextract_channel_from_filter(bridge.rs:235),
which returnsNonefor a multi-value#h→ it setschannel_ids = accessiblebut does
not clearchannel_id.
Both predicates reach the SQL:
AND channel_id = chA
AND (channel_id IS NULL OR channel_id IN (<accessible>))
The filters_match post-filter (bridge.rs:1299) can't recover — chB's rows were never
fetched. Which value survives is server-side and deterministic: nostr's GenericTags is
BTreeMap<SingleLetterTag, BTreeSet<String>>, so "first" is always the lexicographically
smallest #h.
A minimal fix is to return None for a multi-value #h, matching the sibling extractors at
bridge.rs:235 and count.rs:17. Callers then treat the filter as global, widen to the
accessible set, and let filters_match enforce the OR set — what WS REQ already does:
if key == "h" {
- for val in tag_values {
- if let Ok(id) = val.parse::<uuid::Uuid>() {
- return Some(id);
- }
+ if tag_values.len() != 1 {
+ return None;
}
+ return tag_values.iter().next()?.parse::<uuid::Uuid>().ok();
}
On origin/main @ b7bb1512, a test asserting the multi-value case fails before this change
and passes after; handlers::req::tests is 50/50 with three added tests, and
cargo fmt -p buzz-relay -- --check is clean. (The 9 api::media / api::admin failures in
the full lib suite are Sqlx(PoolTimedOut) and fail identically without the patch.)
- Dominant language
- Rust
- Stars
- 33.7k
- Forks
- 4.4k
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 239
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from block/buzz
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
workflow_sink's mention parser never masks code regions — @name inside a code span wakes the agent Open
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 Half a day Newbie friendliness 88/100
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bitcoindevkit/bdk-ffi#1125 ·