Skip to content

Fix routing for empty binding key to topic exchange#16271

Merged
ansd merged 5 commits intomainfrom
allow-empty-topic-filter
May 5, 2026
Merged

Fix routing for empty binding key to topic exchange#16271
ansd merged 5 commits intomainfrom
allow-empty-topic-filter

Conversation

@ansd
Copy link
Copy Markdown
Member

@ansd ansd commented Apr 29, 2026

Fixes #16221

What?

Fix a bug in the Khepri v4 topic trie projection where messages published with an empty routing key to a topic exchange were incorrectly routed to queues bound to other topic exchanges with an empty binding key.

Why?

The MQTT 5.0 spec states:

All Topic Names and Topic Filters MUST be at least one character long

In contrast, the AMQP 0.9.1 spec state:

The routing key used for a topic exchange MUST consist of zero or more words
delimited by dots. Each word may contain the letters A-Z and a-z and digits 0-9.
The routing pattern follows the same rules as the routing key with the addition
that * matches a single word, and # matches zero or more words

Hence zero words, i.e. empty routing keys and empty binding keys, are expliclity allowed for the topic exchange type.

In the Khepri v4 projection, the topic trie used a global root atom as the initial node ID for all topic exchanges. When a binding was created with an empty binding key (<<>>), split_topic_key_binary/1 returned an empty list ([]). The trie traversal stopped immediately at the root, meaning the binding was inserted into the rabbit_khepri_topic_binding_v4 ETS table with the global root atom as the LeafNodeId.

Because the LeafNodeId lacked any exchange-specific context, all empty bindings across all topic exchanges were attached to the exact same global root node. Consequently, when a message was published with an empty routing key to any topic exchange, the routing logic (trie_bindings/3) would scan the ETS table starting at the global root node and incorrectly match bindings belonging to completely unrelated topic exchanges.

How?

To fix this and ensure exchange isolation, the conceptual root of the trie was changed from the global root atom to an exchange-specific tuple: {root, XSrc} (where XSrc is {VHost, ExchangeName}).

  • rabbit_khepri:trie_follow_down_create/3 and trie_follow_down_get_path/3 now initialize their trie traversal with {root, XSrc}.
  • rabbit_db_topic_exchange:trie_match/5 now initiates routing from {root, {VHost, XName}}.

This structural change guarantees that empty bindings are isolated per exchange in the bindings ETS table, completely resolving the cross-exchange leakage while fully supporting AMQP 0-9-1's allowance for empty binding keys without breaking backward compatibility.

TODOs

  • New Khepri projection version (see 2nd commit)
  • Release notes

@ansd ansd force-pushed the allow-empty-topic-filter branch 2 times, most recently from b1fbae8 to f9e5fd7 Compare April 30, 2026 11:08
@ansd ansd marked this pull request as ready for review April 30, 2026 13:23
@ansd ansd added this to the 4.3.1 milestone Apr 30, 2026
@ansd ansd requested a review from Copilot April 30, 2026 13:32
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect topic exchange routing for empty routing keys/binding keys in the Khepri topic-trie projection by introducing an exchange-scoped trie root (v5 projection) to prevent cross-exchange binding leakage.

Changes:

  • Add a new Khepri topic binding projection v5 with exchange-specific root node IDs and a new topic_binding_projection_v5 feature flag.
  • Update topic routing (rabbit_db_topic_exchange) to select per-version ETS table names and start traversal from the version-appropriate root node.
  • Update/add tests to enable v5 and cover “zero words” (empty) topic routing/binding keys.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
