louislam/uptime-kuma · error · Error

Expected TLS alert '${expectedTlsAlert}' but received '${res

Error message

Expected TLS alert '${expectedTlsAlert}' but received '${result.alertName}' (${result.alertNumber})

What it means

Sibling branch in checkTlsAlert (tcp.js:328-331). attemptTlsConnection failed (success:false) and parseTlsAlertNumber extracted a numeric alert, but its name does not match the expected_tls_alert the user configured. So the server DID send a TLS alert — just not the one the monitor asserts should appear.

Source

Thrown at server/monitor-types/tcp.js:329

        heartbeat.ping = result.responseTime;

        // Handle TLS info for certificate expiry monitoring
        if (result.tlsInfo && monitor.isEnabledExpiryNotification()) {
            await monitor.handleTlsInfo(result.tlsInfo);
        }

        // Check if we got the expected alert
        // Note: Error messages below could be translated, but alert names (e.g., certificate_required)
        // are from RFC 8446 spec and should remain in English for consistency with the spec.
        if (result.alertName === expectedTlsAlert) {
            heartbeat.status = UP;
            heartbeat.msg = `TLS alert received as expected: ${result.alertName} (${result.alertNumber})`;
        } else if (result.success) {
            throw new Error(
                `Expected TLS alert '${expectedTlsAlert}' but connection succeeded. The server accepted the connection without requiring a client certificate.`
            );
        } else if (result.alertNumber !== null) {
            throw new Error(
                `Expected TLS alert '${expectedTlsAlert}' but received '${result.alertName}' (${result.alertNumber})`
            );
        } else {
            throw new Error(
                `Expected TLS alert '${expectedTlsAlert}' but got unexpected error: ${result.errorMessage}`
            );
        }
    }

    /**
     * Attempt TLS connection and capture result/alert
     * @param {object} monitor Monitor object
     * @param {object} options TLS connection options
     * @param {number} startTime Connection start timestamp
     * @param {number} timeout Connection timeout in ms
     * @returns {Promise<object>} Connection result with success, responseTime, tlsInfo, alertNumber, alertName, errorMessage
     */
    attemptTlsConnection(monitor, options, startTime, timeout) {

View on GitHub (pinned to 6b5ea01557)

Solutions

  1. Compare result.alertName from the heartbeat with expectedTlsAlert — they must match exactly (RFC 8446 snake_case names).
  2. Update the monitor's expected_tls_alert to the alert the server now legitimately returns, OR fix the server so it returns the expected one.
  3. For 'bad_certificate' alerts, supply a valid client cert/key in the monitor (tls_cert/tls_key) so the test reflects the intended scenario.
  4. Run `openssl s_client -connect host:port` locally to see the alert the server emits.

Example fix

// before: monitor.expected_tls_alert = 'certificate_required' but server sends handshake_failure
// after: align expectation with observed behaviour
monitor.expected_tls_alert = 'handshake_failure';
Defensive patterns

Strategy: validation

Validate before calling

// Capture the real alert the server emits, then align configuration
function captureServerAlert(host, port) {
    const out = require('child_process').execSync(
        `openssl s_client -connect ${host}:${port} -servername ${host} -tlsextdebug 2>&1 < /dev/null`,
        { encoding: 'utf8', timeout: 8000 }
    );
    const m = out.match(/alert\s+(\d+)[^:]*:\s*([A-Za-z_]+)/);
    return m ? { number: +m[1], name: m[2] } : null;
}

Type guard

function isAlertResult(v) {
    return v != null && typeof v === 'object'
        && typeof v.alertNumber === 'number'
        && typeof v.alertName === 'string';
}

Try / catch

try {
    await monitor.checkTlsAlert(monitor, heartbeat, expectedTlsAlert);
} catch (e) {
    if (/but received/.test(e.message)) {
        // extract the received alert and reconcile config or server
        const received = e.message.match(/received '([a-z_]+)'\s*\((\d+)\)/);
        log.warn(`Alert mismatch — expected ${expectedTlsAlert}, got ${received?.[1]} (${received?.[2]})`);
    }
    throw e;
}

Prevention

When it happens

Trigger: Server returns 'handshake_failure' (40) when the user expected 'certificate_required' (116); client offered a cert the server rejected with 'bad_certificate' (10); protocol/cipher mismatch yields 'illegal_parameter' (47) instead of an auth alert.

Common situations: The expected_tls_alert was guessed rather than taken from an observed run; server-side TLS policy changed to reject for a different reason; client cert present but expired prompts 'certificate_expired' instead of 'certificate_required'.

Understand the failure class

Related errors


AI-assisted analysis of louislam/uptime-kuma@6b5ea01557 (2026-08-12). Data as JSON: /api/errors/868f854f11ab9850. Report an issue: GitHub.