Skip to content

Commit 66899f3

Browse files
authored
Merge pull request #3027 from element-hq/valere/missing_livekit_url_config
Error Handling: gracefully handle missing MatrixRTC focus configuration
2 parents b355615 + f38adf1 commit 66899f3

File tree

7 files changed

+229
-21
lines changed

7 files changed

+229
-21
lines changed

locales/en/app.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
},
7575
"disconnected_banner": "Connectivity to the server has been lost.",
7676
"error": {
77+
"call_is_not_supported": "Call is not supported",
7778
"call_not_found": "Call not found",
7879
"call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one</1>.</0>",
7980
"connection_lost": "Connection lost",
@@ -84,8 +85,10 @@
8485
"generic_description": "Submitting debug logs will help us track down the problem.",
8586
"insufficient_capacity": "Insufficient capacity",
8687
"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.",
88+
"matrix_rtc_focus_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
8789
"open_elsewhere": "Opened in another tab",
88-
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page."
90+
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
91+
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server admin."
8992
},
9093
"group_call_loader": {
9194
"banned_body": "You have been banned from the room.",

src/RichError.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE in the repository root for full details.
66
*/
77

8-
import { type FC, type ReactNode } from "react";
9-
import { useTranslation } from "react-i18next";
8+
import { Trans, useTranslation } from "react-i18next";
109
import {
10+
ErrorIcon,
1111
HostIcon,
1212
PopOutIcon,
1313
} from "@vector-im/compound-design-tokens/assets/web/icons";
1414

15+
import type { ComponentType, FC, ReactNode, SVGAttributes } from "react";
1516
import { ErrorView } from "./ErrorView";
17+
import { type ElementCallError, ErrorCategory } from "./utils/errors.ts";
1618

1719
/**
1820
* An error consisting of a terse message to be logged to the console and a
@@ -65,3 +67,46 @@ export class InsufficientCapacityError extends RichError {
6567
super("Insufficient server capacity", <InsufficientCapacity />);
6668
}
6769
}
70+
71+
type ECErrorProps = {
72+
error: ElementCallError;
73+
};
74+
75+
const GenericECError: FC<{ error: ElementCallError }> = ({
76+
error,
77+
}: ECErrorProps) => {
78+
const { t } = useTranslation();
79+
80+
let title: string;
81+
let icon: ComponentType<SVGAttributes<SVGElement>>;
82+
switch (error.category) {
83+
case ErrorCategory.CONFIGURATION_ISSUE:
84+
title = t("error.call_is_not_supported");
85+
icon = HostIcon;
86+
break;
87+
default:
88+
title = t("error.generic");
89+
icon = ErrorIcon;
90+
}
91+
return (
92+
<ErrorView Icon={icon} title={title}>
93+
<p>
94+
{error.localisedMessage ?? (
95+
<Trans
96+
i18nKey="error.unexpected_ec_error"
97+
components={[<b />, <code />]}
98+
values={{ errorCode: error.code }}
99+
/>
100+
)}
101+
</p>
102+
</ErrorView>
103+
);
104+
};
105+
106+
export class ElementCallRichError extends RichError {
107+
public ecError: ElementCallError;
108+
public constructor(ecError: ElementCallError) {
109+
super(ecError.message, <GenericECError error={ecError} />);
110+
this.ecError = ecError;
111+
}
112+
}

src/room/GroupCallView.tsx

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
} from "react";
1717
import { type MatrixClient } from "matrix-js-sdk/src/client";
1818
import {
19-
Room,
2019
isE2EESupported as isE2EESupportedBrowser,
20+
Room,
2121
} from "livekit-client";
2222
import { logger } from "matrix-js-sdk/src/logger";
2323
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
@@ -44,7 +44,7 @@ import { CallEndedView } from "./CallEndedView";
4444
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
4545
import { useProfile } from "../profile/useProfile";
4646
import { findDeviceByName } from "../utils/media";
47-
import { ActiveCall, ConnectionLostError } from "./InCallView";
47+
import { ActiveCall } from "./InCallView";
4848
import { MUTE_PARTICIPANT_COUNT, type MuteStates } from "./MuteStates";
4949
import { useMediaDevices } from "../livekit/MediaDevicesContext";
5050
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
@@ -61,6 +61,13 @@ import { callEventAudioSounds } from "./CallEventAudioRenderer";
6161
import { useLatest } from "../useLatest";
6262
import { usePageTitle } from "../usePageTitle";
6363
import { ErrorView } from "../ErrorView";
64+
import {
65+
ConnectionLostError,
66+
ElementCallError,
67+
ErrorCategory,
68+
ErrorCode,
69+
} from "../utils/errors.ts";
70+
import { ElementCallRichError } from "../RichError.tsx";
6471

6572
declare global {
6673
interface Window {
@@ -165,6 +172,28 @@ export const GroupCallView: FC<Props> = ({
165172
const latestDevices = useLatest(deviceContext);
166173
const latestMuteStates = useLatest(muteStates);
167174

175+
const enterRTCSessionOrError = async (
176+
rtcSession: MatrixRTCSession,
177+
perParticipantE2EE: boolean,
178+
): Promise<void> => {
179+
try {
180+
await enterRTCSession(rtcSession, perParticipantE2EE);
181+
} catch (e) {
182+
if (e instanceof ElementCallError) {
183+
// e.code === ErrorCode.MISSING_LIVE_KIT_SERVICE_URL)
184+
setEnterRTCError(e);
185+
} else {
186+
logger.error(`Unknown Error while entering RTC session`, e);
187+
const error = new ElementCallError(
188+
e instanceof Error ? e.message : "Unknown error",
189+
ErrorCode.UNKNOWN_ERROR,
190+
ErrorCategory.UNKNOWN,
191+
);
192+
setEnterRTCError(error);
193+
}
194+
}
195+
};
196+
168197
useEffect(() => {
169198
const defaultDeviceSetup = async ({
170199
audioInput,
@@ -214,7 +243,7 @@ export const GroupCallView: FC<Props> = ({
214243
await defaultDeviceSetup(
215244
ev.detail.data as unknown as JoinCallData,
216245
);
217-
await enterRTCSession(rtcSession, perParticipantE2EE);
246+
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
218247
widget.api.transport.reply(ev.detail, {});
219248
})().catch((e) => {
220249
logger.error("Error joining RTC session", e);
@@ -227,13 +256,13 @@ export const GroupCallView: FC<Props> = ({
227256
} else {
228257
// No lobby and no preload: we enter the rtc session right away
229258
(async (): Promise<void> => {
230-
await enterRTCSession(rtcSession, perParticipantE2EE);
259+
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
231260
})().catch((e) => {
232261
logger.error("Error joining RTC session", e);
233262
});
234263
}
235264
} else {
236-
void enterRTCSession(rtcSession, perParticipantE2EE);
265+
void enterRTCSessionOrError(rtcSession, perParticipantE2EE);
237266
}
238267
}
239268
}, [
@@ -247,6 +276,9 @@ export const GroupCallView: FC<Props> = ({
247276
]);
248277

249278
const [left, setLeft] = useState(false);
279+
const [enterRTCError, setEnterRTCError] = useState<ElementCallError | null>(
280+
null,
281+
);
250282
const navigate = useNavigate();
251283

252284
const onLeave = useCallback(
@@ -347,8 +379,8 @@ export const GroupCallView: FC<Props> = ({
347379
const onReconnect = useCallback(() => {
348380
setLeft(false);
349381
resetError();
350-
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
351-
logger.error("Error re-entering RTC session on reconnect", e);
382+
enterRTCSessionOrError(rtcSession, perParticipantE2EE).catch((e) => {
383+
logger.error("Error re-entering RTC session", e);
352384
});
353385
}, [resetError]);
354386

@@ -397,7 +429,9 @@ export const GroupCallView: FC<Props> = ({
397429
client={client}
398430
matrixInfo={matrixInfo}
399431
muteStates={muteStates}
400-
onEnter={() => void enterRTCSession(rtcSession, perParticipantE2EE)}
432+
onEnter={() =>
433+
void enterRTCSessionOrError(rtcSession, perParticipantE2EE)
434+
}
401435
confineToRoom={confineToRoom}
402436
hideHeader={hideHeader}
403437
participantCount={participantCount}
@@ -407,7 +441,14 @@ export const GroupCallView: FC<Props> = ({
407441
);
408442

409443
let body: ReactNode;
410-
if (isJoined) {
444+
if (enterRTCError) {
445+
// If an ElementCallError was recorded, then create a component that will fail to render and throw
446+
// an ElementCallRichError error. This will then be handled by the ErrorBoundary component.
447+
const ErrorComponent = (): ReactNode => {
448+
throw new ElementCallRichError(enterRTCError);
449+
};
450+
body = <ErrorComponent />;
451+
} else if (isJoined) {
411452
body = (
412453
<>
413454
{shareModal}

src/room/InCallView.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,12 @@ import {
9797
useSetting,
9898
} from "../settings/settings";
9999
import { ReactionsReader } from "../reactions/ReactionsReader";
100+
import { ConnectionLostError } from "../utils/errors.ts";
100101

101102
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
102103

103104
const maxTapDurationMs = 400;
104105

105-
export class ConnectionLostError extends Error {}
106-
107106
export interface ActiveCallProps
108107
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
109108
e2eeSystem: EncryptionSystem;

src/rtcSessionHelpers.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import EventEmitter from "events";
1313
import { enterRTCSession, leaveRTCSession } from "../src/rtcSessionHelpers";
1414
import { mockConfig } from "./utils/test";
1515
import { ElementWidgetActions, widget } from "./widget";
16+
import { ErrorCode } from "./utils/errors.ts";
1617

1718
const actualWidget = await vi.hoisted(async () => vi.importActual("./widget"));
1819
vi.mock("./widget", () => ({
@@ -137,3 +138,50 @@ test("leaveRTCSession doesn't close the widget on a fatal error", async () => {
137138
expect.anything(),
138139
);
139140
});
141+
142+
test("It fails with configuration error if no live kit url config is set in fallback", async () => {
143+
mockConfig({});
144+
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
145+
146+
const mockedSession = vi.mocked({
147+
room: {
148+
roomId: "roomId",
149+
client: {
150+
getDomain: vi.fn().mockReturnValue("example.org"),
151+
},
152+
},
153+
memberships: [],
154+
getFocusInUse: vi.fn(),
155+
joinRoomSession: vi.fn(),
156+
}) as unknown as MatrixRTCSession;
157+
158+
await expect(enterRTCSession(mockedSession, false)).rejects.toThrowError(
159+
expect.objectContaining({ code: ErrorCode.MISSING_MATRIX_RTC_FOCUS }),
160+
);
161+
});
162+
163+
test("It should not fail with configuration error if homeserver config has livekit url but not fallback", async () => {
164+
mockConfig({});
165+
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({
166+
"org.matrix.msc4143.rtc_foci": [
167+
{
168+
type: "livekit",
169+
livekit_service_url: "http://my-well-known-service-url.com",
170+
},
171+
],
172+
});
173+
174+
const mockedSession = vi.mocked({
175+
room: {
176+
roomId: "roomId",
177+
client: {
178+
getDomain: vi.fn().mockReturnValue("example.org"),
179+
},
180+
},
181+
memberships: [],
182+
getFocusInUse: vi.fn(),
183+
joinRoomSession: vi.fn(),
184+
}) as unknown as MatrixRTCSession;
185+
186+
await enterRTCSession(mockedSession, false);
187+
});

src/rtcSessionHelpers.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ Please see LICENSE in the repository root for full details.
88
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
99
import { logger } from "matrix-js-sdk/src/logger";
1010
import {
11-
type LivekitFocus,
12-
type LivekitFocusActive,
1311
isLivekitFocus,
1412
isLivekitFocusConfig,
13+
type LivekitFocus,
14+
type LivekitFocusActive,
1515
} from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
1616
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
1717

1818
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
1919
import { Config } from "./config/Config";
20-
import { ElementWidgetActions, type WidgetHelpers, widget } from "./widget";
20+
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
21+
import { MatrixRTCFocusMissingError } from "./utils/errors.ts";
2122

2223
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
2324

@@ -80,10 +81,7 @@ async function makePreferredLivekitFoci(
8081
}
8182

8283
if (preferredFoci.length === 0)
83-
throw new Error(
84-
`No livekit_service_url is configured so we could not create a focus.
85-
Currently we skip computing a focus based on other users in the room.`,
86-
);
84+
throw new MatrixRTCFocusMissingError(domain ?? "");
8785
return Promise.resolve(preferredFoci);
8886

8987
// TODO: we want to do something like this:

0 commit comments

Comments
 (0)