block / block/buzz

HTTP bridge /query with multiple #h values silently narrows to the lexicographically smallest channel UUID — Workflows screen shows "No workflows yet"

Open
#5,053 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.