"""
import asyncio
import logging
+import re
import threading
from typing import Callable, List
return value.encode("utf-8") if isinstance(value, str) else value
+_WHITESPACE = re.compile(r"\s+")
+
+
+def _sanitize_subject(subject: str) -> str:
+ # Routing keys come from shlex-quoted user text (e.g. "choose 'multi
+ # word option'") and can legally contain spaces under AMQP's topic
+ # exchange, which treats a routing key as an arbitrary string. NATS's
+ # wire protocol is whitespace-delimited ("PUB <subject> <#bytes>"), so a
+ # literal space inside a subject corrupts the PUB frame and the server
+ # drops the connection - confirmed live, message never reaches any
+ # subscriber. Since '#' is translated to ".*" by routing.matches() and
+ # absorbs any character here, substituting whitespace doesn't affect
+ # dispatch matching.
+ return _WHITESPACE.sub("_", subject)
+
+
def _stream_name_for(queue: str) -> str:
return queue.upper()
self._loop.close()
def publish(self, subject: str, body: bytes) -> None:
- full_subject = CLASSIFIER_PREFIX + _to_str(subject)
+ full_subject = CLASSIFIER_PREFIX + _sanitize_subject(_to_str(subject))
self._call(self._nc.publish(full_subject, _to_bytes(body)))
def publish_queue(self, queue: str, body: bytes, durable: bool = True, max_age_seconds: int = None) -> None:
--- /dev/null
+# -*- coding: utf-8 -*-
+import pytest
+
+from distbot.common.broker_nats import _sanitize_subject
+
+
+@pytest.mark.parametrize("subject,expected", [
+ # A quoted, multi-word plugin argument (e.g. choose "'multi word
+ # option'") shows up as a single shlex token containing spaces, which
+ # AMQP's topic exchange tolerates as an arbitrary routing key but NATS's
+ # whitespace-delimited wire protocol ("PUB <subject> <#bytes>") does
+ # not - an unsanitized space here corrupts the PUB frame and the
+ # message never reaches any subscriber (confirmed against a live
+ # nats-server).
+ ("nick.choose.will ich nicht", "nick.choose.will_ich_nicht"),
+ ("nick.choose.a\tb", "nick.choose.a_b"),
+ ("nick.dice.5", "nick.dice.5"),
+])
+def test_sanitize_subject_strips_whitespace(subject, expected):
+ assert _sanitize_subject(subject) == expected