"Failed to connect to Livekit server (Reason: ServerUnreachable)" left a user with nothing to try; they retried for a long time and gave up. - Title "Couldn't connect to voice". The message depends on the reason: unreachable/timeout-style errors explain that voice needs its own live connection, which VPNs, antivirus web shields and some networks block, even when chat works, and say what to try; NotAllowed says the call may be full or access changed. - The raw reason, status and message move under Technical details. - LivekitConnectionError and PeerConnectionTimeoutError now get a Try again button (the existing reconnect path), shown as primary: the Lotus theme renders `secondary` as dark-on-dark. Verified in the Lotus client against a local LiveKit with the signal socket and validate request blocked: the dialog appears after ~20 s; unblocking and pressing Try again joins with the mic published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
/*
|
|
Copyright 2025 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { t } from "i18next";
|
|
import { type ConnectionError, ConnectionErrorReason } from "livekit-client";
|
|
|
|
import { i18nKey } from "./i18n";
|
|
|
|
export enum ErrorCode {
|
|
/**
|
|
* Configuration problem due to no MatrixRTC transport provided by homeserver and no fallback configured.
|
|
*/
|
|
MISSING_MATRIX_RTC_TRANSPORT = "MISSING_MATRIX_RTC_TRANSPORT",
|
|
CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR",
|
|
INTERNAL_MEMBERSHIP_MANAGER = "INTERNAL_MEMBERSHIP_MANAGER",
|
|
FAILED_TO_START_LIVEKIT = "FAILED_TO_START_LIVEKIT",
|
|
/** LiveKit indicates that the server has hit its track limits */
|
|
INSUFFICIENT_CAPACITY_ERROR = "INSUFFICIENT_CAPACITY_ERROR",
|
|
E2EE_NOT_SUPPORTED = "E2EE_NOT_SUPPORTED",
|
|
STICKY_EVENTS_NOT_SUPPORTED = "STICKY_EVENTS_NOT_SUPPORTED",
|
|
OPEN_ID_ERROR = "OPEN_ID_ERROR",
|
|
/** [lotus] The SFU token service refused us with a human-readable reason (e.g. the voice-limit guard: channel full / no permission). */
|
|
SFU_TOKEN_REFUSED = "SFU_TOKEN_REFUSED",
|
|
NO_MATRIX_2_AUTHORIZATION_SERVICE = "NO_MATRIX_2_0_AUTHORIZATION_SERVICE",
|
|
SFU_ERROR = "SFU_ERROR",
|
|
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
|
}
|
|
|
|
export enum ErrorCategory {
|
|
/** Calling is not supported, server misconfigured (JWT service missing, no MSC support ...)*/
|
|
CONFIGURATION_ISSUE = "CONFIGURATION_ISSUE",
|
|
NETWORK_CONNECTIVITY = "NETWORK_CONNECTIVITY",
|
|
CLIENT_CONFIGURATION = "CLIENT_CONFIGURATION",
|
|
UNKNOWN = "UNKNOWN",
|
|
SYSTEM_FAILURE = "SYSTEM_FAILURE",
|
|
// SYSTEM_FAILURE / FEDERATION_FAILURE ..
|
|
}
|
|
|
|
/**
|
|
* Structure for errors that occur when using ElementCall.
|
|
*/
|
|
export class ElementCallError extends Error {
|
|
public code: ErrorCode;
|
|
public category: ErrorCategory;
|
|
public localisedMessage?: string;
|
|
public localisedTitle: string;
|
|
|
|
// Alternative to localisedMessage, for rich error rendering
|
|
public localisedMessageKey?: string;
|
|
public localisedMessageValues?: Record<string, string>;
|
|
|
|
/** [lotus] Raw detail shown under a collapsed "Technical details", not in the message. */
|
|
public technicalDetails?: string;
|
|
|
|
protected constructor(
|
|
localisedTitle: string,
|
|
code: ErrorCode,
|
|
category: ErrorCategory,
|
|
localisedMessage?: string,
|
|
cause?: Error,
|
|
) {
|
|
super(localisedTitle, { cause });
|
|
this.localisedTitle = localisedTitle;
|
|
this.localisedMessage = localisedMessage;
|
|
this.category = category;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configuration problem due to no MatrixRTC transport provided by homeserver and no fallback configured.
|
|
*/
|
|
export class MatrixRTCTransportMissingError extends ElementCallError {
|
|
public domain: string;
|
|
|
|
/**
|
|
* Creates an instance of MatrixRTCTransportMissingError.
|
|
* @param domain - The domain where the MatrixRTC transport is missing.
|
|
*/
|
|
public constructor(domain: string) {
|
|
super(
|
|
t("error.call_is_not_supported"),
|
|
ErrorCode.MISSING_MATRIX_RTC_TRANSPORT,
|
|
ErrorCategory.CONFIGURATION_ISSUE,
|
|
t("error.matrix_rtc_transport_missing", {
|
|
domain,
|
|
brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call",
|
|
errorCode: ErrorCode.MISSING_MATRIX_RTC_TRANSPORT,
|
|
}),
|
|
);
|
|
this.domain = domain;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating that the connection to the call was lost and could not be re-established.
|
|
*/
|
|
export class ConnectionLostError extends ElementCallError {
|
|
public constructor() {
|
|
super(
|
|
t("error.connection_lost"),
|
|
ErrorCode.CONNECTION_LOST_ERROR,
|
|
ErrorCategory.NETWORK_CONNECTIVITY,
|
|
t("error.connection_lost_description"),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating a failure in the membership manager causing the join call
|
|
* operation to fail.
|
|
*/
|
|
export class MembershipManagerError extends ElementCallError {
|
|
/**
|
|
* Creates an instance of MembershipManagerError.
|
|
*
|
|
* @param error - The underlying error that caused the membership manager failure.
|
|
*/
|
|
public constructor(error: Error) {
|
|
super(
|
|
t("error.membership_manager"),
|
|
ErrorCode.INTERNAL_MEMBERSHIP_MANAGER,
|
|
ErrorCategory.SYSTEM_FAILURE,
|
|
t("error.membership_manager_description"),
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating that this deployment pins `matrix_rtc_mode=matrix_2_0` in
|
|
* config.json but the homeserver does not advertise MSC4354 (sticky events),
|
|
* which the Matrix 2.0 mode requires.
|
|
*/
|
|
export class StickyEventsRequiredError extends ElementCallError {
|
|
public constructor() {
|
|
super(
|
|
t("error.sticky_events_required"),
|
|
ErrorCode.STICKY_EVENTS_NOT_SUPPORTED,
|
|
ErrorCategory.CONFIGURATION_ISSUE,
|
|
t("error.sticky_events_required_description"),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating that end-to-end encryption is not supported in the current environment.
|
|
*/
|
|
export class E2EENotSupportedError extends ElementCallError {
|
|
public constructor() {
|
|
super(
|
|
t("error.e2ee_unsupported"),
|
|
ErrorCode.E2EE_NOT_SUPPORTED,
|
|
ErrorCategory.CLIENT_CONFIGURATION,
|
|
t("error.e2ee_unsupported_description"),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating an unknown issue occurred during a call operation.
|
|
*/
|
|
export class UnknownCallError extends ElementCallError {
|
|
/**
|
|
* Creates an instance of UnknownCallError.
|
|
* @param error - The underlying error that caused the unknown issue.
|
|
*/
|
|
public constructor(error: Error) {
|
|
super(
|
|
t("error.generic"),
|
|
ErrorCode.UNKNOWN_ERROR,
|
|
ErrorCategory.UNKNOWN,
|
|
undefined,
|
|
// Properly set it as a cause for a better reporting on sentry
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating a failure to obtain an OpenID token.
|
|
*/
|
|
export class FailToGetOpenIdToken extends ElementCallError {
|
|
/**
|
|
* Creates an instance of FailToGetOpenIdToken.
|
|
* @param error - The underlying error that caused the failure.
|
|
*/
|
|
public constructor(error: Error) {
|
|
super(
|
|
t("error.generic"),
|
|
ErrorCode.OPEN_ID_ERROR,
|
|
ErrorCategory.CONFIGURATION_ISSUE,
|
|
undefined,
|
|
// Properly set it as a cause for a better reporting on sentry
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
export class NoMatrix2AuthorizationService extends ElementCallError {
|
|
/**
|
|
* Creates an instance of NoMatrix2_0AuthorizationService.
|
|
* @param error - The underlying error that caused the failure.
|
|
*/
|
|
public constructor(error: Error) {
|
|
super(
|
|
t("error.generic"),
|
|
ErrorCode.NO_MATRIX_2_AUTHORIZATION_SERVICE,
|
|
ErrorCategory.CONFIGURATION_ISSUE,
|
|
t("error.no_matrix_2_authorization_service"),
|
|
// Properly set it as a cause for a better reporting on sentry
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating a failure to start publishing on a LiveKit connection.
|
|
*/
|
|
export class FailToStartLivekitConnection extends ElementCallError {
|
|
/**
|
|
* Creates an instance of FailToStartLivekitConnection.
|
|
* @param e - An optional error message providing additional context.
|
|
*/
|
|
public constructor(e?: string) {
|
|
super(
|
|
t("error.failed_to_start_livekit"),
|
|
ErrorCode.FAILED_TO_START_LIVEKIT,
|
|
ErrorCategory.NETWORK_CONNECTIVITY,
|
|
e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating that a LiveKit's server has hit its track limits.
|
|
*/
|
|
/**
|
|
* [lotus] The SFU token service answered 403 with a reason we can show verbatim
|
|
* — the voice-limit guard says things like "This voice channel is full." or
|
|
* "You don't have permission to share your screen here." Without this the user
|
|
* only ever saw "Something went wrong (OPEN_ID_ERROR)".
|
|
*/
|
|
export class SFUTokenRefusedError extends ElementCallError {
|
|
public constructor(reason: string, cause?: Error) {
|
|
super(
|
|
t("error.sfu_token_refused"),
|
|
ErrorCode.SFU_TOKEN_REFUSED,
|
|
ErrorCategory.CONFIGURATION_ISSUE,
|
|
reason,
|
|
cause,
|
|
);
|
|
}
|
|
}
|
|
|
|
export class InsufficientCapacityError extends ElementCallError {
|
|
public constructor() {
|
|
super(
|
|
t("error.insufficient_capacity"),
|
|
ErrorCode.INSUFFICIENT_CAPACITY_ERROR,
|
|
ErrorCategory.UNKNOWN,
|
|
t("error.insufficient_capacity_description"),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating that room creation is restricted by the SFU.
|
|
* Only authorized users can create rooms, so the room must exist before connecting (done by the auth jwt service)
|
|
*/
|
|
export class SFURoomCreationRestrictedError extends ElementCallError {
|
|
public constructor() {
|
|
super(
|
|
t("error.room_creation_restricted"),
|
|
ErrorCode.SFU_ERROR,
|
|
ErrorCategory.CONFIGURATION_ISSUE,
|
|
t("error.room_creation_restricted_description"),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error indicating that the SFU peer-to-peer connection timed out.
|
|
*/
|
|
export class PeerConnectionTimeoutError extends ElementCallError {
|
|
public constructor() {
|
|
super(
|
|
t("error.peer_connection_timeout"),
|
|
ErrorCode.SFU_ERROR,
|
|
ErrorCategory.NETWORK_CONNECTIVITY,
|
|
);
|
|
this.localisedMessageKey = i18nKey(
|
|
"error.peer_connection_timeout_description",
|
|
);
|
|
this.localisedMessageValues = {
|
|
linkUrl:
|
|
"https://docs.element.io/latest/element-server-suite-pro/configuring-components/configuring-matrix-rtc/#sfu-connectivity-troubleshooting",
|
|
};
|
|
}
|
|
}
|
|
|
|
export class LivekitConnectionError extends ElementCallError {
|
|
public constructor(cause: ConnectionError) {
|
|
super(
|
|
t("error.livekit_connection_error"),
|
|
ErrorCode.SFU_ERROR,
|
|
ErrorCategory.NETWORK_CONNECTIVITY,
|
|
);
|
|
// [lotus] "An error occurred while connecting to the Livekit server
|
|
// (Reason: ServerUnreachable)" left people with nothing to try (Gitea
|
|
// element-call: a friend retried for ages while Element worked). Say what
|
|
// it means and what to do; keep the raw reason under Technical details.
|
|
this.localisedMessageKey =
|
|
cause.reason === ConnectionErrorReason.NotAllowed
|
|
? i18nKey("error.livekit_not_allowed_description")
|
|
: i18nKey("error.livekit_unreachable_description");
|
|
this.localisedMessageValues = { reason: cause.reasonName };
|
|
this.technicalDetails = [
|
|
`Reason: ${cause.reasonName}`,
|
|
cause.status ? `Status: ${cause.status}` : null,
|
|
cause.message,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
}
|
|
}
|