deps/rabbit/src/rabbit_khepri.erl Adds v5 projection support (table naming, root scoping), new feature-flag callbacks, and bumps supported trie version.
deps/rabbit/src/rabbit_db_topic_exchange.erl Routes via versioned trie/binding ETS tables and uses an exchange-specific root for v5+.
deps/rabbit/src/rabbit_core_ff.erl Introduces topic_binding_projection_v5 feature flag (depends on v4) with enable/post-enable callbacks.
deps/rabbit/test/rabbit_db_topic_exchange_SUITE.erl Switches test suite to v5 ETS tables and enables the v5 feature flag in setup.
deps/rabbit/test/bindings_SUITE.erl Adds regression test to validate empty routing key/binding key behavior across multiple topic exchanges.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread deps/rabbit/src/rabbit_khepri.erl
ansd added 2 commits April 30, 2026 15:41
Fixes #16221

 ## What?

Fix a bug in the Khepri v4 topic trie projection where
messages published with an empty routing key to a topic exchange
were incorrectly routed to queues bound to other topic exchanges
with an empty binding key.

 ## Why?

The MQTT 5.0 spec states:
> All Topic Names and Topic Filters MUST be at least one character long

In contrast, the AMQP 0.9.1 spec state:
> The routing key used for a topic exchange MUST consist of **zero** or more words
  delimited by dots. Each word may contain the letters A-Z and a-z and digits 0-9.
  The routing pattern follows the same rules as the routing key with the addition
  that * matches a single word, and # matches zero or more words

Hence zero words, i.e. empty routing keys and empty binding keys, are expliclity
allowed for the topic exchange type.

In the Khepri v4 projection, the topic trie used a global `root` atom
as the initial node ID for all topic exchanges. When a binding was
created with an empty binding key (`<<>>`), `split_topic_key_binary/1`
returned an empty list (`[]`). The trie traversal stopped immediately
at the root, meaning the binding was inserted into the
`rabbit_khepri_topic_binding_v4` ETS table with the global `root` atom
as the `LeafNodeId`.

Because the `LeafNodeId` lacked any exchange-specific context, all
empty bindings across all topic exchanges were
attached to the exact same global `root` node. Consequently, when a
message was published with an empty routing key to any topic exchange,
the routing logic (`trie_bindings/3`) would scan the ETS table starting
at the global `root` node and incorrectly match bindings belonging to
completely unrelated topic exchanges.

 ## How?

To fix this and ensure exchange isolation, the conceptual root of the
trie was changed from the global `root` atom to an exchange-specific
tuple: `{root, XSrc}` (where `XSrc` is `{VHost, ExchangeName}`).
- `rabbit_khepri:trie_follow_down_create/3` and `trie_follow_down_get_path/3`
  now initialize their trie traversal with `{root, XSrc}`.
- `rabbit_db_topic_exchange:trie_match/5` now initiates routing from
  `{root, {VHost, XName}}`.

This structural change guarantees that empty bindings are isolated per
exchange in the bindings ETS table, completely resolving the cross-exchange
leakage while fully supporting AMQP 0-9-1's allowance for empty binding
keys without breaking backward compatibility.
This commit introduces a new feature flag, `topic_binding_projection_v5`,
and a corresponding Khepri projection version (v5) to deploy the
exchange isolation fix for empty binding keys.
@ansd ansd force-pushed the allow-empty-topic-filter branch from f9e5fd7 to 05a2191 Compare April 30, 2026 13:41
Copy link
Copy Markdown
Collaborator

@dumbbell dumbbell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The patch looks good to me. I only have a minor style comment.

Comment thread deps/rabbit/src/rabbit_khepri.erl Outdated
@ansd ansd requested a review from dumbbell April 30, 2026 13:50
@ansd
Copy link
Copy Markdown
Member Author

ansd commented Apr 30, 2026

I did some more manual testing including:

  • upgrading from v4.2.x to 4.3.x to this PR branch
  • upgrading from v4.2.x to this PR branch

In both cases after enabling all feature flags, the state with regards to the projection tables looked as expected.

@michaelklishin michaelklishin modified the milestones: 4.3.1, 4.4.0 Apr 30, 2026
@ansd ansd merged commit 6971ed6 into main May 5, 2026
189 checks passed
@ansd ansd deleted the allow-empty-topic-filter branch May 5, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants