HTTP bridge /query silently scopes multi-value #h filters to a single channel (breaks desktop Workflows list)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 85/100
Research direction
Start in api/bridge.rs at query_events and compare its query construction with the arity-guarded helper in handlers/req.rs. Reproduce POST /query using a filter with two #h channel UUIDs, then verify that results from either channel are returned while single-value and untagged queries retain their current behavior.
Written by the indexing model from the issue text.
Description
Describe the bug
The HTTP bridge POST /query returns wrong (empty) results for any filter with a multi-value #h tag. Instead of matching events in ANY of the listed channels (NIP-01 OR semantics), the query gets silently scoped to whichever channel UUID happens to iterate first out of the tag-value set — all other channels are ignored.
Root cause: query_events in api/bridge.rs builds its catch-all DB query via handlers::req::build_event_query_from_filter, which calls extract_channel_id_from_filter (handlers/req.rs). That helper returns the first parseable UUID from the #h values with no arity check:
// handlers/req.rs
fn extract_channel_id_from_filter(filter: &Filter) -> Option<uuid::Uuid> {
for (tag_key, tag_values) in filter.generic_tags.iter() {
...
if key == "h" {
for val in tag_values {
if let Ok(id) = val.parse::<uuid::Uuid>() {
return Some(id); // <-- multi-#h collapses to one channel
}
}
}
}
...
}
bridge.rs has its own extract_channel_from_filter with the correct vs.len() == 1 guard, and the WS REQ path guards arity too (per_filter_channel in handle_req, and filter_fully_pushable explicitly documents multi-#h as not pushable) — but the bridge's query construction goes through the unguarded helper, so the SQL WHERE channel_id = $x predicate pins the query to one channel before the (correct) filters_match post-filter ever sees the rows.
Since generic_tags values are an ordered set, "first" is effectively the lexicographically smallest UUID, so which channel wins is data-dependent and surprising.
User-visible impact: the desktop Workflows screen fetches workflows for all member channels in one batched query (get_channels_workflows, single filter with "#h": [all member channel ids]). For any user in more than one channel whose lexicographically-first channel has no workflows, the screen shows "No workflows yet" even though workflows exist and run. Workflows created from the dialog appear to vanish (create succeeds, list never shows them, and they can't be edited/deleted from the UI).
Steps to reproduce
- On a relay with channels A and B (UUIDs such that A sorts before B), create a workflow in channel B (via desktop dialog or
buzz workflows create). POST /query(NIP-98 auth) with body[{"kinds":[30620],"#h":["<A>","<B>"]}]→[].- Same query with
[{"kinds":[30620],"#h":["<B>"]}]→ returns the workflow. - Same query with
[{"kinds":[30620]}](no#h) → also returns the workflow (access-scope fallback works; only the multi-value scoping is broken). - In the desktop app, be a member of ≥2 channels and open the Workflows screen → "No workflows yet".
Verified against a self-hosted relay running ghcr.io/block/buzz:main (2026-08-02); the unguarded helper is unchanged on current main.
Expected behavior
A multi-value #h filter matches events in any of the listed channels, like the WS REQ path: either push the full value set into SQL (e.g. channel_id = ANY($ids), intersected with the caller's accessible channels), or leave channel_id unset when len > 1 and let the existing access scope + filters_match post-filter handle it. Reusing the arity-guarded extract_channel_from_filter already in bridge.rs for query construction would fix the wrong-scoping half in one line.
Version and platform
- Buzz version: desktop 0.5.4; relay image
ghcr.io/block/buzz:mainpulled 2026-08-02 - OS: macOS (desktop), Linux/Railway (relay)
Logs / additional context
- Reproduced with raw NIP-98
POST /queryrequests (steps 2–4 above) — happy to share the exact request/response pairs. - Related but separate quirks noticed while debugging (can file separately if useful):
workflows deleteremoves the workflow from the scheduler but the kind:30620 event remains visible in list/get; and the create dialog surfaces no feedback when the list can't show the result, so users click Create repeatedly and accumulate duplicates.
- 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
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
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