Files
xmpp-server-prosody/references/conversejs-blank-page-and-auth-testing.md
T
2026-09-06 13:51:11 +00:00

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 with export{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 → release converse.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 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:

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.)