Web

Prevent QZ Tray security popups for repeated prints

Stop QZ Tray security popups: initialize certificate and signature once, keep the WebSocket open, and use server-side signing to avoid prompts for QZ Tray.

1 answer 1 view

How to prevent warning popups from appearing every time I connect to QZ Tray using JavaScript, even after successful connection and printer listing?

I have a working QZ Tray setup that connects to QZ Tray on the client machine and lists available printers. However, every call to printAndReadTest() triggers security confirmation popups (likely for certificate and signature).

JavaScript code:

js
function signRequest(toSign) {
 return function(resolve, reject) {
 $.ajax({
 url: '<?= base_url('api/sign-message') ?>',
 method: 'POST',
 data: {
 request: toSign
 },
 dataType: 'text',
 success: resolve,
 error: reject
 });
 };
}

async function printAndReadTest() {
 try {
 if (!qz.websocket.isActive()) {
 console.log("Kobler til QZ Tray...");

 // Set up certificate and signature
 qz.security.setCertificatePromise(function(resolve, reject) {
 // Fetch your public certificate
 $.ajax({
 url: '<?= base_url('api/get-certificate') ?>',
 cache: false,
 dataType: 'text',
 success: resolve,
 error: reject
 });
 });

 qz.security.setSignatureAlgorithm("SHA512");
 qz.security.setSignaturePromise(signRequest);

 await qz.websocket.connect();
 }

 var printers = await qz.printers.find();
 console.log("Tilkobling til QZ Tray vellykket!\n\nTilgjengelige skrivere:\n" + printers.join("\n"));
 console.log("Tilgjengelige skrivere:", printers);

 } catch (err) {
 console.error("QZ Feil:", err);
 alert("Kunne ikke koble til QZ Tray. Sjekk om QZ Tray er installert og aktivert.");
 }
}

PHP server-side code:

php
public function get_certificate()
{
 $cert_path = FCPATH . 'qz-tray-cert.pem';
 
 if (!file_exists($cert_path)) {
 $this->output->set_status_header(404);
 echo 'Certificate not found';
 return;
 }
 
 $this->output
 ->set_content_type('text/plain')
 ->set_output(file_get_contents($cert_path));
}

public function sign_message()
{
 $request = $this->input->post('request');
 $private_key_path = FCPATH . 'qz-tray-private-key.pem';
 
 if (!file_exists($private_key_path)) {
 $this->output->set_status_header(500);
 echo 'Private key not found';
 return;
 }
 
 $private_key = file_get_contents($private_key_path);
 
 if (!openssl_sign($request, $signature, $private_key, OPENSSL_ALGO_SHA512)) {
 $this->output->set_status_header(500);
 echo 'Signing failed';
 return;
 }
 
 $this->output
 ->set_content_type('text/plain')
 ->set_output(base64_encode($signature));
}

The connection succeeds and printers are listed, but the popups appear repeatedly. What am I doing wrong? How can I suppress these QZ Tray warning popups for repeated calls?

The repeated QZ Tray popups happen because the certificate/signature promises are being treated as new security events each time — set them once (on page load or during a one-time init), keep the WebSocket connection alive between jobs, and make sure your server-side signing returns the exact base64 signature expected (no extra whitespace or encoding differences). Cache the certificate and signature client-side (or let the user “remember” the decision in QZ Tray), use server-side signing (never put private keys in the browser), and verify the private key / certificate pair and signing algorithm match.


Contents


Why QZ Tray shows repeated security popups

QZ Tray treats certificate/signature setup and each signing request as a security event. If the security promises or the signature/certificate presented to QZ change between calls (or are re-initialized repeatedly), QZ will show the confirmation UI again. The short fix is simple: set the certificate and signature promise exactly once (not inside a function you call for every job) and keep the WebSocket connection open so QZ doesn’t re-evaluate security for each call. The how-to guides that explain this pattern (move the certificate & signature setup outside repeated functions and cache them) are practical and show exactly the same steps you need to follow in your JavaScript code (see this practical walkthrough).

See a concise checklist in the next sections and a working code example below; if you want a quick read before fixing your code, this tutorial explains the same idea: move certificate & signature setup out of the print routine and reuse the connection (UMaTechnology guide).


qz tray certificate: set once and cache it

Why moving the certificate matters: QZ calls the promise you give it to obtain a certificate string. If that promise is re-registered or it returns a different string (extra whitespace, different line endings, different cert), QZ treats it like a new origin/cert and will prompt again. So:

  • Call qz.security.setCertificatePromise(…) once during app init. Cache the certificate in a JS variable (or localStorage if you want persistence).
  • Trim whitespace and ensure the certificate text includes the standard PEM header/footer exactly: -----BEGIN CERTIFICATE----- … -----END CERTIFICATE-----.
  • Do not ship your private key to the browser. Always sign server-side and return a base64 signature.

