# Converse.js v14 blank page: websocket_url fix + auth verification Follow-up to `webchat-conversejs-and-caddy-trap.md` (same ICQ project, 2026-08-28). These lessons were confirmed after the initial web client deployment: the page served HTML/CSS/JS fine but rendered NOTHING. ## ⚠️ ROOT CAUSE #1 (FIXED 2026-08-28): Converse v14 is an ES MODULE, not UMD The v14 dist bundle (`converse.min.js`) is built as a genuine ESM module: - it uses `import.meta.url` (webpack auto-publicPath) and ends with `export{c as default}`; - it lazy-loads chunks via dynamic `import("./" + chunkName)` (e.g. `chunkjs/locales/...`). Loading it with a classic ` ``` Verification (headless chromium, no extra installs — chromium is in snap on bigbox): ```bash chromium --headless --no-sandbox --disable-gpu --virtual-time-budget=15000 \ --dump-dom https://chat.nixg.ru/ | grep -c converse-login-form # 1 = UI rendered # console errors BEFORE the fix: chromium --headless --no-sandbox --enable-logging=stderr --virtual-time-budget=20000 \ --dump-dom https://chat.nixg.ru/ 2>&1 | grep -iE 'CONSOLE|Uncaught' ``` ## ROOT CAUSE #2 (earlier, already fixed): `bosh_service_url` vs `websocket_url` | Option | What Converse does | |--------|-------------------| | `websocket_url: 'wss://host/xmpp-websocket'` | Connects via WebSocket. **Use this.** | | `bosh_service_url: 'wss://host/xmpp-websocket'` | Treats the URL as BOSH (HTTP long-polling endpoint). With a wss:// URL and no BOSH handler on the server, the connection fails SILENTLY and the UI never renders. | Evidence from the v14 bundle: ```js function d1() { return ("WebSocket" in window || "MozWebSocket" in window) && cZ.get("websocket_url") ? cZ.get("websocket_url") : cZ.get("bosh_service_url") ? cZ.get("bosh_service_url") : ""; } ``` So `websocket_url` takes priority; `bosh_service_url` is only a BOSH fallback. Passing a wss:// URL via `bosh_service_url` is a guaranteed blank page. ## Locales are embedded in v14 The bundle contains all translations as lazy-loaded webpack chunks (`./src/i18n/locales/*/LC_MESSAGES/converse.po`). `i18n: 'ru'` needs NO external locale files — do not go hunting for .po files or a locales_path. ## `assets_path` default is `/dist` Defaults contain `assets_path: "/dist"`. If you deploy bare files (converse.min.js/css in the web root) rather than the full dist layout, set `assets_path` explicitly to a path that exists, or Converse may look for extra assets under /dist and fail. This is the leading hypothesis when the page is STILL blank after the websocket_url fix. ## Diagnosing a blank page without a browser Check the nginx (webchat container) access log: **a healthy Converse loads JS/CSS with 200s AND then issues a GET to /xmpp-websocket** (the WS upgrade). If css/js get 200s but NO /xmpp-websocket request ever appears, the client is failing during initialize/rendering BEFORE it attempts the socket — i.e. a client-side config/asset problem, not a server problem. Confirm the server side separately (see below) so you know the failure is client-side. ```bash docker logs icq-webchat --since 10m | grep -vE "GET /converse\.min" | tail -20 # look for: GET /xmpp-websocket (or absence of it) ``` ## Verify auth end-to-end over WebSocket (slixmpp) Proves the whole path (browser → Caddy → nginx → Prosody → SASL) works even when the web client is broken. slixmpp needs a no-verify SSL context for self-signed certs: ```bash pip install slixmpp --break-system-packages ``` ```python import asyncio, ssl, slixmpp class Bot(slixmpp.ClientXMPP): def __init__(self, jid, password): super().__init__(jid, password) self.add_event_handler('session_start', self.on_start) self.add_event_handler('failed_auth', self.on_failed) self.add_event_handler('disconnected', self.on_disconnect) async def on_start(self, event): print("AUTH OK:", self.boundjid) await self.disconnect() def on_failed(self, event): print("AUTH FAIL"); self.disconnect() def on_disconnect(self, event): self.stop() async def main(): bot = Bot('user@chat.nixg.ru', 'PASSWORD') ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE bot.ssl_context = ctx bot.connect(('wss://chat.nixg.ru/xmpp-websocket',)) # full external path incl. TLS await asyncio.wait_for(bot.disconnected, timeout=25) asyncio.run(main()) ``` Expect `AUTH OK: user@chat.nixg.ru/...`. Notes: - Direct 5222 TCP test will ALSO hit `SSLCertVerificationError` on self-signed certs — the `ssl_context` override fixes both, but prefer the wss:// test (validates Caddy TLS and the WS proxy too). - `slixmpp.connect()` has NO `reattempt=` kwarg in current versions — omit it. - `prosodyctl register user domain pass` inside the container is how you set/reset a password (idempotent, works even if the account exists). ## CDN-hardening: local assets cdn.conversejs.org may be unreachable/slow from RF. Download the release once and serve locally: ```bash cd /opt/icq/webchat curl -sL -o converse.min.js https://cdn.conversejs.org/dist/converse.min.js curl -sL -o converse.min.css https://cdn.conversejs.org/dist/converse.min.css # NOTE: https://cdn.conversejs.org/css/converse.min.css does NOT exist (404); the css # lives under /dist/ in v14. Check with `curl -sI` and `head -c 100` — a 1.4KB "css" is a 404. ``` Verify the downloaded JS parses: `node --check converse.min.js` → "JS СИНТАКСИС OK". (If `node` isn't present, `which node` first; the Hermes sandbox runs it from ~/.local/bin.)