From: Thorsten Date: Sat, 8 Aug 2026 09:58:51 +0000 (+0200) Subject: Fix NATS silently dropping messages whose routing key has an empty token X-Git-Url: https://git.aero2k.de/?a=commitdiff_plain;h=HEAD;p=urlbot-v3.git Fix NATS silently dropping messages whose routing key has an empty token bot.py joins shlex tokens with '.' and keeps any punctuation the body had, so a message ending in a full stop (" ... ich will das.") yields a routing key ending in '.' - an empty trailing token. AMQP's topic exchange routed that to '#' fine, but nats-server accepts the PUB without any error while "classifier.>" never matches it, so the message vanished with nothing logged on either side. Leading dots and a typed "..." (its own token) fail the same way. Collapse runs of dots and trim the ends in _sanitize_subject(), with a fallback token for a key that reduces to nothing. Empty tokens carry no information, and the binding keys plugins use still match the trimmed form. Symptom that surfaced it: a posted YouTube URL followed by a sentence went unanswered - and with URLResolver deliberately skipping youtube URLs, no fallback title appeared either. Co-Authored-By: Claude Opus 5 --- diff --git a/src/distbot/common/broker_nats.py b/src/distbot/common/broker_nats.py index cf37b67..1c260c9 100644 --- a/src/distbot/common/broker_nats.py +++ b/src/distbot/common/broker_nats.py @@ -50,6 +50,11 @@ def _to_bytes(value) -> bytes: _WHITESPACE = re.compile(r"\s+") +_EMPTY_TOKENS = re.compile(r"\.{2,}") + +# Used when a routing key sanitizes down to nothing at all (e.g. a message +# body of just "."), since "classifier." is itself an empty trailing token. +EMPTY_SUBJECT_TOKEN = "_" def _sanitize_subject(subject: str) -> str: @@ -62,7 +67,22 @@ def _sanitize_subject(subject: str) -> str: # subscriber. Since '#' is translated to ".*" by routing.matches() and # absorbs any character here, substituting whitespace doesn't affect # dispatch matching. - return _WHITESPACE.sub("_", subject) + subject = _WHITESPACE.sub("_", subject) + + # Same class of problem, different NATS rule: an *empty token* makes a + # subject undeliverable. bot.py builds routing keys by joining shlex + # tokens with '.', and the tokens keep any punctuation the body had, so + # an ordinary sentence ending in a full stop ("... ich will das.") + # yields "...ich.will.das." - a trailing empty token. AMQP's topic + # exchange routes that to '#' happily; nats-server accepts the PUB + # without any error but "classifier.>" then never matches it, so the + # message vanishes with nothing logged anywhere (confirmed live). + # Leading dots and runs of dots (a typed "..." becomes its own token) + # fail the same way. Collapsing and trimming them is safe for dispatch: + # empty tokens carry no information, and the binding keys plugins + # actually use ("nick.dice.#", "#") match the trimmed form just as well. + subject = _EMPTY_TOKENS.sub(".", subject).strip(".") + return subject or EMPTY_SUBJECT_TOKEN def _stream_name_for(queue: str) -> str: diff --git a/tests/test_unit/test_broker_nats.py b/tests/test_unit/test_broker_nats.py index 18cd7ab..021d8fe 100644 --- a/tests/test_unit/test_broker_nats.py +++ b/tests/test_unit/test_broker_nats.py @@ -18,3 +18,23 @@ from distbot.common.broker_nats import _sanitize_subject ]) def test_sanitize_subject_strips_whitespace(subject, expected): assert _sanitize_subject(subject) == expected + + +@pytest.mark.parametrize("subject,expected", [ + # An empty token makes a subject undeliverable: nats-server accepts the + # PUB without an error, but "classifier.>" never matches it and the + # message is dropped silently. The common case is a body ending in a + # full stop, which is why a posted URL followed by a sentence went + # unanswered with nothing in the logs. + ("httpswww.youtube.comwatchvco57sfct-h0.ich.will.das.", + "httpswww.youtube.comwatchvco57sfct-h0.ich.will.das"), + ("nick.dice.5.", "nick.dice.5"), + # a typed "..." becomes a token of its own + ("hallo.....welt", "hallo.welt"), + (".hallo", "hallo"), + ("nick.dice.5", "nick.dice.5"), + # a body of just "." leaves nothing to route on + (".", "_"), +]) +def test_sanitize_subject_drops_empty_tokens(subject, expected): + assert _sanitize_subject(subject) == expected