Practical reference: a real-world React/TypeScript guide shows the one-time init pattern and why backend signing is important (private key never in frontend) — read that for more context (Real-world QZ Tray guide).


Step-by-step JavaScript fix (qz tray javascript)

The minimal change to your code is to initialize QZ once and reuse that initialization for every print call. Below are two examples: a modern fetch-based init (recommended) and a jQuery-friendly snippet closer to your original code.

Recommended (modern) init + print:

js
// global flags / cache
let qzInitialized = false;
let cachedCert = null;

async function initQZ() {
 if (qzInitialized) return;
 // Fetch and cache certificate once
 cachedCert = await fetch('/api/get-certificate', { cache: 'no-store' })
 .then(r => r.text())
 .then(t => t.trim());

 qz.security.setCertificatePromise(function(resolve) {
 resolve(cachedCert);
 });

 qz.security.setSignatureAlgorithm('SHA512');
 qz.security.setSignaturePromise(function(toSign) {
 // QZ expects a function that returns (resolve, reject) => { ... }
 return function(resolve, reject) {
 fetch('/api/sign-message', {
 method: 'POST',
 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 body: new URLSearchParams({ request: toSign })
 })
 .then(r => r.text())
 .then(sig => resolve(sig.trim()))
 .catch(reject);
 };
 });

 qzInitialized = true;
}

async function printAndReadTest() {
 try {
 await initQZ();

 // Only connect if not already connected
 if (!qz.websocket.isActive()) {
 await qz.websocket.connect();
 }

 const printers = await qz.printers.find();
 console.log('Tilkobling til QZ Tray vellykket!\n\nTilgjengelige skrivere:\n' + printers.join('\n'));
 } catch (err) {
 console.error('QZ Error:', err);
 alert('Kunne ikke koble til QZ Tray. Sjekk om QZ Tray er installert og aktivert.');
 }
}

Minimal change using jQuery (matching your original style):

js
let qzInitialized = false;
let cachedCert = null;

function initQZ(callback) {
 if (qzInitialized) {
 if (callback) callback();
 return;
 }

 $.ajax({
 url: '<?= base_url('api/get-certificate') ?>',
 method: 'GET',
 cache: false,
 dataType: 'text'
 }).done(function(cert) {
 cachedCert = cert.trim();
 qz.security.setCertificatePromise(function(resolve) { resolve(cachedCert); });
 qz.security.setSignatureAlgorithm('SHA512');
 qz.security.setSignaturePromise(function(toSign) {
 return signRequest(toSign); // reuse your existing signRequest()
 });
 qzInitialized = true;
 if (callback) callback();
 }).fail(function(err) {
 console.error('Failed to load certificate', err);
 });
}

async function printAndReadTest() {
 try {
 await new Promise((resolve) => initQZ(resolve));
 if (!qz.websocket.isActive()) {
 await qz.websocket.connect();
 }
 var printers = await qz.printers.find();
 console.log("Tilkobling til QZ Tray vellykket!\n\nTilgjengelige skrivere:\n" + printers.join("\n"));
 } catch (err) {
 console.error("QZ Feil:", err);
 }
}

Key points:

  • Do not call setCertificatePromise/setSignaturePromise inside a function that’s invoked for every print job.
  • Use a single shared promise/handler. That prevents QZ from thinking the caller changed between jobs.
  • Keep the WebSocket connection open between print jobs; call connect only when qz.websocket.isActive() is false.

Server-side signing (PHP): ensure correct base64 signature

Your PHP logic is close, but using the private key resource functions and trimming helps avoid subtle issues. Here’s a robust version:

php
public function get_certificate()
{
 $cert_path = FCPATH . 'qz-tray-cert.pem';
 if (!file_exists($cert_path)) {
 $this->output->set_status_header(404)->set_output('Certificate not found');
 return;
 }
 $cert = trim(file_get_contents($cert_path));
 $this->output->set_content_type('text/plain')->set_output($cert);
}

