Skip to content

Fix routing for empty binding key to topic exchange (backport #16271)#16309

Open
mergify[bot] wants to merge 1 commit intov4.3.xfrom
mergify/bp/v4.3.x/pr-16271
Open

Fix routing for empty binding key to topic exchange (backport #16271)#16309
mergify[bot] wants to merge 1 commit intov4.3.xfrom
mergify/bp/v4.3.x/pr-16271

Conversation

@mergify
Copy link
Copy Markdown

@mergify mergify Bot commented May 5, 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

* Fix routing for empty binding key to topic exchange

Fixes https://github.com/rabbitmq/rabbitmq-server/discussions/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.

* Add new v5 projection for topic exchange routing

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.

* Fix style

* Add 4.3.1 release notes

* Exercise one more code path #16271 #16221

---------

Co-authored-by: Michael Klishin <michaelklishin@icloud.com>
(cherry picked from commit 6971ed6)
@mergify mergify Bot assigned ansd May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant