block / block/buzz

HTTP bridge /query silently scopes multi-value #h filters to a single channel (breaks desktop Workflows list)

Open
#4,659 0 comments 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

**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:

```rust
// handlers/req.rs
fn extract_channel_id_from_filter(filter: &Filter) -> Option {
for (tag_key, tag_values) in filter.generic_tags.iter() {
...
if key == "h" {
for val in tag_values {
if let Ok(id) = val.parse::() {
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**

1. 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`).
2. `POST /query` (NIP-98 auth) with body `[{"kinds":[30620],"#h":["",""]}]` → **`[]`**.
3. Same query with `[{"kinds":[30620],"#h":[""]}]` → returns the workflow.
4. Same query with `[{"kinds":[30620]}]` (no `#h`) → also returns the workflow (access-scope fallback works; only the multi-value scoping is broken).
5. 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:main` pulled 2026-08-02
- OS: macOS (desktop), Linux/Railway (relay)

**Logs / additional context**

- Reproduced with raw NIP-98 `POST /query` requests (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 delete` removes 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.