HTTP bridge /query with multiple #h values silently narrows to the lexicographically smallest channel UUID — Workflows screen shows "No workflows yet"
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
A `POST /query` filter with multiple `#h` values returns only events from the channel whose UUID sorts first; all other listed channels are silently ignored. The desktop Workflows screen (experimental) issues exactly this shape — one filter with all member-channel ids (`getChannelsWorkflows`) — so whenever the lexicographically smallest member channel contains no workflows, the screen shows "No workflows yet" even though workflows exist and run fine.
## Reproduction (relay revision 6dbc946, self-hosted, single community)
Signed NIP-98 `POST /query` (agent key + `x-auth-tag`), body `[{"#h":[…],"kinds":[30620]}]`. Three channels: `intake` (1 workflow), `servers` (2 workflows), `opdrachten` (0 workflows). UUID order: `opdrachten` < `intake` < `servers`.
| # | `#h` values | expected | returned |
|---|---|---|---|
| A | intake, servers | 3 | 1 (intake only) |
| B | servers | 2 | 2 |
| C | servers, intake (reversed order) | 3 | 1 (intake only) |
| E | servers, opdrachten (empty) | 2 | **0** |
| G | intake, welcome | 1 | 1 (intake) |
Order-independent (A vs C) → not "first in request wins" but "smallest UUID wins", consistent with iterating a sorted `BTreeSet`.
## Root cause
`crates/buzz-relay/src/handlers/req.rs`, `extract_channel_id_from_filter` (single-filter variant, ~line 858): it returns the **first parseable value** of the `#h` tag set. `nostr::Filter::generic_tags` values are a `BTreeSet`, so "first" = lexicographically smallest. `build_event_query_from_filter` then pins the SQL to `channel_id = `.
The multi-filter variant `extract_channel_id_from_filters` (~line 1029) has the correct guard ("Multiple distinct channel IDs — fall back to global"), but the HTTP bridge path (`api/bridge.rs::query_events_authed` → `build_event_query_from_filter`) uses the single-filter variant without that guard. The bridge's own `extract_channel_from_filter` (used for the access pre-check) does require `len == 1`, so the access check and the query builder disagree about the same filter.
## Suggested fix
Give `extract_channel_id_from_filter` the same semantics as the plural variant: return `None` when the filter carries more than one distinct channel UUID. `apply_access_scope_to_query` then scopes the query to the caller's accessible channels (IN-list), and the existing per-event post-check (`filters_match` + accessible-channel check) keeps results correct.
## Related UI papercut
`WorkflowsView` renders `allWorkflowsQuery.data ?? []`, so a failed or empty query is indistinguishable from "no workflows" — an error state would have made this much easier to spot.
Contributor guide
Research direction
Start in `crates/buzz-relay/src/handlers/req.rs` by comparing `extract_channel_id_from_filter` with `extract_channel_id_from_filters`, then trace the HTTP bridge path through `api/bridge.rs::query_events_authed` and `build_event_query_from_filter`. Done means a filter with multiple distinct `#h` channel IDs no longer narrows results to the smallest UUID, while single-channel behavior and access checks remain correct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100