7.0 KiB
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 withexport{c as default}; - it lazy-loads chunks via dynamic
import("./" + chunkName)(e.g.chunkjs/locales/...).
Loading it with a classic <script src="converse.min.js"> throws in Chrome/Firefox:
Uncaught SyntaxError: Cannot use 'import.meta' outside a module
→ the bundle NEVER executes → converse is undefined → blank page, and no request to
/xmpp-websocket ever appears in nginx logs (the client dies before connecting).
node --check converse.min.js passes (valid ES syntax), so syntax-checking gives FALSE
confidence. Check for import.meta / export{c as default} instead — build two
distinct bundles and shipped both:
dist/converse.min.js: ESM entry — load ONLY via<script type="module">+import converse from '/dist/converse.min.js'in a second inline module script.- Serve the FULL dist directory from the release tarball (GitHub
conversejs/converse→ releaseconverse.js-14.0.0.tgz→package/dist/), which contains:chunkjs/locales/*,libomemo.esm.min.js,curve25519_compiled.wasm,sounds/,webfonts/,images/. Dynamic imports resolve relative to the module URL, so/dist/converse.min.js⇒ chunks at/dist/chunkjs/...automatically.
Minimal working skeleton (see /opt/icq/webchat/index.html for the full version):
<link rel="stylesheet" href="/dist/converse.min.css">
<div id="conversejs"></div>
<script type="module" src="/dist/converse.min.js"></script>
<script type="module">
import converse from '/dist/converse.min.js';
converse.initialize({ websocket_url: 'wss://chat.nixg.ru/xmpp-websocket',
view_mode: 'fullscreen', i18n: 'ru' });
</script>
Verification (headless chromium, no extra installs — chromium is in snap on bigbox):
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:
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.
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:
pip install slixmpp --break-system-packages
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
SSLCertVerificationErroron self-signed certs — thessl_contextoverride fixes both, but prefer the wss:// test (validates Caddy TLS and the WS proxy too). slixmpp.connect()has NOreattempt=kwarg in current versions — omit it.prosodyctl register user domain passinside 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:
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.)