mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-24 19:19:21 +00:00
feat(errors): Report livekit as such rather than like unknown errors
This commit is contained in:
@@ -108,12 +108,16 @@
|
|||||||
"generic_description": "Submitting debug logs will help us track down the problem.",
|
"generic_description": "Submitting debug logs will help us track down the problem.",
|
||||||
"insufficient_capacity": "Insufficient capacity",
|
"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.",
|
"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 }}).",
|
"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": "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.",
|
"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": "Opened in another tab",
|
||||||
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
|
"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": "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.",
|
"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."
|
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server admin."
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { ConnectionError } from "livekit-client";
|
||||||
import { MatrixError } from "matrix-js-sdk";
|
import { MatrixError } from "matrix-js-sdk";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +29,9 @@ import {
|
|||||||
type ElementCallError,
|
type ElementCallError,
|
||||||
FailToGetOpenIdToken,
|
FailToGetOpenIdToken,
|
||||||
InsufficientCapacityError,
|
InsufficientCapacityError,
|
||||||
|
LivekitConnectionError,
|
||||||
MatrixRTCTransportMissingError,
|
MatrixRTCTransportMissingError,
|
||||||
|
PeerConnectionTimeoutError,
|
||||||
UnknownCallError,
|
UnknownCallError,
|
||||||
} from "../utils/errors.ts";
|
} from "../utils/errors.ts";
|
||||||
import { mockConfig } from "../utils/test.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)
|
// Technical details should not be present (ConnectionLostError has no cause)
|
||||||
expect(screen.queryByText("Technical details")).not.toBeInTheDocument();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -92,7 +92,26 @@ const ErrorPage: FC<ErrorPageProps> = ({
|
|||||||
widget={widget}
|
widget={widget}
|
||||||
>
|
>
|
||||||
<p>
|
<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
|
<Trans
|
||||||
i18nKey="error.unexpected_ec_error"
|
i18nKey="error.unexpected_ec_error"
|
||||||
components={[<b />, <code />]}
|
components={[<b />, <code />]}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,9 @@ import {
|
|||||||
} from "@livekit/components-core";
|
} from "@livekit/components-core";
|
||||||
import {
|
import {
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
type Room as LivekitRoom,
|
ConnectionErrorReason,
|
||||||
type RemoteParticipant,
|
type RemoteParticipant,
|
||||||
|
type Room as LivekitRoom,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc";
|
import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { BehaviorSubject, map } from "rxjs";
|
import { BehaviorSubject, map } from "rxjs";
|
||||||
@@ -30,6 +31,8 @@ import { type ObservableScope } from "../../ObservableScope.ts";
|
|||||||
import {
|
import {
|
||||||
ElementCallError,
|
ElementCallError,
|
||||||
InsufficientCapacityError,
|
InsufficientCapacityError,
|
||||||
|
LivekitConnectionError,
|
||||||
|
PeerConnectionTimeoutError,
|
||||||
SFURoomCreationRestrictedError,
|
SFURoomCreationRestrictedError,
|
||||||
UnknownCallError,
|
UnknownCallError,
|
||||||
} from "../../../utils/errors.ts";
|
} 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.
|
// In the first case there will not be a 404, so we are in the second case.
|
||||||
throw new SFURoomCreationRestrictedError();
|
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;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { type ConnectionError } from "livekit-client";
|
||||||
|
|
||||||
export enum ErrorCode {
|
export enum ErrorCode {
|
||||||
/**
|
/**
|
||||||
@@ -43,6 +44,10 @@ export class ElementCallError extends Error {
|
|||||||
public localisedMessage?: string;
|
public localisedMessage?: string;
|
||||||
public localisedTitle: string;
|
public localisedTitle: string;
|
||||||
|
|
||||||
|
// Alternative to localisedMessage, for rich error rendering
|
||||||
|
public localisedMessageKey?: string;
|
||||||
|
public localisedMessageValues?: Record<string, string>;
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
localisedTitle: string,
|
localisedTitle: string,
|
||||||
code: ErrorCode,
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user