For discussion

This commit is contained in:
Hugh Nimmo-Smith
2025-01-20 17:25:46 +00:00
parent d901e21804
commit 4ca99f0e59
3 changed files with 209 additions and 105 deletions

View File

@@ -72,8 +72,12 @@
}, },
"disconnected_banner": "Connectivity to the server has been lost.", "disconnected_banner": "Connectivity to the server has been lost.",
"error": { "error": {
"auth_connection_failed_details": "<0>The application could not reach the call authentication service at <2>{{url}}</2>. If you are the server admin, check the network logs and make sure <5>lk-jwt-service</5> is listening at that address.</0>", "configuration_error": "Configuration error",
"auth_connection_rejected_details": "<0>The application connected to the call authentication service at <2>{{url}}</2>, but it responded with status code {{status}} ({{response}}). If you are the server admin, make sure <10>lk-jwt-service</10> is listening at that address and check the logs for more information.</0>", "configuration_error_description": "There is a configuration issues with the system. Please contact your administrator.",
"server_error": "Server error",
"server_error_description": "There is a server issue with the system. Please try again and contact your administrator if the problem persists.",
"network_error": "Network error",
"network_error_description": "There is a network issue with the system. Please check your network connection and try again. Alternatively try a different network if available.",
"call_not_found": "Call not found", "call_not_found": "Call not found",
"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>", "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>",
"connection_failed": "Connection failed", "connection_failed": "Connection failed",

View File

@@ -14,10 +14,11 @@ import {
} from "react"; } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { import {
ErrorIcon,
OfflineIcon, OfflineIcon,
PopOutIcon, PopOutIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons"; } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Button, Link } from "@vector-im/compound-web"; import { Button } from "@vector-im/compound-web";
import { ErrorView } from "./ErrorView"; import { ErrorView } from "./ErrorView";
@@ -58,40 +59,22 @@ export class OpenElsewhereError extends RichError {
} }
} }
interface AuthConnectionFailedProps { interface ConfigurationErrorViewProps {
livekitServiceUrl: string; children?: ReactNode;
} }
const AuthConnectionFailed: FC<AuthConnectionFailedProps> = ({ const ConfigurationErrorView: FC<ConfigurationErrorViewProps> = ({
livekitServiceUrl, children,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
const onShowDetailsClick = useCallback(() => setShowDetails(true), []); const onShowDetailsClick = useCallback(() => setShowDetails(true), []);
return ( return (
<ErrorView Icon={OfflineIcon} title={t("error.connection_failed")}> <ErrorView Icon={ErrorIcon} title={t("error.configuration_error")}>
<p>{t("error.connection_failed_description")}</p> <p>{t("error.configuration_error_description")}</p>
{showDetails ? ( {showDetails ? (
<Trans children
i18nKey="error.auth_connection_failed_details"
url={livekitServiceUrl}
>
<p>
The application could not reach the call authentication service at{" "}
<Link href={livekitServiceUrl} target="_blank">
{{ url: livekitServiceUrl } as unknown as ReactElement}
</Link>
. If you are the server admin, check the network logs and make sure{" "}
<Link
href="https://github.com/element-hq/lk-jwt-service/"
target="_blank"
>
lk-jwt-service
</Link>{" "}
is listening at that address.
</p>
</Trans>
) : ( ) : (
<Button kind="tertiary" onClick={onShowDetailsClick}> <Button kind="tertiary" onClick={onShowDetailsClick}>
{t("error.show_details")} {t("error.show_details")}
@@ -101,82 +84,186 @@ const AuthConnectionFailed: FC<AuthConnectionFailedProps> = ({
); );
}; };
export class AuthConnectionFailedError extends RichError { interface NetworkErrorViewProps {
public constructor(livekitServiceUrl: string, cause?: unknown) { children?: ReactNode;
}
const NetworkErrorView: FC<NetworkErrorViewProps> = ({ children }) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const onShowDetailsClick = useCallback(() => setShowDetails(true), []);
return (
<ErrorView Icon={OfflineIcon} title={t("error.network_error")}>
<p>{t("error.network_error_description")}</p>
{showDetails ? (
children
) : (
<Button kind="tertiary" onClick={onShowDetailsClick}>
{t("error.show_details")}
</Button>
)}
</ErrorView>
);
};
interface ServerErrorViewProps {
children?: ReactNode;
}
const ServerErrorView: FC<ServerErrorViewProps> = ({ children }) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const onShowDetailsClick = useCallback(() => setShowDetails(true), []);
return (
<ErrorView Icon={ErrorIcon} title={t("error.server_error")}>
<p>{t("error.server_error_description")}</p>
{showDetails ? (
children
) : (
<Button kind="tertiary" onClick={onShowDetailsClick}>
{t("error.show_details")}
</Button>
)}
</ErrorView>
);
};
export class ConfigurationError extends RichError {
public constructor(message: string, richMessage: ReactNode, cause?: unknown) {
super( super(
`Failed to connect to ${livekitServiceUrl}`, message,
<AuthConnectionFailed livekitServiceUrl={livekitServiceUrl} />, <ConfigurationErrorView>{richMessage}</ConfigurationErrorView>,
cause, cause,
); );
} }
} }
interface AuthConnectionRejectedProps { export class NetworkError extends RichError {
livekitServiceUrl: string; public constructor(message: string, richMessage: ReactNode, cause?: unknown) {
status: number; super(message, <NetworkErrorView>{richMessage}</NetworkErrorView>, cause);
response: string; }
} }
const AuthConnectionRejected: FC<AuthConnectionRejectedProps> = ({ export class ServerError extends RichError {
livekitServiceUrl, public constructor(message: string, richMessage: ReactNode, cause?: unknown) {
status, super(message, <ServerErrorView>{richMessage}</ServerErrorView>, cause);
response, }
}) => { }
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const onShowDetailsClick = useCallback(() => setShowDetails(true), []);
return ( export class URLBuildingConfigurationError extends ConfigurationError {
<ErrorView Icon={OfflineIcon} title={t("error.connection_failed")}> public constructor(baseUrl: string, cause?: unknown) {
<p>{t("error.connection_rejected_description")}</p> let message: string;
{showDetails ? ( if (cause instanceof Error) {
<Trans message = cause.message;
i18nKey="error.auth_connection_rejected_details" } else {
url={livekitServiceUrl} message = "Unknown error";
status={status} }
response={response}
>
<p>
The application connected to the call authentication service at{" "}
<Link href={livekitServiceUrl} target="_blank">
{{ url: livekitServiceUrl } as unknown as ReactElement}
</Link>
, but it responded with status code{" "}
{{ status } as unknown as ReactElement} (
{{ response } as unknown as ReactElement}). If you are the server
admin, make sure{" "}
<Link
href="https://github.com/element-hq/lk-jwt-service/"
target="_blank"
>
lk-jwt-service
</Link>{" "}
is listening at that address and check the logs for more
information.
</p>
</Trans>
) : (
<Button kind="tertiary" onClick={onShowDetailsClick}>
{t("error.show_details")}
</Button>
)}
</ErrorView>
);
};
export class AuthConnectionRejectedError extends RichError {
public constructor(
livekitServiceUrl: string,
status: number,
response: string,
) {
super( super(
`Failed to connect to ${livekitServiceUrl} (status ${status})`, `Unable to build URL based on: ${baseUrl}`,
<AuthConnectionRejected <Trans
livekitServiceUrl={livekitServiceUrl} i18nKey="error.invalid_url_details"
status={status} baseUrl={baseUrl}
response={response} message={message}
/>, >
<p>
The URL derived from{" "}
<code>{{ baseUrl } as unknown as ReactElement}</code> is not valid:{" "}
<pre>{{ message } as unknown as ReactElement}</pre>
</p>
</Trans>,
cause,
);
}
}
export class ResourceNotFoundConfigurationError extends ConfigurationError {
public constructor(url: URL) {
super(
`The server returned a 404 response for: ${url.href}`,
<Trans i18nKey="error.resource_not_found_details" url={url.href}>
<p>
The request to{" "}
<code>{{ url: url.href } as unknown as ReactElement}</code> returned a{" "}
<code>404</code> response.
</p>
</Trans>,
);
}
}
export class UnexpectedResponseCodeError extends ServerError {
public constructor(url: URL, status: number, response: string) {
super(
`Received unexpected response code from ${url.href}: ${status}`,
<Trans
i18nKey="error.unexpected_response_code_details"
url={url.href}
status={status}
response={response}
>
<p>
The application received an unexpected response from{" "}
<code>{{ url } as unknown as ReactElement}</code>. It received status
code <code>{{ status } as unknown as ReactElement}</code>:{" "}
<pre>{{ response } as unknown as ReactElement}</pre>.
</p>
</Trans>,
);
}
}
export class FetchError extends ServerError {
public constructor(url: URL, cause: unknown) {
let message: string;
if (cause instanceof Error) {
message = cause.message;
} else {
message = "Unknown error";
}
super(
`Failed to connect to ${url.href}: ${message}`,
<Trans
i18nKey="error.fetch_error_details"
url={url.href}
message={message}
>
<p>
The application received an unexpected response from{" "}
<code>{{ url: url.href } as unknown as ReactElement}</code>. It
received status code{" "}
<code>{{ message } as unknown as ReactElement}</code>.
</p>
</Trans>,
);
}
}
export class InvalidServerResponseError extends ServerError {
public constructor(url: URL, cause: unknown) {
let message: string;
if (cause instanceof Error) {
message = cause.message;
} else {
message = "Unknown error";
}
super(
`Invalid response received from ${url.href}: ${message}`,
<Trans
i18nKey="error.invalid_server_response_error_details"
url={url.href}
message={message}
>
<p>
The server at{" "}
<code>{{ url: url.href } as unknown as ReactElement}</code> returned
an invalid response:{" "}
<pre>{{ message } as unknown as ReactElement}</pre>
</p>
</Trans>,
); );
} }
} }

View File

@@ -13,8 +13,11 @@ import { type LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
import { useActiveLivekitFocus } from "../room/useActiveFocus"; import { useActiveLivekitFocus } from "../room/useActiveFocus";
import { import {
AuthConnectionFailedError, FetchError,
AuthConnectionRejectedError, InvalidServerResponseError,
ResourceNotFoundConfigurationError,
UnexpectedResponseCodeError,
URLBuildingConfigurationError,
} from "../RichError"; } from "../RichError";
export interface SFUConfig { export interface SFUConfig {
@@ -87,9 +90,18 @@ async function getLiveKitJWT(
roomName: string, roomName: string,
openIDToken: IOpenIDToken, openIDToken: IOpenIDToken,
): Promise<SFUConfig> { ): Promise<SFUConfig> {
let url: URL;
try {
// TODO: check that relative URLs are handled as expected by this
url = new URL("sfu/get", livekitServiceURL);
} catch (e) {
throw new URLBuildingConfigurationError(livekitServiceURL, e);
}
let res: Response; let res: Response;
try { try {
res = await fetch(livekitServiceURL + "/sfu/get", { res = await fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -101,16 +113,17 @@ async function getLiveKitJWT(
}), }),
}); });
} catch (e) { } catch (e) {
throw new AuthConnectionFailedError(livekitServiceURL, e); throw new FetchError(url, e);
} }
if (!res.ok) { if (!res.ok) {
throw res.status === 404 throw res.status === 404
? new AuthConnectionFailedError(livekitServiceURL) ? new ResourceNotFoundConfigurationError(url)
: new AuthConnectionRejectedError( : new UnexpectedResponseCodeError(url, res.status, await res.text());
livekitServiceURL, }
res.status,
await res.text(), try {
); return await res.json();
} catch (e) {
throw new InvalidServerResponseError(url, e);
} }
return await res.json();
} }