Files
pulse/frontend-modern/src/utils/connectionErrorPresentation.ts
T
rcourtman 321f563a52 Surface blocked workload inventory sources
Show workload-capable source failures on Workloads and keep matching Proxmox host agents attached to their API source when inventory collection is blocked.
2026-05-14 00:22:18 +01:00

124 lines
4.4 KiB
TypeScript

/**
* connectionErrorPresentation
*
* Translates raw backend connection errors (often Go error chains exposing
* implementation detail like 'context deadline exceeded' or
* 'Client.Timeout exceeded while awaiting headers') into short user-facing
* messages.
*
* The connection-table row already shows the connection name and host, so
* the humanized message focuses on the failure mode and a short remediation
* hint without restating identity.
*
* For unknown errors we fall back to the raw message stripped of the most
* common implementation prefixes so it's at least readable.
*/
const POLL_PREFIX_PATTERN = /^poll_[a-z_]+\s+failed\s+on\s+[^:]+:\s*/i;
// Patterns from common Go error chains: `Get "https://...":`, `Post "..."`.
const HTTP_VERB_PATTERN = /\b(?:Get|Post|Put|Delete|Patch|Head)\s+"[^"]+":\s*/g;
interface HumanizedConnectionError {
/** Short headline ('Connection timed out', 'Host not found', etc.). */
headline: string;
/** One-line remediation hint, or null when none applies. */
hint: string | null;
}
const TIMEOUT_HINT =
'Check the host is reachable, the port is correct, and the network path is open.';
const TLS_HINT =
'Verify the certificate or pin a fingerprint in TLS settings, or disable TLS verification temporarily for testing.';
const AUTH_HINT = 'Re-check the API token or username/password.';
const HUMANIZED_PATTERNS: { match: RegExp; headline: string; hint: string | null }[] = [
{
match: /context deadline exceeded|Client\.Timeout|i\/o timeout|deadline exceeded/i,
headline: 'Connection timed out',
hint: TIMEOUT_HINT,
},
{
match: /no such host|dns:.*lookup|name does not resolve/i,
headline: 'Host not found',
hint: 'Check the hostname or IP address.',
},
{
match: /connection refused|connect:\s*connection refused/i,
headline: 'Connection refused',
hint: 'The host is reachable but rejected the connection on this port. Check the port is correct and the service is running.',
},
{
match: /no route to host|network is unreachable/i,
headline: 'No network route to host',
hint: 'The host cannot be reached from this Pulse instance. Check VPN, firewall, or VLAN configuration.',
},
{
match: /x509:.*signed by unknown authority|certificate signed by unknown/i,
headline: 'TLS certificate not trusted',
hint: TLS_HINT,
},
{
match: /x509:.*expired|certificate has expired/i,
headline: 'TLS certificate expired',
hint: TLS_HINT,
},
{
match: /tls:\s*handshake failure|tls handshake|bad certificate/i,
headline: 'TLS handshake failed',
hint: TLS_HINT,
},
{
match: /\b401\b|unauthorized|authentication failed|invalid (?:credentials|api token)/i,
headline: 'Authentication failed',
hint: AUTH_HINT,
},
{
match: /\b403\b|forbidden|permission denied/i,
headline: 'Permission denied',
hint: 'The credentials connected, but the user/token lacks the required role.',
},
{
match: /\b404\b|not found/i,
headline: 'Endpoint not found',
hint: 'Pulse reached the host but the API path is missing. Check the URL path or the platform version.',
},
{
match: /\b5\d{2}\b|server error|internal error/i,
headline: 'Server error',
hint: 'The platform returned a server error. Check the platform logs for the underlying cause.',
},
];
const stripImplementationPrefixes = (raw: string): string =>
raw
.replace(POLL_PREFIX_PATTERN, '')
.replace(HTTP_VERB_PATTERN, '')
.trim();
export const humanizeConnectionError = (raw?: string | null): HumanizedConnectionError | null => {
if (!raw) return null;
const trimmed = raw.trim();
if (!trimmed) return null;
for (const pattern of HUMANIZED_PATTERNS) {
if (pattern.match.test(trimmed)) {
return { headline: pattern.headline, hint: pattern.hint };
}
}
// Unknown error: fall back to the raw text with the most common
// implementation-leaking prefixes peeled off so it's at least readable.
const cleaned = stripImplementationPrefixes(trimmed);
return { headline: cleaned || 'Connection error', hint: null };
};
/**
* Render a single-line message suitable for inline display in a table row.
* Combines headline and hint when both apply.
*/
export const formatConnectionErrorMessage = (raw?: string | null): string | null => {
const humanized = humanizeConnectionError(raw);
if (!humanized) return null;
return humanized.hint ? `${humanized.headline}. ${humanized.hint}` : humanized.headline;
};