public function sign_message()
{
 $request = $this->input->post('request', true); // sanitize
 $private_key_path = FCPATH . 'qz-tray-private-key.pem';
 if (!file_exists($private_key_path)) {
 $this->output->set_status_header(500)->set_output('Private key not found');
 return;
 }

 $private_key = file_get_contents($private_key_path);
 $pkey_res = openssl_pkey_get_private($private_key);
 if (!$pkey_res) {
 $this->output->set_status_header(500)->set_output('Invalid private key');
 return;
 }

 if (!openssl_sign($request, $signature, $pkey_res, OPENSSL_ALGO_SHA512)) {
 openssl_free_key($pkey_res);
 $this->output->set_status_header(500)->set_output('Signing failed');
 return;
 }

 openssl_free_key($pkey_res);
 $this->output->set_content_type('text/plain')->set_output(base64_encode($signature));
}

Tips:

  • Ensure the server returns the base64 signature as a plain string (no JSON wrapper, no extra newlines).
  • Use trim() client-side after fetching the response to remove accidental whitespace.
  • Test the server signing from the command line to confirm it outputs a valid base64 signature:
echo -n "test-data" | openssl dgst -sha512 -sign qz-tray-private-key.pem | openssl base64 -A

or a PHP quick-test:

bash
php -r '$d="test";$p=file_get_contents("qz-tray-private-key.pem");$res=openssl_pkey_get_private($p);openssl_sign($d,$sig,$res,OPENSSL_ALGO_SHA512);echo base64_encode($sig)."\n";'

If the CLI-generated signature verifies and the server-generated one matches, the signing is correct.


qz tray suppress warnings: client trust, premium signing, and settings

If you do everything right but still see popups, these are your options:

  • Let users accept the popup and tick “Remember this decision” in the QZ Tray dialog. That prevents future prompts for that origin/certificate pair.
  • Change the QZ Tray app setting (Security → Show security warnings) to off on clients where you control the machine — not recommended for untrusted environments. The community documentation shows this option as a troubleshooting step (UMaTechnology guide).
  • Use QZ’s signing service / premium features to avoid manual certificate installation on every client — some forums and threads discuss bypassing manual cert installs with QZ premium support (Innovative Users forum thread).
  • If you self-sign, you must either install the cert on each client or ensure users accept and remember the signature. Many community threads cover self-signed workflows and pitfalls (see discussions on Reddit for common issues and sample code: reddit client-side signing discussion and silent printing discussion).

Debug checklist & troubleshooting

Use this checklist when popups persist after applying the one-time init + persistent connection pattern:

  • Initialization

  • Did you move qz.security.setCertificatePromise and qz.security.setSignaturePromise to a single init that runs once? (Yes/no.)

  • Are you connecting only when qz.websocket.isActive() === false?

  • Certificate & signature format

  • Does /api/get-certificate return the PEM text exactly (headers, body, footer) and as text/plain? Trim both server and client outputs.

  • Does /api/sign-message return a single base64 string (no JSON, no extra newlines)? Trim on client.

  • Algorithm & key match

  • qz.security.setSignatureAlgorithm(‘SHA512’) must match server signing (OPENSSL_ALGO_SHA512).

  • Verify cert & key pair match:

  • For RSA keys:

  • openssl x509 -noout -modulus -in qz-tray-cert.pem | openssl md5

  • openssl rsa -noout -modulus -in qz-tray-private-key.pem | openssl md5

  • Or inspect openssl x509 -in cert.pem -noout -text and openssl pkey -in key.pem -noout -text.

  • Use the CLI signing test above to confirm server signs correctly.

  • Network & origin

  • Are you calling QZ from the same origin, host and protocol consistently? (http vs https, localhost vs 127.0.0.1 matters.)

  • Inspect browser DevTools Network tab: /api/get-certificate and /api/sign-message responses should be plain text and the signature should match the CLI test.

  • QZ Tray behavior

  • Check the QZ Tray console/logs for “Invalid Signature” or other errors (the UI will often tell you why it refused/asked).

  • If the popup shows “Invalid signature,” verify the signature algorithm/encoding.

  • If you still see popups

  • Try accepting the popup and enable “remember this decision” to test whether the behavior then disappears.

  • Consider using QZ signing service/premium if you want to avoid installing certs or making users confirm.

And yes — if you need a fresh installer for testing, you might search for “qz tray скачать” to download the client, but always prefer the official QZ Tray site for the correct build.


Sources


Conclusion

Move your certificate/signature setup out of the per-print function and initialize once, keep the WebSocket connected between jobs, and make sure your server signs with the expected algorithm and returns a clean base64 string — that combination stops qz tray popups in the vast majority of cases. If you must, let users trust the certificate on their machine or use QZ’s signing/premium options to remove prompts entirely.

Authors
Verified by moderation
Moderation
Prevent QZ Tray security popups for repeated prints