feat(errors): Report livekit as such rather than like unknown errors

This commit is contained in:
Valere
2026-06-04 11:54:42 +02:00
parent 5f257da4f6
commit e7d37f87bb
6 changed files with 1897 additions and 3 deletions

View File

@@ -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:</1> <2>{{ reason }}</2>).",
"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</0> 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:</0> <1>{{ errorCode }}</1>). Please contact your server admin."

View File

@@ -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(
<BrowserRouter>
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
);
// 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(
<BrowserRouter>
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
);
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();
});
});

View File

@@ -92,7 +92,26 @@ const ErrorPage: FC<ErrorPageProps> = ({
widget={widget}
>
<p>
{error.localisedMessage ?? (
{error.localisedMessageKey ? (
<Trans
// @ts-expect-error - Dynamic i18nKey from error object
i18nKey={error.localisedMessageKey}
values={error.localisedMessageValues}
components={[
<a
href={String(error.localisedMessageValues?.linkUrl || "#")}
target="_blank"
rel="noopener noreferrer"
>
{/* Content injected by Trans component */}
</a>,
<b />,
<code />,
]}
/>
) : error.localisedMessage ? (
error.localisedMessage
) : (
<Trans
i18nKey="error.unexpected_ec_error"
components={[<b />, <code />]}

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,9 @@ import {
} from "@livekit/components-core";
import {
ConnectionError,
type Room as LivekitRoom,
ConnectionErrorReason,
type RemoteParticipant,
type Room as LivekitRoom,
} from "livekit-client";
import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc";
import { BehaviorSubject, map } from "rxjs";
@@ -30,6 +31,8 @@ import { type ObservableScope } from "../../ObservableScope.ts";
import {
ElementCallError,
InsufficientCapacityError,
LivekitConnectionError,
PeerConnectionTimeoutError,
SFURoomCreationRestrictedError,
UnknownCallError,
} from "../../../utils/errors.ts";
@@ -248,6 +251,13 @@ export class Connection {
// In the first case there will not be a 404, so we are in the second case.
throw new SFURoomCreationRestrictedError();
}
if (e.reason === ConnectionErrorReason.Timeout) {
// Unabled to establish peer connection within the timeout
throw new PeerConnectionTimeoutError();
}
throw new LivekitConnectionError(e);
}
throw e;
}

View File

@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { t } from "i18next";
import { type ConnectionError } from "livekit-client";
export enum ErrorCode {
/**
@@ -43,6 +44,10 @@ export class ElementCallError extends Error {
public localisedMessage?: string;
public localisedTitle: string;
// Alternative to localisedMessage, for rich error rendering
public localisedMessageKey?: string;
public localisedMessageValues?: Record<string, string>;
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 };
}
}