From e7d37f87bb0295d84315a0cf3befd0ddf3586663 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Jun 2026 11:54:42 +0200 Subject: [PATCH] feat(errors): Report livekit as such rather than like unknown errors --- locales/en/app.json | 6 +- src/room/GroupCallErrorBoundary.test.tsx | 99 + src/room/GroupCallErrorBoundary.tsx | 21 +- .../GroupCallErrorBoundary.test.tsx.snap | 1727 +++++++++++++++++ .../CallViewModel/remoteMembers/Connection.ts | 12 +- src/utils/errors.ts | 35 + 6 files changed, 1897 insertions(+), 3 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 48005adc..ca971fbc 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -108,12 +108,16 @@ "generic_description": "Submitting debug logs will help us track down the problem.", "insufficient_capacity": "Insufficient capacity", "insufficient_capacity_description": "The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.", + "livekit_connection_error": "Failed to connect to Livekit server", + "livekit_connection_error_description": "An error occurred while connecting to the Livekit server (<1>Reason: <2>{{ reason }}).", "matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).", "membership_manager": "Membership Manager Error", - "membership_manager_description": "The Membership Manager had to shut down. This is caused by many consequtive failed network requests.", + "membership_manager_description": "The Membership Manager had to shut down. This is caused by many consecutive failed network requests.", "no_matrix_2_authorization_service": "The authorization service for your media server (SFU) is out of date.", "open_elsewhere": "Opened in another tab", "open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.", + "peer_connection_timeout": "Connection timeout", + "peer_connection_timeout_description": "Connection to the media server timed out. Try switching to a different network or disabling your VPN. If the problem persists, see our <0>troubleshooting guide or contact your server administrator.", "room_creation_restricted": "Failed to create call", "room_creation_restricted_description": "Call creation might be restricted to authorized users only. Try again later, or contact your server admin if the problem persists.", "unexpected_ec_error": "An unexpected error occurred (<0>Error Code: <1>{{ errorCode }}). Please contact your server admin." diff --git a/src/room/GroupCallErrorBoundary.test.tsx b/src/room/GroupCallErrorBoundary.test.tsx index a3044f5a..891a9724 100644 --- a/src/room/GroupCallErrorBoundary.test.tsx +++ b/src/room/GroupCallErrorBoundary.test.tsx @@ -16,6 +16,7 @@ import { } from "react"; import { BrowserRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; +import { ConnectionError } from "livekit-client"; import { MatrixError } from "matrix-js-sdk"; import { @@ -28,7 +29,9 @@ import { type ElementCallError, FailToGetOpenIdToken, InsufficientCapacityError, + LivekitConnectionError, MatrixRTCTransportMissingError, + PeerConnectionTimeoutError, UnknownCallError, } from "../utils/errors.ts"; import { mockConfig } from "../utils/test.ts"; @@ -320,3 +323,99 @@ test("should not show technical details when error has no matrix error cause", a // Technical details should not be present (ConnectionLostError has no cause) expect(screen.queryByText("Technical details")).not.toBeInTheDocument(); }); + +describe("LiveKit ConnectionError variants", () => { + test.each([ + { + name: "notAllowed", + error: ConnectionError.notAllowed("Permission denied by server", 403), + expectedReason: "NotAllowed", + }, + { + name: "timeout", + error: ConnectionError.timeout("Connection timed out"), + expectedReason: "Timeout", + }, + { + name: "serverUnreachable", + error: ConnectionError.serverUnreachable("Server is unreachable", 503), + expectedReason: "ServerUnreachable", + }, + { + name: "serviceNotFound", + error: ConnectionError.serviceNotFound( + "RTC service not found", + "v0-rtc" as const, + ), + expectedReason: "ServiceNotFound", + }, + { + name: "internal", + error: ConnectionError.internal("Internal server error", { + status: 500, + statusText: "Internal Server Error", + }), + expectedReason: "InternalError", + }, + ])( + "should display LiveKit $name error correctly", + async ({ error, expectedReason }) => { + const TestComponent = (): ReactNode => { + throw new LivekitConnectionError(error); + }; + + const { asFragment } = render( + + + + + , + ); + + // Check title + await screen.findByText("Failed to connect to Livekit server"); + + // Check that reason is displayed in the description + expect(screen.getByText(/Reason:/i)).toBeInTheDocument(); + expect(screen.getByText(expectedReason)).toBeInTheDocument(); + + expect(asFragment()).toMatchSnapshot(); + }, + ); + + test("should link to troubleshoot guide when timeout error", async () => { + const error = new PeerConnectionTimeoutError(); + + const TestComponent = (): ReactNode => { + throw error; + }; + + const { asFragment } = render( + + + + + , + ); + + await screen.findByText("Connection timeout"); + + // Verify the link is present and has correct href + const link = screen.getByText("troubleshooting guide"); + expect(link).toHaveAttribute( + "href", + "https://docs.element.io/latest/element-server-suite-pro/configuring-components/configuring-matrix-rtc/#sfu-connectivity-troubleshooting", + ); + + // Snapshot the complete rendered error + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/room/GroupCallErrorBoundary.tsx b/src/room/GroupCallErrorBoundary.tsx index ab84678c..390a5a8c 100644 --- a/src/room/GroupCallErrorBoundary.tsx +++ b/src/room/GroupCallErrorBoundary.tsx @@ -92,7 +92,26 @@ const ErrorPage: FC = ({ widget={widget} >

- {error.localisedMessage ?? ( + {error.localisedMessageKey ? ( + + {/* Content injected by Trans component */} + , + , + , + ]} + /> + ) : error.localisedMessage ? ( + error.localisedMessage + ) : ( , ]} diff --git a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap index e7b38078..f20371fd 100644 --- a/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap +++ b/src/room/__snapshots__/GroupCallErrorBoundary.test.tsx.snap @@ -158,6 +158,1733 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = ` `; +exports[`LiveKit ConnectionError variants > should display LiveKit 'cancelled' error correctly 1`] = ` + +

+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + Cancelled + + ). +

+ +
+
+
+
+ +`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'internal' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + InternalError + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'leaveRequest' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + LeaveRequest + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'notAllowed' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + NotAllowed + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'serverUnreachable' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + ServerUnreachable + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'serviceNotFound' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + ServiceNotFound + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'timeout' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + Timeout + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should display LiveKit 'websocket' error correctly 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Failed to connect to Livekit server +

+

+ An error occurred while connecting to the Livekit server ( + + Reason: + + + + WebSocket + + ). +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should link to troubleshoot guide when timeout error 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Connection timeout +

+

+ Connection to the media server timed out. Try switching to a different network or disabling your VPN. If the problem persists, see our + + troubleshooting guide + + or contact your server administrator. +

+ +
+
+
+
+
+`; + +exports[`LiveKit ConnectionError variants > should render complete error message with snapshot 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Connection timeout +

+

+ Connection to the media server timed out. Try switching to a different network or disabling your VPN. If the problem persists, see our + + troubleshooting guide + + or contact your server administrator. +

+ +
+
+
+
+
+`; + +exports[`PeerConnectionTimeoutError with troubleshooting link > should render complete error message with snapshot 1`] = ` + +
+
+ +
+
+
+
+
+ +
+

+ Connection timeout +

+

+ Connection to the media server timed out. Try switching to a different network or disabling your VPN. If the problem persists, see our + + troubleshooting guide + + or contact your server administrator. +

+ +
+
+
+
+
+`; + exports[`should have a close button in widget mode 1`] = `
; + protected constructor( localisedTitle: string, code: ErrorCode, @@ -235,3 +240,33 @@ export class SFURoomCreationRestrictedError extends ElementCallError { ); } } + +/** + * 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 = "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, + ); + this.localisedMessageKey = "error.livekit_connection_error_description"; + this.localisedMessageValues = { reason: cause.reasonName }; + } +}