Skip to content

Filters key off queryId, which is neither guaranteed present nor unique #62

Description

@roborourke

Scoping a filter to its loop relies entirely on the query block's queryId attribute: the filter blocks write query-{queryId}-{key} into the URL, filter_query_loop_block_query_vars() copies queryId into the query as query_id, and pre_get_posts_transpose_query_vars() matches the parameter prefix against it.

That attribute is neither guaranteed to exist nor guaranteed to be unique, and both edges fail badly.

1. No queryId at all — the filter silently does nothing

A query block whose markup carries a query object but no queryId:

<!-- wp:query {"query":{"perPage":10,"postType":"post","order":"asc","orderBy":"title","inherit":false}} -->

The filter renders happily and writes ?query-0-category=alpha (from $block->context['queryId'] ?? 0). But filter_query_loop_block_query_vars() only sets query_id when isset( $block->context['queryId'] ), so the loop's query has none, pre_get_posts_transpose_query_vars() returns early, and nothing is filtered.

Verified on WordPress 6.8.8 and 7.0.4: ?query-0-category=alpha and ?query-category=alpha both return the full, unfiltered set. The control looks like it works — the selection sticks, the URL changes, the results don't.

2. Two loops sharing a queryId — one control filters both

Two query blocks with the same queryId (7 in both), a taxonomy filter in the first only:

URL Loop 1 Loop 2
/repro-dup/ all 4 posts all 4 posts
/repro-dup/?query-7-category=alpha Alpha One, Alpha Two Alpha One, Alpha Two

The second loop has no filter block, and is narrowed anyway.

This matters because duplicating a query loop block in the editor copies queryId along with everything else — see WordPress/gutenberg#55823 ("Duplicating Query Block can result in duplicate Query IDs", still open) and the closed WordPress/gutenberg#56902 ("Same queryId for all Query Loops"). Core's own enhanced pagination has the same problem, and the proposals upstream (a isUnique / non-copyable attribute property, or deriving the id from an instance id at render) have not landed.

That also looks like the mechanism behind #28, where filtering applied to every loop on the page: two loops built by duplicating one share an id, so one prefix addresses both. Filtering is correctly scoped between loops with distinct ids, which is why that report did not reproduce from hand-written fixtures.

Worth deciding

Whether the plugin should defend itself here, and how much:

  • Render nothing (or an editor warning) when a filter block sits in a loop with no queryId, rather than a control that cannot work.
  • Detect two loops sharing an id in one render pass and warn, since the plugin cannot honour both.
  • Or derive the scope from something the plugin controls rather than trusting the authored attribute — which needs the filter block and the loop to agree on it at render time, so it is not a small change.

The first is small and removes a silent failure. The rest deserves a design decision before anyone writes code.

Environment

Reproduced on WordPress 6.8.8 and 7.0.4, PHP 8.4, Twenty Twenty-Five, plugin at main (d8468ea).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions