Files
element-web/src/utils/ErrorUtils.tsx
Richard van der Hoff a365533367 ChangeRecoveryKey: error handling (#29262)
* CreateSecretStorageDialog: error handling

I'm fed up with setup operations in EW failing silently. Rather than leaving
the user with a mysteriously broken client, let's at least tell them that
something has gone wrong, so that they can report the issue and we can
investigate.

Obviously, showing an unactionable Error dialog is a last resort: ideally, we
should handle the error ourselves, or give the user actionable steps to resolve
the problem. But that takes significant design and engineering.

Just swallowing errors is the worst of all possible options.

* Fix typo in test name

* Improve test coverage
2025-02-14 16:44:34 +00:00

221 lines
7.4 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, _td, lookupString, type Tags, type TranslatedString, type TranslationKey } from "../languageHandler";
import SdkConfig from "../SdkConfig";
import { type ValidatedServerConfig } from "./ValidatedServerConfig";
import ExternalLink from "../components/views/elements/ExternalLink";
import Modal from "../Modal.tsx";
import ErrorDialog from "../components/views/dialogs/ErrorDialog.tsx";
export const resourceLimitStrings = {
"monthly_active_user": _td("error|mau"),
"hs_blocked": _td("error|hs_blocked"),
"": _td("error|resource_limits"),
};
export const adminContactStrings = {
"": _td("error|admin_contact"),
};
/**
* Produce a translated error message for a
* M_RESOURCE_LIMIT_EXCEEDED error
*
* @param {string} limitType The limit_type from the error
* @param {string} adminContact The admin_contact from the error
* @param {Object} strings Translatable string for different
* limit_type. Must include at least the empty string key
* which is the default. Strings may include an 'a' tag
* for the admin contact link.
* @param {Object} extraTranslations Extra translation substitution functions
* for any tags in the strings apart from 'a'
* @returns {*} Translated string or react component
*/
export function messageForResourceLimitError(
limitType: string | undefined,
adminContact: string | undefined,
strings: Record<string, TranslationKey>,
extraTranslations?: Tags,
): TranslatedString {
let errString = limitType ? strings[limitType] : undefined;
if (errString === undefined) errString = strings[""];
const linkSub = (sub: string): ReactNode => {
if (adminContact) {
return (
<a href={adminContact} target="_blank" rel="noreferrer noopener">
{sub}
</a>
);
} else {
return sub;
}
};
if (lookupString(errString).includes("<a>")) {
return _t(errString, {}, Object.assign({ a: linkSub }, extraTranslations));
} else {
return _t(errString, {}, extraTranslations!);
}
}
export function messageForSyncError(err: Error): ReactNode {
if (err instanceof MatrixError && err.errcode === "M_RESOURCE_LIMIT_EXCEEDED") {
const limitError = messageForResourceLimitError(
err.data.limit_type,
err.data.admin_contact,
resourceLimitStrings,
);
const adminContact = messageForResourceLimitError(
err.data.limit_type,
err.data.admin_contact,
adminContactStrings,
);
return (
<div>
<div>{limitError}</div>
<div>{adminContact}</div>
</div>
);
} else {
return <div>{_t("error|sync")}</div>;
}
}
export function messageForLoginError(
err: MatrixError,
serverConfig: Pick<ValidatedServerConfig, "hsName" | "hsUrl">,
): ReactNode {
if (err.errcode === "M_RESOURCE_LIMIT_EXCEEDED") {
const errorTop = messageForResourceLimitError(
err.data.limit_type,
err.data.admin_contact,
resourceLimitStrings,
);
const errorDetail = messageForResourceLimitError(
err.data.limit_type,
err.data.admin_contact,
adminContactStrings,
);
return (
<div>
<div>{errorTop}</div>
<div className="mx_Login_smallError">{errorDetail}</div>
</div>
);
} else if (err.httpStatus === 401 || err.httpStatus === 403) {
if (err.errcode === "M_USER_DEACTIVATED") {
return _t("auth|account_deactivated");
} else if (SdkConfig.get("disable_custom_urls")) {
return (
<div>
<div>{_t("auth|incorrect_credentials")}</div>
<div className="mx_Login_smallError">
{_t("auth|incorrect_credentials_detail", {
hs: serverConfig.hsName,
})}
</div>
</div>
);
} else {
return _t("auth|incorrect_credentials");
}
} else {
return messageForConnectionError(err, serverConfig);
}
}
export function messageForConnectionError(
err: Error,
serverConfig: Pick<ValidatedServerConfig, "hsName" | "hsUrl">,
): ReactNode {
let errorText = _t("error|connection");
if (err instanceof ConnectionError) {
if (
window.location.protocol === "https:" &&
(serverConfig.hsUrl.startsWith("http:") || !serverConfig.hsUrl.startsWith("http"))
) {
return (
<span>
{_t(
"error|mixed_content",
{},
{
a: (sub) => {
return (
<a
target="_blank"
rel="noreferrer noopener"
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
>
{sub}
</a>
);
},
},
)}
</span>
);
}
return (
<span>
{_t(
"error|tls",
{},
{
a: (sub) => (
<ExternalLink target="_blank" rel="noreferrer noopener" href={serverConfig.hsUrl}>
{sub}
</ExternalLink>
),
},
)}
</span>
);
} else if (err instanceof MatrixError) {
if (err.errcode) {
errorText += `(${err.errcode})`;
} else if (err.httpStatus) {
errorText += ` (HTTP ${err.httpStatus})`;
}
}
return errorText;
}
/**
* Utility for handling unexpected errors: pops up the error dialog.
*
* Example usage:
* ```
* try {
* /// complicated operation
* } catch (e) {
* logErrorAndShowErrorDialog("Failed complicated operation", e);
* }
* ```
*
* This isn't particularly intended to be pretty; rather it lets the user know that *something* has gone wrong so that
* they can report a bug. The general idea is that it's better to let the user know of a failure, even if they
* can't do anything about it, than it is to fail silently with the appearance of success.
*
* @param title - Title for the error dialog.
* @param error - The thrown error. Becomes the content of the error dialog.
*/
export function logErrorAndShowErrorDialog(title: string, error: any): void {
logger.error(`${title}:`, error);
Modal.createDialog(ErrorDialog, { title, description: `${error}` });
}