From 1281b0e21f277d545e0c860fe032b01a7f6d3f65 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 20 Jun 2026 23:35:09 +0200 Subject: [PATCH] fix(qlik): send Origin/User-Agent on engine ws + verbose 403 diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qlik proxy validates the WebSocket Origin header (whitelist) — a browser always sends it. Add Origin + User-Agent to the engine ws. Also surface the 403 response (server/www-authenticate/body) for diagnosis. Co-Authored-By: Claude Opus 4.8 --- src/lib/qlik-client.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/lib/qlik-client.ts b/src/lib/qlik-client.ts index fd74333..6181bb0 100644 --- a/src/lib/qlik-client.ts +++ b/src/lib/qlik-client.ts @@ -168,7 +168,13 @@ function openEngine(appGuid: string, sess: QlikSession, cfg: QlikConfig): Promis const url = `wss://${cfg.host}/app/${encodeURIComponent(appGuid)}?Xrfkey=${sess.xrfkey}`; return new Promise((resolve, reject) => { const ws = new WebSocket(url, { - headers: { Cookie: sess.cookie, "X-Qlik-Xrfkey": sess.xrfkey }, + headers: { + Cookie: sess.cookie, + "X-Qlik-Xrfkey": sess.xrfkey, + // Le proxy Qlik valide l'Origin des websockets (whitelist) — un navigateur l'envoie toujours. + Origin: `https://${cfg.host}`, + "User-Agent": "Mozilla/5.0", + }, rejectUnauthorized: !cfg.tlsInsecure, handshakeTimeout: cfg.timeoutMs, }); @@ -188,7 +194,14 @@ function openEngine(appGuid: string, sess: QlikSession, cfg: QlikConfig): Promis else p.resolve(msg.result as Record); } }); - ws.on("unexpected-response", (_req, res) => { clearTimeout(timer); reject(new Error(`[qlik] ws HTTP ${res.statusCode}`)); }); + ws.on("unexpected-response", (_req, res) => { + clearTimeout(timer); + let body = ""; + res.on("data", (d: Buffer) => (body += d.toString())); + res.on("end", () => reject(new Error( + `[qlik] ws HTTP ${res.statusCode} server=${res.headers["server"]} wwwauth=${res.headers["www-authenticate"]} body=${body.slice(0, 160)}`, + ))); + }); ws.on("error", (e: Error) => { clearTimeout(timer); reject(e); }); ws.on("close", () => { for (const p of pending.values()) p.reject(new Error("[qlik] ws ferme")); pending.clear(); });