Skip to content

Stop the topic '*' wildcard from matching across dots - #2568

Merged
auvipy merged 3 commits into
celery:mainfrom
chuenchen309:fix/topic-star-crosses-dots
Aug 31, 2026
Merged

Stop the topic '*' wildcard from matching across dots#2568
auvipy merged 3 commits into
celery:mainfrom
chuenchen309:fix/topic-star-crosses-dots

Conversation

@chuenchen309

Copy link
Copy Markdown
Contributor

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 topic exchange routes messages based on words separated by dots, using wildcard characters * (any single word), and # (one or more words).

The bug

wildcards = {'*': r'.*?[^\.]',
             '#': r'.*?'}

* 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.*.c becomes ^a\..*?[^\.]\.c$, which happily matches a.b.x.c.

Reproducer

Through the public API, no mocks — Connection('memory://'), a queue bound with a.*.c, three messages published:

binding 'a.*.c' received:
   - CONTROL: routing_key='a.b.c'      (one word — should arrive)
   - routing_key='a.b.x.c'             (two words — should NOT arrive)
   - routing_key='a.b.x.y.c'           (three words — should NOT arrive)

The control arrives correctly, so this is the wildcard specifically rather than a broken reproducer. The compiled patterns:

binding current pattern matches
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.b

Scope

virtual/base.py:448 sets exchange_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.

wildcards = {'*': r'[^\.]+',
             '#': r'.*?'}

I checked the boundaries explicitly — * still matches exactly one word and still rejects an empty word (a.*.c vs a..c) and an empty key, and a.#.c still matches both a.b.c and a.b.x.c.

On #

While here I noticed a.#.c doesn't match a.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_lookup table — stock.us.nasdaq.tech must reach only the stock.# queue, not the stock.us.* one. It fails on main (Extra items in the left set: 'rBar') and passes with the fix.

There was no coverage for this: test_Topic and test_TopicMultibind only exercise stock.us.* against the single-word stock.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 (15 SQS, 1 pyro, both missing local credentials/deps), none attributable to this change. flake8 clean.

Disclosure

AI-assisted, and I'd rather say so plainly. The reproducer, the controls, the boundary table, the exchange_types scope check, and the main-vs-branch baseline diff are all things I ran and read myself rather than assumed. Happy to adjust scope or framing.

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>
@auvipy
auvipy requested a lite review from Copilot August 31, 2026 10:07
@auvipy auvipy added this to the 5.7.0 milestone Aug 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 TopicExchange wildcard 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 like stock.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

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.17%. Comparing base (e592de0) to head (fafff8e).
✅ All tests successful. No failed tests found.

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.
📢 Have feedback on the report? Share it here.

@auvipy
auvipy merged commit 78b528b into celery:main Aug 31, 2026
42 checks passed
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.

3 participants