block / block/buzz

`POST /query` returns events from only one channel when a filter has multiple `#h` values

Open Beginner friendly
#4,381 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**

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

1. Join two channels, A and B, where only B contains a `kind:30620` workflow, and A's channel
UUID sorts lexicographically before B's.
2. Open the Workflows view.
3. The list is empty.

Directly against the bridge, with a NIP-98-signed `POST /query`:

```jsonc
[{"kinds":[30620],"#h":["",""]}] // returns only chA's events
[{"kinds":[30620],"#h":[""]}] // 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`:

```rust
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_filter` uses the singular `extract_channel_id_from_filter`
(`req.rs:851`), which returns the **first** `#h` UUID → `EventQuery.channel_id = Some(chA)`.
- `apply_access_scope_to_query` is passed `extract_channel_from_filter` (`bridge.rs:235`),
which returns `None` for a multi-value `#h` → it sets `channel_ids = accessible` but does
not clear `channel_id`.

Both predicates reach the SQL:

```sql
AND channel_id = chA
AND (channel_id IS NULL OR channel_id IN ())
```

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>`, 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:

```rust
if key == "h" {
- for val in tag_values {
- if let Ok(id) = val.parse::() {
- return Some(id);
- }
+ if tag_values.len() != 1 {
+ return None;
}
+ return tag_values.iter().next()?.parse::().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.)

Contributor guide

Open the contributing guide

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.

Assessment

Tech stack
postgresql, rust
Domain
backend-api-design, databases
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.