_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:
# 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:
])
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