From d0bfef06829671aab674d78844979983c5fbf35e Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 6 May 2026 09:18:27 +0200 Subject: [PATCH] Improve error messages for sfu auth problems --- locales/en/app.json | 1 + src/ErrorView.module.css | 18 +++++ src/livekit/openIDSFU.test.ts | 29 ++++++-- src/livekit/openIDSFU.ts | 10 ++- src/room/GroupCallErrorBoundary.test.tsx | 68 +++++++++++++++++++ src/room/GroupCallErrorBoundary.tsx | 15 ++++ .../remoteMembers/Connection.test.ts | 7 +- 7 files changed, 136 insertions(+), 12 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 5398930f..08e5f0d2 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -236,6 +236,7 @@ "stop_video_button_label": "Stop video", "submitting": "Submitting…", "switch_camera": "Switch camera", + "technical_details": "", "unauthenticated_view_body": "Not registered yet? <2>Create an account", "unauthenticated_view_login_button": "Login to your account", "unauthenticated_view_ssla_caption": "By clicking \"Go\", you agree to our <2>Software and Services License Agreement (SSLA)", diff --git a/src/ErrorView.module.css b/src/ErrorView.module.css index bd68f5e3..e37079ca 100644 --- a/src/ErrorView.module.css +++ b/src/ErrorView.module.css @@ -20,3 +20,21 @@ color: var(--cpd-color-text-secondary); text-align: center; } + +.technicalDetails { + margin-top: var(--cpd-space-1x); +} + +.technicalDetailsSummary { + cursor: pointer; + font-weight: bold; +} + +.technicalDetailsPre { + margin-top: var(--cpd-space-2x); + padding: var(--cpd-space-2x); + background-color: var(--cpd-color-bg-subtle-secondary); + overflow: auto; + font-size: var(--cpd-font-size-body-sm); + white-space: pre-wrap; +} diff --git a/src/livekit/openIDSFU.test.ts b/src/livekit/openIDSFU.test.ts index d269569f..2ddb6c95 100644 --- a/src/livekit/openIDSFU.test.ts +++ b/src/livekit/openIDSFU.test.ts @@ -15,6 +15,7 @@ import { vitest, } from "vitest"; import fetchMock from "fetch-mock"; +import { MatrixError } from "matrix-js-sdk"; import { getSFUConfigWithOpenID, type OpenIDClientParts } from "./openIDSFU"; import { testJWTToken } from "../utils/test-fixtures"; @@ -63,7 +64,10 @@ describe("getSFUConfigWithOpenID", () => { fetchMock.post("https://sfu.example.org/sfu/get", () => { return { status: 500, - body: { error: "Test failure" }, + body: { + errcode: "M_LOOKUP_FAILED", + error: "Failed to look up user info from homeserver", + }, }; }); try { @@ -75,9 +79,12 @@ describe("getSFUConfigWithOpenID", () => { ); } catch (ex: unknown) { expect(ex).toBeInstanceOf(FailToGetOpenIdToken); - expect((ex as FailToGetOpenIdToken).cause).toEqual( - new Error("SFU Config fetch failed with status code 500"), + expect((ex as FailToGetOpenIdToken).cause).toBeInstanceOf(MatrixError); + const mxError = (ex as Error).cause as MatrixError; + expect(mxError.message).toEqual( + "MatrixError: [500] Failed to look up user info from homeserver", ); + void (await fetchMock.flush()); return; } @@ -181,13 +188,19 @@ describe("getSFUConfigWithOpenID", () => { fetchMock.post("https://sfu.example.org/get_token", () => { return { status: 500, - body: { error: "Test failure" }, + body: { + errcode: "M_LOOKUP_FAILED", + error: "Failed to look up user info from homeserver", + }, }; }); fetchMock.post("https://sfu.example.org/sfu/get", () => { return { status: 500, - body: { error: "Test failure" }, + body: { + errcode: "M_LOOKUP_FAILED", + error: "Failed to look up user info from homeserver", + }, }; }); try { @@ -203,8 +216,10 @@ describe("getSFUConfigWithOpenID", () => { ); } catch (ex) { expect(ex).toBeInstanceOf(FailToGetOpenIdToken); - expect((ex as FailToGetOpenIdToken).cause).toEqual( - new Error("SFU Config fetch failed with status code 500"), + expect((ex as FailToGetOpenIdToken).cause).toBeInstanceOf(MatrixError); + const mxError = (ex as Error).cause as MatrixError; + expect(mxError.message).toEqual( + "MatrixError: [500] Failed to look up user info from homeserver", ); void (await fetchMock.flush()); } diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 2d6c45b6..00cf69b1 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -5,7 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk"; +import { + type IOpenIDToken, + type MatrixClient, + parseErrorResponse, +} from "matrix-js-sdk"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type Logger } from "matrix-js-sdk/lib/logger"; @@ -248,7 +252,7 @@ async function getLiveKitJWT( }); if (!res.ok) { - throw new Error("SFU Config fetch failed with status code " + res.status); + throw parseErrorResponse(res, await res.text()); } return await res.json(); } @@ -308,7 +312,7 @@ export async function getLiveKitJWTWithDelayDelegation( if (res.status === 404) { throw new NotSupportedError(msg); } else { - throw new Error(msg); + throw parseErrorResponse(res, await res.text()); } } return await res.json(); diff --git a/src/room/GroupCallErrorBoundary.test.tsx b/src/room/GroupCallErrorBoundary.test.tsx index 86921710..a3044f5a 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 { MatrixError } from "matrix-js-sdk"; import { type CallErrorRecoveryAction, @@ -25,6 +26,7 @@ import { ConnectionLostError, E2EENotSupportedError, type ElementCallError, + FailToGetOpenIdToken, InsufficientCapacityError, MatrixRTCTransportMissingError, UnknownCallError, @@ -252,3 +254,69 @@ test("should have a close button in widget mode", async () => { ); expect(mockWidget.api.transport.stop).toHaveBeenCalled(); }); + +test("should show technical details when error has a matrixError cause", async () => { + const underlyingError = new MatrixError( + { + errcode: "M_LOOKUP_FAILED", + error: "Failed to look up user info from homeserver", + }, + 500, + "https://matrix-rtc.m.localhost/livekit/jwt/sfu/get", + ); + const error = new FailToGetOpenIdToken(underlyingError); + + const TestComponent = (): ReactNode => { + throw error; + }; + + render( + + + + + , + ); + + await screen.findByText("Something went wrong"); + + // Technical details should be present + const detailsElement = screen.getByText("Technical details"); + expect(detailsElement).toBeInTheDocument(); + + // Verify error details are shown + expect( + screen.getByText(/Failed to look up user info from homeserver/i, { + selector: "pre", + }), + ).toBeInTheDocument(); +}); + +test("should not show technical details when error has no matrix error cause", async () => { + const error = new ConnectionLostError(); + + const TestComponent = (): ReactNode => { + throw error; + }; + + render( + + + + + , + ); + + await screen.findByText("Connection lost"); + + // Technical details should not be present (ConnectionLostError has no cause) + expect(screen.queryByText("Technical details")).not.toBeInTheDocument(); +}); diff --git a/src/room/GroupCallErrorBoundary.tsx b/src/room/GroupCallErrorBoundary.tsx index ca407ed4..ab84678c 100644 --- a/src/room/GroupCallErrorBoundary.tsx +++ b/src/room/GroupCallErrorBoundary.tsx @@ -23,6 +23,7 @@ import { } from "@vector-im/compound-design-tokens/assets/web/icons"; import { Button } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/lib/logger"; +import { MatrixError } from "matrix-js-sdk"; import { ConnectionLostError, @@ -34,6 +35,7 @@ import { import { FullScreenView } from "../FullScreenView.tsx"; import { ErrorView } from "../ErrorView.tsx"; import { type WidgetHelpers } from "../widget.ts"; +import styles from "../ErrorView.module.css"; export type CallErrorRecoveryAction = "reconnect"; // | "retry" ; @@ -78,6 +80,9 @@ const ErrorPage: FC = ({ }); } + const technicalError = + error.cause instanceof MatrixError ? error.cause : null; + return ( = ({ /> )}

+ {technicalError ? ( +
+ + {t("technical_details")} + +
+              {technicalError.message}
+            
+
+ ) : null} {actions && actions.map((action, index) => (