HKUDS/Vibe-Trading · critical · ValueError
ssl_certfile and ssl_keyfile must both be set for WSS, or bo
Error message
ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty
What it means
Raised by WebSocketChannel._build_ssl_context (called from start) when exactly one of ssl_certfile / ssl_keyfile is set. A TLS server needs a certificate and its matching private key as a pair; providing only one makes WSS impossible, so the channel refuses to start rather than falling back to plaintext.
Source
Thrown at agent/src/channels/websocket.py:376
except ConnectionClosed:
self._cleanup_connection(connection)
except Exception as e:
self.logger.warning("failed to send {} event: {}", event, e)
@classmethod
def default_config(cls) -> dict[str, Any]:
return WebSocketConfig().model_dump(by_alias=True)
def _expected_path(self) -> str:
return _normalize_config_path(self.config.path)
def _build_ssl_context(self) -> ssl.SSLContext | None:
cert = self.config.ssl_certfile.strip()
key = self.config.ssl_keyfile.strip()
if not cert and not key:
return None
if not cert or not key:
raise ValueError(
"ssl_certfile and ssl_keyfile must both be set for WSS, or both left empty"
)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.load_cert_chain(certfile=cert, keyfile=key)
return ctx
# -- HTTP dispatch ------------------------------------------------------
async def _dispatch_http(self, connection: Any, request: WsRequest) -> Any:
"""Route an inbound HTTP request to the HTTP handler or WS upgrade."""
got, query = _parse_request_path(request.path)
# WebSocket upgrade — channel handles this itself
expected_ws = self._expected_path()
if got == expected_ws and _is_websocket_upgrade(request):
client_id = _query_first(query, "client_id") or ""
if len(client_id) > 128:View on GitHub (pinned to 80ffdda44c)
Solutions
- Set both fields: ssl_certfile=/etc/ssl/app/fullchain.pem and ssl_keyfile=/etc/ssl/app/privkey.pem
- Check that each path actually exists and is readable by the service user (a wrong path that yields an empty string triggers the same error)
- If you terminate TLS at a reverse proxy (nginx/traefik), clear both fields and bind plaintext behind the proxy
- Reload/renew certificates if a renewal job emptied one of the files
Example fix
# before ssl_certfile = "/etc/letsencrypt/live/app/fullchain.pem" # keyfile missing # after ssl_certfile = "/etc/letsencrypt/live/app/fullchain.pem" ssl_keyfile = "/etc/letsencrypt/live/app/privkey.pem"
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def ssl_pair_ok(cert: str, key: str) -> bool:
cert, key = cert.strip(), key.strip()
return (cert and key and Path(cert).is_file() and Path(key).is_file()) or (not cert and not key)
assert ssl_pair_ok(cfg.ssl_certfile, cfg.ssl_keyfile), "provide BOTH cert and key, or neither" Type guard
def has_complete_ssl_pair(cert: str, key: str) -> bool:
return bool(cert.strip()) == bool(key.strip()) Try / catch
try:
await channel.start()
except ValueError as e:
if "ssl_certfile" in str(e):
log.error("TLS misconfigured: set both ssl_certfile and ssl_keyfile, or clear both behind a TLS proxy")
raise Prevention
- Reference cert and key from the same issuance dir (letsencrypt live/<domain>/{fullchain,privkey}.pem)
- Add a startup preflight that checks both files exist and are readable
- If TLS terminates at a proxy, leave both fields empty rather than setting one
When it happens
Trigger: Starting the channel with ssl_certfile set but ssl_keyfile empty (or vice versa). Both fields are stripped; if exactly one is truthy, ValueError is raised before SSLContext is built.
Common situations: Pointing both fields at the same PEM bundle containing only the cert (common with fullchain.pem from Let's Encrypt) and forgetting privkey.pem; env var for one file misnamed so it resolves empty; cert issued as cert+key in separate secret mounts where only one was mounted; migrating from HTTP to WSS and configuring only the certificate.
Related errors
- unix_socket_path must not contain NUL bytes
- unix_socket_path must be an absolute path
- path must start with "/"
- token_issue_path must start with "/"
- token_issue_path must differ from path (the WebSocket upgrad
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/c23d35741b127cd8.
Report an issue: GitHub.