Stop the topic '*' wildcard from matching across dots - #2568
Merged
Conversation
TopicExchange documents '*' as any single word, but compiled it to '.*?[^\.]', whose leading '.*?' also matches dots. Only the final character was constrained, so 'a.*.c' matched 'a.b.x.c' and every virtual transport delivered messages to queues that never bound them. Compile '*' to a single dot-free word instead. '#' is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes topic-exchange wildcard semantics in the virtual transport implementation so that * matches exactly one dot-delimited word (as documented), preventing unintended over-delivery to queues bound with patterns like a.*.c.
Changes:
- Update
TopicExchangewildcard translation so*compiles to a single dot-free word regex ([^\.]+) instead of a cross-dot match. - Add a unit test case ensuring
stock.us.*does not match a longer routing key likestock.us.nasdaq.tech.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
kombu/transport/virtual/exchange.py |
Fixes * wildcard regex so it cannot match across dot separators in virtual topic routing. |
t/unit/transport/virtual/test_exchange.py |
Adds coverage to prevent regressions for multi-word routing keys against * bindings. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2568 +/- ##
=======================================
Coverage 83.17% 83.17%
=======================================
Files 79 79
Lines 10636 10636
Branches 1243 1243
=======================================
Hits 8846 8846
Misses 1584 1584
Partials 206 206 ☔ View full report in Codecov by Harness. |
auvipy
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A topic binding using
*matches routing keys with any number of words, not one — so queues receive messages they never bound.TopicExchange's own docstring is the spec here:The bug
*compiles to.*?[^\.]— "anything, then a non-dot". The intent reads as "one word, not ending in a dot", but the leading.*?is unconstrained and.matches dots, so only the final character is actually restricted.a.*.cbecomes^a\..*?[^\.]\.c$, which happily matchesa.b.x.c.Reproducer
Through the public API, no mocks —
Connection('memory://'), a queue bound witha.*.c, three messages published:The control arrives correctly, so this is the wildcard specifically rather than a broken reproducer. The compiled patterns:
a.*.c^a\..*?[^\.]\.c$a.b.c,a.b.x.c,a.b.x.y.c*.b^.*?[^\.]\.b$a.b,a.x.b*^.*?[^\.]$a,a.bScope
virtual/base.py:448setsexchange_types = dict(STANDARD_EXCHANGE_TYPES)and no transport overrides it, so every virtual transport routes through this — redis, mongodb, filesystem, SQS, kafka, and so on. Real AMQP brokers route server-side and are unaffected.The failure direction is over-delivery rather than loss: a queue silently processes messages outside its binding, which is why I'd call it more than cosmetic.
Fix
Compile
*to a single dot-free word.#is untouched.I checked the boundaries explicitly —
*still matches exactly one word and still rejects an empty word (a.*.cvsa..c) and an empty key, anda.#.cstill matches botha.b.canda.b.x.c.On
#While here I noticed
a.#.cdoesn't matcha.c, where AMQP defines#as zero or more words. I've deliberately left that alone: kombu's docstring says#is "one or more words", so the code matches its own documented contract, and changing it means restructuring the split/join rather than a wildcards entry. Happy to open a separate issue if you'd like it reconsidered.Tests
Added one case to the existing
test_Topic::test_lookuptable —stock.us.nasdaq.techmust reach only thestock.#queue, not thestock.us.*one. It fails onmain(Extra items in the left set: 'rBar') and passes with the fix.There was no coverage for this:
test_Topicandtest_TopicMultibindonly exercisestock.us.*against the single-wordstock.us.nasdaq, so neither behaviour was pinned — no existing test changed.Full unit suite, same command and tree both ways: 1184 passed with the fix vs. 1183 on
main(the extra one is the new case). Failures are byte-identical between the two runs — 16 pre-existing ones in my sandbox (15SQS, 1pyro, both missing local credentials/deps), none attributable to this change.flake8clean.Disclosure
AI-assisted, and I'd rather say so plainly. The reproducer, the controls, the boundary table, the
exchange_typesscope check, and themain-vs-branch baseline diff are all things I ran and read myself rather than assumed. Happy to adjust scope or framing.