Files
pleno-vue/src/features/edgeGateways/edgeGatewayErrors.js
T

271 lines
9.2 KiB
JavaScript

const EDGE_GATEWAY_ERROR_MESSAGES = {
EDGE_GATEWAY_OFFLINE: {
title: "Gateway offline",
description: "Gatewayen sender ikke heartbeat. Kontrollér service og netværk, og prøv igen.",
},
EDGE_GATEWAY_STALE_HEARTBEAT: {
title: "Forældet heartbeat",
description: "Gatewayen svarer langsomt eller ustabilt. Kontrollér forbindelsen, og prøv igen.",
},
EDGE_GATEWAY_INVALID_TOKEN: {
title: "Ugyldige legitimationsoplysninger",
description: "Gateway-token blev afvist. Rotér credentials, og genstart agenten.",
},
EDGE_GATEWAY_OPERATION_TIMEOUT: {
title: "Operation timed out",
description: "Gatewayoperationen tog for lang tid. Gennemgå logs og forsøg igen.",
},
EDGE_GATEWAY_UNSUPPORTED_VERSION: {
title: "Version ikke understøttet",
description: "Målversionen eller artefaktet understøttes ikke af den installerede gateway.",
},
EDGE_GATEWAY_CONFLICT: {
title: "Operation i konflikt",
description: "En anden gatewayoperation kører allerede. Vent til den er afsluttet eller fejlet.",
},
EDGE_GATEWAY_VALIDATION_FAILED: {
title: "Ugyldig anmodning",
description: "Kontrollér input og prøv igen.",
},
EDGE_GATEWAY_CANCELLED: {
title: "Operation cancelled",
description: "The gateway operation was cancelled before it completed.",
},
};
const MESSAGE_CODE_PATTERNS = [
[/offline/i, "EDGE_GATEWAY_OFFLINE"],
[/heartbeat/i, "EDGE_GATEWAY_STALE_HEARTBEAT"],
[/token|credential/i, "EDGE_GATEWAY_INVALID_TOKEN"],
[/timeout/i, "EDGE_GATEWAY_OPERATION_TIMEOUT"],
[/version/i, "EDGE_GATEWAY_UNSUPPORTED_VERSION"],
[/conflict|already active|already in progress/i, "EDGE_GATEWAY_CONFLICT"],
[/cancel/i, "EDGE_GATEWAY_CANCELLED"],
[/validation|required|unsupported/i, "EDGE_GATEWAY_VALIDATION_FAILED"],
];
const WEBSOCKET_CLOSE_MESSAGES = {
agent_offline: "Gateway agent is not connected to the broker. Restart the edge agent or check the broker URL.",
agent_disconnected: "Gateway agent disconnected from the broker.",
agent_exit: "Gateway shell exited.",
shell_open_timeout: "Gateway agent did not confirm that the shell opened before the broker timeout.",
shell_spawn_failed: "Gateway failed to start the shell process.",
shell_session_expired: "The terminal session expired before the shell opened.",
upgrade_rejected: "The broker rejected the terminal websocket upgrade.",
socket_error: "Terminal websocket connection failed.",
};
const SENSITIVE_DIAGNOSTIC_KEYS = new Set([
"accesstoken",
"agenttoken",
"apikey",
"authorization",
"auth",
"clientsecret",
"jwt",
"key",
"password",
"secret",
"signature",
"token",
]);
const normalizeDiagnosticKey = (key) =>
String(key || "")
.replace(/[^a-z0-9]/gi, "")
.toLowerCase();
const isSensitiveDiagnosticKey = (key) => SENSITIVE_DIAGNOSTIC_KEYS.has(normalizeDiagnosticKey(key));
const redactSensitiveAssignments = (value) =>
String(value)
.replace(/\b(authorization|auth)\s*[:=]\s*(?:Bearer|Basic)\s+[^\s,;&]+/gi, (_match, key) => `${key}=***`)
.replace(
/([?&;,\s]?(access[_-]?token|agent[_-]?token|api[_-]?key|authorization|auth|client[_-]?secret|jwt|key|password|secret|signature|token)\s*[:=]\s*)("[^"\s,;&]*"|'[^'\s,;&]*'|[^\s,;&]+)/gi,
(_match, prefix) => `${prefix}***`
);
export function redactEdgeGatewayUrl(value) {
if (!value) {
return null;
}
try {
const url = new URL(String(value), typeof window !== "undefined" ? window.location.origin : "http://localhost");
if (url.username) {
url.username = "***";
}
if (url.password) {
url.password = "***";
}
Array.from(url.searchParams.keys()).forEach((key) => {
if (isSensitiveDiagnosticKey(key)) {
url.searchParams.set(key, "***");
}
});
return url.toString();
} catch (_error) {
return redactSensitiveAssignments(String(value));
}
}
export function redactEdgeGatewayDiagnostic(value) {
if (value === null || value === undefined) {
return value;
}
return redactSensitiveAssignments(
String(value).replace(/\b(?:wss?|https?|mqtts?):\/\/[^\s<>"')]+/gi, (match) => redactEdgeGatewayUrl(match))
);
}
const formatDiagnosticLines = (diagnostics, prefix = "Diagnostic") => {
if (!diagnostics || typeof diagnostics !== "object" || Array.isArray(diagnostics)) {
return [];
}
const lines = [];
const push = (label, value) => {
if (value === null || value === undefined || value === "") {
return;
}
lines.push(`${label}: ${redactEdgeGatewayDiagnostic(value)}`);
};
push(`${prefix} code`, diagnostics.reason_code || diagnostics.error_code);
push("Broker URL", diagnostics.broker_url);
push("WebSocket URL", diagnostics.ws_url);
push("Derived URL warning", diagnostics.derived_warning);
if (diagnostics.broker_presence && typeof diagnostics.broker_presence === "object") {
push("Broker connected", diagnostics.broker_presence.connected === true ? "yes" : "no");
push("Broker connection", diagnostics.broker_presence.connection_id);
push("Broker last seen", diagnostics.broker_presence.last_seen_at);
push(
"Broker age",
Number.isFinite(Number(diagnostics.broker_presence.age_seconds))
? `${Number(diagnostics.broker_presence.age_seconds)}s`
: null
);
push("Broker last error", diagnostics.broker_presence.last_error);
push("Broker disconnect reason", diagnostics.broker_presence.disconnect_reason);
}
return lines;
};
export function inferEdgeGatewayErrorCode(error) {
const payload = error?.response?.data?.data || error?.response?.data || {};
const explicitCode = payload?.error_code || payload?.code || null;
if (explicitCode) {
return explicitCode;
}
const message =
payload?.message || error?.message || error?.response?.data?.message || error?.response?.statusText || "";
const matched = MESSAGE_CODE_PATTERNS.find(([pattern]) => pattern.test(String(message)));
return matched ? matched[1] : "EDGE_GATEWAY_VALIDATION_FAILED";
}
export function normalizeEdgeGatewayError(error) {
const code = inferEdgeGatewayErrorCode(error);
const entry = EDGE_GATEWAY_ERROR_MESSAGES[code] || EDGE_GATEWAY_ERROR_MESSAGES.EDGE_GATEWAY_VALIDATION_FAILED;
const payload = error?.response?.data?.data || error?.response?.data || {};
const message = payload?.message || error?.message || entry.description;
const method = error?.config?.method ? String(error.config.method).toUpperCase() : null;
const url = error?.config?.url || null;
const status = Number.isInteger(error?.response?.status) ? error.response.status : null;
const requestId =
payload?.correlation_id ||
payload?.request_id ||
error?.response?.headers?.["x-request-id"] ||
error?.response?.headers?.["x-correlation-id"] ||
null;
const details = [];
if (method && url) {
details.push(`Request: ${method} ${url}`);
}
if (status) {
details.push(`Status: HTTP ${status}`);
}
if (payload?.error_code) {
details.push(`Backend code: ${payload.error_code}`);
}
if (requestId) {
details.push(`Request ID: ${requestId}`);
}
details.push(...formatDiagnosticLines(payload?.diagnostics, "Backend diagnostic"));
return {
code,
title: entry.title,
description: entry.description,
message,
details,
technical: {
method,
url,
status,
requestId,
backendCode: payload?.error_code || null,
diagnostics: payload?.diagnostics || null,
},
};
}
export function normalizeGatewayWebSocketClose(event = {}, context = {}) {
const payload = event && typeof event === "object" && !Array.isArray(event) ? event : { reason: event };
const reason = String(payload.reason || payload.reason_code || context.reason || "socket_closed").trim();
const codeValue = payload.code ?? context.code;
const code = Number.isFinite(Number(codeValue)) ? Number(codeValue) : null;
const wasClean = typeof payload.wasClean === "boolean" ? payload.wasClean : null;
const readyState = Number.isFinite(Number(payload.readyState ?? context.readyState))
? Number(payload.readyState ?? context.readyState)
: null;
const url = redactEdgeGatewayUrl(payload.url || payload.ws_url || context.url || context.ws_url);
const details = [];
if (code !== null) {
details.push(`WebSocket code: ${code}`);
}
if (reason) {
details.push(`Close reason: ${redactEdgeGatewayDiagnostic(reason)}`);
}
if (wasClean !== null) {
details.push(`Clean close: ${wasClean ? "yes" : "no"}`);
}
if (readyState !== null) {
details.push(`Ready state: ${readyState}`);
}
if (url) {
details.push(`WebSocket URL: ${url}`);
}
details.push(...formatDiagnosticLines(payload.details || payload.diagnostics || context.diagnostics));
const explicitMessage = String(payload.message || "").trim();
const mappedMessage = WEBSOCKET_CLOSE_MESSAGES[reason] || "";
const handshakeMessage =
code === 1005 || code === 1006
? "Broker did not complete the terminal WebSocket handshake. Verify EDGE_PUBLIC_BROKER_URL, proxy routing, and broker logs."
: "";
return {
reason,
code,
wasClean,
readyState,
url,
message: explicitMessage || mappedMessage || handshakeMessage || "",
details,
technical: {
reason,
code,
wasClean,
readyState,
url,
},
};
}
export { EDGE_GATEWAY_ERROR_MESSAGES };