{"record":{"id":"90079febc0c42ad0","repo":"bytedance/deer-flow","slug":"channels-buzz-relay-url-must-be-a-ws-or-wss","errorCode":null,"errorMessage":"channels.buzz.relay_url must be a ws:// or wss:// URL","messagePattern":"channels\\.buzz\\.relay_url must be a ws:// or wss:// URL","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/app/channels/buzz.py","lineNumber":258,"sourceCode":"      transient and retried. The connector's worst failure mode is going silently\n      deaf, so an unknown reason resolves toward \"keep listening\";\n      ``MAX_RESUBSCRIBE_ATTEMPTS`` is what stops that from becoming a loop when the\n      guess is wrong.\n    \"\"\"\n    text = (reason or \"\").strip().lower()\n    if text.startswith(_PERMANENT_CLOSE_PREFIXES):\n        return False\n    return not any(marker in text for marker in _PERMANENT_CLOSE_MARKERS)\n\n\nclass BuzzChannel(Channel):\n    _connect: Any = None  # test seam: async callable returning an async-context-manager transport\n\n    def __init__(self, bus: MessageBus, config: dict[str, Any]) -> None:\n        super().__init__(name=\"buzz\", bus=bus, config=config)\n        self._relay_url = str(config.get(\"relay_url\", \"\")).strip()\n        if not self._relay_url.startswith((\"ws://\", \"wss://\")):\n            raise ValueError(\"channels.buzz.relay_url must be a ws:// or wss:// URL\")\n        # One community per relay URL (see the design's multi-community note), so the\n        # relay host is this channel's workspace: it scopes inbound dedupe, the\n        # persisted connection row written by `/connect`, and the lookup that resolves\n        # that row back on the inbound path. Computed once here so those three uses\n        # can never drift apart.\n        self._workspace_id = urlparse(self._relay_url).netloc\n        self._private_key_raw = str(config.get(\"private_key\", \"\"))\n        self._keys: buzz_nostr.NostrKeys | None = None  # parsed in start() so coincurve stays lazy\n        self._allowed_users = {buzz_nostr.parse_pubkey(v) for v in config.get(\"allowed_users\", []) or []}\n        self._require_mention = bool(config.get(\"require_mention\", True))\n        self._mention_free = {str(c) for c in config.get(\"mention_free_channels\", []) or []}\n        self._channel_meta: dict[str, dict[str, Any]] = {}\n        self._stream_targets: dict[tuple[str, str | None], str] = {}\n        self._stream_tails: dict[tuple[str, str | None], list[str]] = {}  # overflow chunk ids beyond chunk 0, per conversation\n        self._last_requester: dict[tuple[str, str | None], str] = {}\n        self._pending_auth_challenge: str | None = None  # set from an AUTH relay frame; consumed by the NIP-42 flow in _session\n        self._seen_created_at: dict[str, int] = {}  # channel id -> high-water mark of PROCESSED created_at (see _advance_watermark)\n        self._chat_subscriptions: set[str] = set()  # channel ids with a live per-channel REQ on the CURRENT connection","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/channels/buzz.py#L240-L276","documentation":"BuzzChannel's constructor validates channels.buzz.relay_url at channel construction time: after stripping whitespace it must start with ws:// or wss://. This is fail-fast configuration validation — the relay URL is also used to derive _workspace_id via urlparse().netloc, so a malformed URL would corrupt inbound dedupe and the persisted connection row downstream.","triggerScenarios":"config.yaml contains channels.buzz.relay_url set to an http(s):// URL, a bare hostname like relay.example.com, an empty string, or a value with leading spaces that strips to none of the accepted schemes. The channel manager instantiates BuzzChannel during startup or config reload, and the ValueError aborts registration.","commonSituations":"Operator copies a nostr relay's https:// web URL instead of its websocket endpoint; forgets the scheme entirely; leaves relay_url empty while enabling the buzz channel; typo like wss:/ (single slash).","solutions":["Set channels.buzz.relay_url to the relay's websocket endpoint, e.g. wss://relay.example.com.","Confirm the relay actually speaks websocket (most Nostr relays publish both forms — use the ws one).","If you do not intend to run Buzz, remove or disable the channels.buzz block instead of leaving a placeholder URL.","Restart the Gateway after fixing config.yaml."],"exampleFix":"# before\nchannels:\n  buzz:\n    enabled: true\n    relay_url: https://relay.example.com\n\n# after\nchannels:\n  buzz:\n    enabled: true\n    relay_url: wss://relay.example.com","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\nRELAY = config.get('channels', {}).get('buzz', {}).get('relay_url', '')\nif RELAY and urlparse(RELAY).scheme not in ('ws', 'wss'):\n    raise SystemExit(f\"invalid buzz relay_url: {RELAY!r} — must be ws:// or wss://\")","typeGuard":null,"tryCatchPattern":"try:\n    channel = BuzzChannel(bus, cfg)\nexcept ValueError as e:\n    logger.error('buzz channel disabled: %s', e)\n    # skip registration instead of crashing the channel manager","preventionTips":["Validate channels.buzz.relay_url in `make doctor`/config schema so the error surfaces before startup.","Copy relay URLs from the relay's websocket documentation, not its landing page.","Unit-test channel constructors against the example config in CI."],"tags":["config","buzz","websocket","validation"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}