GramJS: Rework error handling (#6891)

This commit is contained in:
zubiden 2026-04-27 14:29:27 +02:00 committed by Alexander Zinchuk
parent 230e9797d4
commit e5b932b8ea
13 changed files with 97 additions and 131 deletions

View File

@ -2,6 +2,7 @@ import { Api as GramJs, errors } from '../../../lib/gramjs';
import type { RegularLangKey } from '../../../types/language';
import type { RegularLangFnParameters } from '../../../util/localization';
import type { ApiError } from '../../types';
import { DEBUG } from '../../../config';
import {
@ -39,6 +40,10 @@ const ERROR_KEYS: Record<string, RegularLangKey> = {
PASSKEY_CREDENTIAL_NOT_FOUND: 'ErrorPasskeyUnknown',
};
function resolveErrorKey(errorMessage: string) {
return ERROR_KEYS[errorMessage] || ERROR_KEYS[errorMessage.replace(/_\d+$/, '')];
}
export type MessageRepairContext = Pick<GramJs.TypeMessage, 'peerId' | 'id'>;
export type MediaRepairContext = MessageRepairContext;
@ -102,6 +107,20 @@ export function checkErrorType(error: unknown): error is Error {
return true;
}
export function buildApiError(error: Error): Pick<ApiError, 'message' | 'code' | 'hasErrorKey'> {
if (error instanceof errors.RPCError) {
return {
message: error.errorMessage,
code: error.code,
hasErrorKey: true,
};
}
return {
message: error.message,
};
}
export function wrapError<T extends Error>(error: T): WrappedError<T> {
let messageKey: RegularLangFnParameters | undefined;
@ -123,9 +142,12 @@ export function wrapError<T extends Error>(error: T): WrappedError<T> {
variables: { time: formatWait(error.seconds) },
};
} else if (error instanceof errors.RPCError) {
messageKey = {
key: ERROR_KEYS[error.errorMessage],
};
const key = resolveErrorKey(error.errorMessage);
if (key) {
messageKey = {
key,
};
}
}
if (!messageKey) {

View File

@ -82,7 +82,9 @@ import {
import {
addPhotoToLocalDb,
} from '../helpers/localDb';
import { checkErrorType, isChatFolder, wrapError } from '../helpers/misc';
import {
buildApiError, checkErrorType, isChatFolder, wrapError,
} from '../helpers/misc';
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import {
@ -1676,12 +1678,10 @@ export async function addChatMembers(chat: ApiChat, users: ApiUser[]) {
return addChatUsersResult.flat().filter(Boolean);
}
} catch (err: unknown) {
const message = err instanceof RPCError ? err.errorMessage : (err as Error).message;
const apiError = buildApiError(err as Error);
sendApiUpdate({
'@type': 'error',
error: {
message,
},
error: apiError,
});
}
return undefined;

View File

@ -1,5 +1,6 @@
import {
Api as GramJs,
errors,
sessions,
type Update,
} from '../../../lib/gramjs';
@ -40,6 +41,7 @@ import {
addWebPageMediaToLocalDb,
} from '../helpers/localDb';
import {
buildApiError,
isResponseUpdate, log,
} from '../helpers/misc';
import localDb, { clearLocalDb, type RepairInfo } from '../localDb';
@ -451,9 +453,9 @@ export async function fetchCurrentUser() {
}
export function dispatchErrorUpdate<T extends GramJs.AnyRequest>(err: Error, request: T) {
const message = err instanceof RPCError ? err.errorMessage : err.message;
const { message, code } = buildApiError(err);
const isSlowMode = message === 'FLOOD' && (
const isSlowMode = err instanceof errors.FloodError && (
request instanceof GramJs.messages.SendMessage
|| request instanceof GramJs.messages.SendMedia
|| request instanceof GramJs.messages.SendMultiMedia
@ -463,6 +465,7 @@ export function dispatchErrorUpdate<T extends GramJs.AnyRequest>(err: Error, req
'@type': 'error',
error: {
message,
code,
isSlowMode,
hasErrorKey: true,
},

View File

@ -109,6 +109,7 @@ import {
getEntityTypeById,
} from '../gramjsBuilders';
import {
buildApiError,
deserializeBytes,
resolveMessageApiChatId,
} from '../helpers/misc';
@ -237,7 +238,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
},
);
} catch (err: any) {
const { message } = err;
const { message, code } = buildApiError(err);
// When fetching messages for the bot @replies, there may be situations when the user was banned
// in the comment group or this group was deleted
@ -246,6 +247,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
'@type': 'error',
error: {
message,
code,
isSlowMode: false,
hasErrorKey: true,
},
@ -821,12 +823,12 @@ export async function editMessage({
console.warn(err);
}
const { message: messageErr } = err as Error;
const apiError = buildApiError(err as Error);
sendApiUpdate({
'@type': 'error',
error: {
message: messageErr,
...apiError,
hasErrorKey: true,
},
});
@ -887,12 +889,12 @@ export async function editTodo({
console.warn(err);
}
const { message: messageErr } = err as Error;
const apiError = buildApiError(err as Error);
sendApiUpdate({
'@type': 'error',
error: {
message: messageErr,
...apiError,
hasErrorKey: true,
},
});
@ -936,12 +938,12 @@ export async function appendTodoList({
console.warn(err);
}
const { message: messageErr } = err as Error;
const apiError = buildApiError(err as Error);
sendApiUpdate({
'@type': 'error',
error: {
message: messageErr,
...apiError,
hasErrorKey: true,
},
});

View File

@ -1,6 +1,6 @@
import type { Api } from '../../../lib/gramjs';
import type { TypedBroadcastChannel } from '../../../util/browser/multitab';
import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
import type { ApiError, ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
import type { LocalDb } from '../localDb';
import type { MethodArgs, MethodResponse, Methods } from '../methods/types';
import type { OriginPayload, ThenArg, WorkerMessageEvent } from './types';
@ -313,7 +313,7 @@ function subscribeToWorker(onUpdate: OnApiUpdate) {
export function handleMethodResponse(data: {
messageId: string;
response?: ThenArg<MethodResponse<keyof Methods>>;
error?: { message: string };
error?: Pick<ApiError, 'message' | 'code' | 'hasErrorKey'>;
}) {
const requestState = requestStates.get(data.messageId);
if (requestState) {

View File

@ -1,6 +1,6 @@
import type { DebugLevel } from '../../../util/debugConsole';
import type {
ApiInitialArgs, ApiUpdate,
ApiError, ApiInitialArgs, ApiUpdate,
} from '../../types';
import type { LocalDb } from '../localDb';
import type { MethodArgs, MethodResponse, Methods } from '../methods/types';
@ -17,7 +17,7 @@ export type WorkerPayload =
type: 'methodResponse';
messageId: string;
response?: ThenArg<MethodResponse<keyof Methods>>;
error?: { message: string };
error?: Pick<ApiError, 'message' | 'code' | 'hasErrorKey'>;
}
|
{

View File

@ -7,7 +7,7 @@ import type { OriginMessageEvent, WorkerPayload } from './types';
import { DEBUG } from '../../../config';
import { DEBUG_LEVELS } from '../../../util/debugConsole';
import { throttleWithTickEnd } from '../../../util/schedulers';
import { log } from '../helpers/misc';
import { buildApiError, log } from '../helpers/misc';
import { callApi, cancelApiProgress, initApi } from '../methods/init';
declare const self: WorkerGlobalScope;
@ -110,7 +110,7 @@ onmessage = ({ data }: OriginMessageEvent) => {
sendToOrigin({
type: 'methodResponse',
messageId,
error: { message: error.message },
error: buildApiError(error),
});
}
}

View File

@ -174,6 +174,7 @@ export type ApiDialog = ApiDialogError | ApiDialogMessage | ApiDialogContact | A
export type ApiError = {
message: string;
code?: number;
entities?: ApiMessageEntity[];
hasErrorKey?: boolean;
isSlowMode?: boolean;

View File

@ -12,7 +12,7 @@ import { IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/browser/windowEnviron
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
import { getAllNotificationsCount } from '../../../util/folderManager';
import getIsAppUpdateNeeded from '../../../util/getIsAppUpdateNeeded';
import getReadableErrorText from '../../../util/getReadableErrorText';
import { shouldShowErrorDialog } from '../../../util/getReadableErrorText';
import { compact, unique } from '../../../util/iteratees';
import { refreshFromCache } from '../../../util/localization';
import * as langProvider from '../../../util/oldLangProvider';
@ -388,7 +388,7 @@ addActionHandler('showDialog', (global, actions, payload): ActionReturnType => {
const { data, tabId = getCurrentTabId() } = payload;
// Filter out errors that we don't want to show to the user
if (data.type === 'error' && data.hasErrorKey && !getReadableErrorText(data)) {
if (data.type === 'error' && !shouldShowErrorDialog(data)) {
return global;
}

View File

@ -4,14 +4,14 @@ import type { Api } from '../tl';
* Base class for all Remote Procedure Call errors.
*/
export class RPCError extends Error {
public code: number | undefined;
public code: number;
public errorMessage: string;
constructor(message: string, request: Api.AnyRequest, code?: number) {
constructor(message: string, request: Api.AnyRequest, code: number) {
super(
'RPCError {0}: {1}{2}'
.replace('{0}', code?.toString() || '')
.replace('{0}', code.toString())
.replace('{1}', message)
.replace('{2}', RPCError._fmtRequest(request)),
);
@ -32,63 +32,37 @@ export class RPCError extends Error {
/**
* The request must be repeated, but directed to a different data center.
*/
export class InvalidDCError extends RPCError {
constructor(message: string, request: Api.AnyRequest, code?: number) {
super(message, request, code);
this.code = code || 303;
this.errorMessage = message || 'ERROR_SEE_OTHER';
}
}
export class InvalidDCError extends RPCError {}
/**
* The query contains errors. In the event that a request was created
* using a form and contains user generated data, the user should be
* notified that the data must be corrected before the query is repeated.
*/
export class BadRequestError extends RPCError {
code = 400;
errorMessage = 'BAD_REQUEST';
}
export class BadRequestError extends RPCError {}
/**
* There was an unauthorized attempt to use functionality available only
* to authorized users.
*/
export class UnauthorizedError extends RPCError {
code = 401;
errorMessage = 'UNAUTHORIZED';
}
export class UnauthorizedError extends RPCError {}
/**
* Privacy violation. For example, an attempt to write a message to
* someone who has blacklisted the current user.
*/
export class ForbiddenError extends RPCError {
code = 403;
errorMessage = 'FORBIDDEN';
}
export class ForbiddenError extends RPCError {}
/**
* An attempt to invoke a non-existent object, such as a method.
*/
export class NotFoundError extends RPCError {
code = 404;
errorMessage = 'NOT_FOUND';
}
export class NotFoundError extends RPCError {}
/**
* Errors related to invalid authorization key, like
* AUTH_KEY_DUPLICATED which can cause the connection to fail.
*/
export class AuthKeyError extends RPCError {
code = 406;
errorMessage = 'AUTH_KEY';
}
export class AuthKeyError extends RPCError {}
/**
* The maximum allowed number of attempts to invoke the given method
@ -96,29 +70,21 @@ export class AuthKeyError extends RPCError {
* attempt to request a large number of text messages (SMS) for the same
* phone number.
*/
export class FloodError extends RPCError {
code = 420;
errorMessage = 'FLOOD';
}
export class FloodError extends RPCError {}
/**
* An internal server error occurred while a request was being processed
* for example, there was a disruption while accessing a database or file
* storage
*/
export class ServerError extends RPCError {
code = 500; // Also witnessed as -500
errorMessage = 'INTERNAL';
}
export class ServerError extends RPCError {}
/**
* Clicking the inline buttons of bots that never (or take to long to)
* call ``answerCallbackQuery`` will result in this "special" RPCError.
*/
export class TimedOutError extends RPCError {
code = 503; // Only witnessed as -503
errorMessage = 'Timeout';
constructor(args: { request: Api.AnyRequest; code: number }) {
super('Timeout', args.request, args.code); // Only witnessed as -503
}
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @stylistic/max-len */
import {
BadRequestError, FloodError, InvalidDCError, RPCError, TimedOutError,
} from './RPCBaseErrors';
@ -8,8 +7,7 @@ export class UserMigrateError extends InvalidDCError {
constructor(args: any) {
const newDc = Number(args.capture || 0);
super(`The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, args.request);
this.message = `The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.newDc = newDc;
}
}
@ -19,8 +17,7 @@ export class PhoneMigrateError extends InvalidDCError {
constructor(args: any) {
const newDc = Number(args.capture || 0);
super(`The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, args.request);
this.message = `The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.newDc = newDc;
}
}
@ -30,11 +27,7 @@ export class SlowModeWaitError extends FloodError {
constructor(args: any) {
const seconds = Number(args.capture || 0);
super(
`A wait of ${seconds} seconds is required before sending another message in this chat ${RPCError._fmtRequest(args.request)}`,
args.request,
);
this.message = `A wait of ${seconds} seconds is required before sending another message in this chat${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.seconds = seconds;
}
}
@ -44,11 +37,7 @@ export class FloodWaitError extends FloodError {
constructor(args: any) {
const seconds = Number(args.capture || 0);
super(
`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`,
args.request,
);
this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.seconds = seconds;
}
}
@ -56,21 +45,14 @@ export class FloodWaitError extends FloodError {
export class FloodPremiumWaitError extends FloodWaitError {
constructor(args: any) {
const seconds = Number(args.capture || 0);
super(`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`);
this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`;
super(args);
this.seconds = seconds;
}
}
export class MsgWaitError extends FloodError {
constructor(args: any) {
super(
`Message failed to be sent.${RPCError._fmtRequest(args.request)}`,
args.request,
);
this.message = `Message failed to be sent.${RPCError._fmtRequest(
args.request,
)}`;
super(args.errorMessage, args.request, args.code);
}
}
@ -79,11 +61,7 @@ export class FloodTestPhoneWaitError extends FloodError {
constructor(args: any) {
const seconds = Number(args.capture || 0);
super(
`A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`,
args.request,
);
this.message = `A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.seconds = seconds;
}
}
@ -93,11 +71,7 @@ export class FileMigrateError extends InvalidDCError {
constructor(args: any) {
const newDc = Number(args.capture || 0);
super(
`The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`,
args.request,
);
this.message = `The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.newDc = newDc;
}
}
@ -107,11 +81,7 @@ export class NetworkMigrateError extends InvalidDCError {
constructor(args: any) {
const newDc = Number(args.capture || 0);
super(
`The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`,
args.request,
);
this.message = `The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`;
super(args.errorMessage, args.request, args.code);
this.newDc = newDc;
}
}
@ -121,17 +91,7 @@ export class EmailUnconfirmedError extends BadRequestError {
constructor(args: any) {
const codeLength = Number(args.capture || 0);
super(
`Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(
args.request,
)}`,
args.request,
400,
);
this.message = `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(
args.request,
)}`;
super(args.errorMessage, args.request, args.code);
this.codeLength = codeLength;
}
}
@ -141,9 +101,7 @@ export class PasswordFreshError extends BadRequestError {
constructor(args: any) {
const seconds = Number(args.capture || 0);
super(`The password was modified less than 24 hours ago, try again in ${seconds} seconds.`, args.request);
this.message = `The password was modified less than 24 hours ago, try again in ${seconds} seconds.`;
super(args.errorMessage, args.request, args.code);
this.seconds = seconds;
}
}
@ -153,9 +111,7 @@ export class SessionFreshError extends BadRequestError {
constructor(args: any) {
const seconds = Number(args.capture || 0);
super(`Session is fresh, please try again in ${seconds} seconds.`, args.request);
this.message = `Session is fresh, please try again in ${seconds} seconds.`;
super(args.errorMessage, args.request, args.code);
this.seconds = seconds;
}
}
@ -181,7 +137,7 @@ export class UserAlreadyAuthorizedError extends Error {
export class PasskeyCredentialNotFoundError extends RPCError {
constructor(args: any) {
super('PASSKEY_CREDENTIAL_NOT_FOUND', args.request);
super(args.errorMessage, args.request, args.code);
}
}

View File

@ -17,7 +17,12 @@ export function RPCMessageToError(
const m = rpcError.errorMessage.match(msgRegex);
if (m) {
const capture = m.length === 2 ? parseInt(m[1], 10) : undefined;
return new Cls({ request, capture });
return new Cls({
request,
capture,
code: rpcError.errorCode,
errorMessage: rpcError.errorMessage,
});
}
}
return new RPCError(rpcError.errorMessage, request, rpcError.errorCode);

View File

@ -136,6 +136,10 @@ const FINAL_PAYMENT_ERRORS = new Set([
'PAYMENT_FAILED',
]);
const ERROR_CODES_WITHOUT_DIALOG = new Set([
406,
]);
export default function getReadableErrorText(error: ApiError) {
const { message, isSlowMode, textParams } = error;
// Currently, Telegram API doesn't return `SLOWMODE_WAIT_X` error as described in the docs
@ -159,3 +163,10 @@ export function getShippingError(error: ApiError): ApiFieldError | undefined {
export function shouldClosePaymentModal(error: ApiError): boolean {
return FINAL_PAYMENT_ERRORS.has(error.message);
}
export function shouldShowErrorDialog(error: ApiError): boolean {
if (error.code && ERROR_CODES_WITHOUT_DIALOG.has(error.code)) return false;
if (error.hasErrorKey && !getReadableErrorText(error)) return false;
return true;
}