Cleanup error messages (#1412)

This commit is contained in:
Abhinav Kumar 2023-10-31 09:44:57 +00:00 committed by GitHub
commit 1e85b4488a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 101 deletions

View file

@ -35,7 +35,7 @@ export default function Login(props: LoginProps) {
router.push(PAGES.CREDENTIALS);
}
} catch (e) {
setFieldError(`${t('UNKNOWN_ERROR} ${e.message}')}`);
setFieldError(`${t('UNKNOWN_ERROR')} (reason:${e.message})`);
}
};

View file

@ -73,7 +73,7 @@ export default function SignUp(props: SignUpProps) {
setData(LS_KEYS.USER, { email });
await sendOtt(email);
} catch (e) {
setFieldError('confirm', `${t('UNKNOWN_ERROR} ${e.message}')}`);
setFieldError('confirm', `${t('UNKNOWN_ERROR')} ${e.message}`);
throw e;
}
try {

View file

@ -12,7 +12,7 @@ import { syncFiles, trashFiles } from 'services/fileService';
import { EnteFile } from 'types/file';
import { SelectedState } from 'types/gallery';
import { ServerErrorCodes } from 'utils/error';
import { ApiError } from 'utils/error';
import { constructFileToCollectionMap, getSelectedFiles } from 'utils/file';
import {
DeduplicateContextType,
@ -30,6 +30,7 @@ import { VerticallyCentered } from 'components/Container';
import Typography from '@mui/material/Typography';
import useMemoSingleThreaded from 'hooks/useMemoSingleThreaded';
import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
import { HttpStatusCode } from 'axios';
export const DeduplicateContext = createContext<DeduplicateContextType>(
DefaultDeduplicateContext
@ -124,21 +125,24 @@ export default function Deduplicate() {
const selectedFiles = getSelectedFiles(selected, duplicateFiles);
await trashFiles(selectedFiles);
} catch (e) {
switch (e.status?.toString()) {
case ServerErrorCodes.FORBIDDEN:
setDialogMessage({
title: t('ERROR'),
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.Forbidden
) {
setDialogMessage({
title: t('ERROR'),
close: { variant: 'critical' },
content: t('NOT_FILE_OWNER'),
});
close: { variant: 'critical' },
content: t('NOT_FILE_OWNER'),
});
} else {
setDialogMessage({
title: t('ERROR'),
close: { variant: 'critical' },
content: t('UNKNOWN_ERROR'),
});
}
setDialogMessage({
title: t('ERROR'),
close: { variant: 'critical' },
content: t('UNKNOWN_ERROR'),
});
} finally {
await syncWithRemote();
finishLoading();

View file

@ -70,7 +70,7 @@ import {
TRASH_SECTION,
} from 'constants/collection';
import { AppContext } from 'pages/_app';
import { CustomError, ServerErrorCodes } from 'utils/error';
import { CustomError } from 'utils/error';
import { PAGES } from 'constants/pages';
import {
COLLECTION_OPS_TYPE,
@ -679,7 +679,7 @@ export default function Gallery() {
}
const tokenValid = await isTokenValid(token);
if (!tokenValid) {
throw new Error(ServerErrorCodes.SESSION_EXPIRED);
throw new Error(CustomError.SESSION_EXPIRED);
}
!silent && startLoading();
const collections = await getAllLatestCollections();
@ -694,7 +694,7 @@ export default function Gallery() {
await syncMapEnabled();
} catch (e) {
switch (e.message) {
case ServerErrorCodes.SESSION_EXPIRED:
case CustomError.SESSION_EXPIRED:
showSessionExpiredMessage();
break;
case CustomError.KEY_MISSING:

View file

@ -24,6 +24,8 @@ import { Trans } from 'react-i18next';
import { Link } from '@mui/material';
import { SUPPORT_EMAIL } from 'constants/urls';
import { DialogBoxAttributes } from 'types/dialogBox';
import { ApiError } from 'utils/error';
import { HttpStatusCode } from 'axios';
const bip39 = require('bip39');
// mobile client library only supports english.
@ -68,7 +70,10 @@ export default function Recover() {
});
}
} catch (e) {
if (e.status === 404) {
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound
) {
logoutUser();
} else {
logError(e, 'two factor recovery page setup failed');

View file

@ -15,6 +15,8 @@ import FormTitle from 'components/Form/FormPaper/Title';
import FormPaperFooter from 'components/Form/FormPaper/Footer';
import { VerticallyCentered } from 'components/Container';
import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
import { ApiError } from 'utils/error';
import { HttpStatusCode } from 'axios';
export default function Home() {
const [sessionID, setSessionID] = useState('');
@ -52,7 +54,10 @@ export default function Home() {
InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
router.push(redirectURL ?? PAGES.CREDENTIALS);
} catch (e) {
if (e.status === 404) {
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound
) {
logoutUser();
} else {
throw e;

View file

@ -33,6 +33,8 @@ import SingleInputForm, {
import EnteSpinner from 'components/EnteSpinner';
import { VerticallyCentered } from 'components/Container';
import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
import { ApiError } from 'utils/error';
import { HttpStatusCode } from 'axios';
export default function Verify() {
const [email, setEmail] = useState('');
@ -122,10 +124,12 @@ export default function Verify() {
}
}
} catch (e) {
if (e?.status === 401) {
setFieldError(t('INVALID_CODE'));
} else if (e?.status === 410) {
setFieldError(t('EXPIRED_CODE'));
if (e instanceof ApiError) {
if (e?.httpStatusCode === HttpStatusCode.Unauthorized) {
setFieldError(t('INVALID_CODE'));
} else if (e?.httpStatusCode === HttpStatusCode.Gone) {
setFieldError(t('EXPIRED_CODE'));
}
} else {
setFieldError(`${t('UNKNOWN_ERROR')} ${e.message}`);
}

View file

@ -1,6 +1,7 @@
import axios, { AxiosRequestConfig } from 'axios';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { addLogLine } from 'utils/logging';
import { logError } from 'utils/sentry';
import { ApiError, CustomError, isApiErrorResponse } from 'utils/error';
interface IHTTPHeaders {
[headerKey: string]: any;
@ -18,34 +19,66 @@ class HTTPService {
axios.interceptors.response.use(
(response) => Promise.resolve(response),
(error) => {
const config = error.config as AxiosRequestConfig;
if (error.response) {
const response = error.response as AxiosResponse;
let apiError: ApiError;
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
logError(error, 'HTTP Service Error', {
url: error.config.url,
method: error.config.method,
cfRay: error.response?.headers['cf-ray'],
xRequestId: error.response?.headers['x-request-id'],
status: error.response.status,
if (isApiErrorResponse(response.data)) {
const responseData = response.data;
logError(error, 'HTTP Service Error', {
url: config.url,
method: config.method,
xRequestId: response.headers['x-request-id'],
httpStatus: response.status,
errMessage: responseData.message,
errCode: responseData.code,
});
apiError = new ApiError(
responseData.message,
responseData.code,
response.status
);
} else {
if (response.status >= 400 && response.status < 500) {
apiError = new ApiError(
CustomError.CLIENT_ERROR,
'',
response.status
);
} else {
apiError = new ApiError(
CustomError.ServerError,
'',
response.status
);
}
}
logError(apiError, 'HTTP Service Error', {
url: config.url,
method: config.method,
cfRay: response.headers['cf-ray'],
xRequestId: response.headers['x-request-id'],
httpStatus: response.status,
});
const { response } = error;
return Promise.reject(response);
throw apiError;
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
addLogLine(
'request failed- no response',
`url: ${error.config.url}`,
`method: ${error.config.method}`
`url: ${config.url}`,
`method: ${config.method}`
);
return Promise.reject(error);
} else {
// Something happened in setting up the request that triggered an Error
addLogLine(
'request failed- axios error',
`url: ${error.config.url}`,
`method: ${error.config.method}`
`url: ${config.url}`,
`method: ${config.method}`
);
return Promise.reject(error);
}

View file

@ -1,10 +1,11 @@
import { HttpStatusCode } from 'axios';
import HTTPService from 'services/HTTPService';
import { AuthEntity, AuthKey } from 'types/authenticator/api';
import { Code } from 'types/authenticator/code';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import { getEndpoint } from 'utils/common/apiUtil';
import { getActualKey, getToken } from 'utils/common/key';
import { CustomError } from 'utils/error';
import { ApiError, CustomError } from 'utils/error';
import { logError } from 'utils/sentry';
const ENDPOINT = getEndpoint();
@ -77,7 +78,10 @@ export const getAuthKey = async (): Promise<AuthKey> => {
);
return resp.data;
} catch (e) {
if (e.status === 404) {
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound
) {
throw Error(CustomError.AUTH_KEY_NOT_FOUND);
} else {
logError(e, 'Get key failed');

View file

@ -40,13 +40,13 @@ import {
UpdateSRPAndKeysResponse,
GetSRPAttributesResponse,
} from 'types/user';
import { ServerErrorCodes } from 'utils/error';
import { ApiError, CustomError } from 'utils/error';
import isElectron from 'is-electron';
import safeStorageService from './electron/safeStorage';
import { deleteAllCache } from 'utils/storage/cache';
import { B64EncryptionResult } from 'types/crypto';
import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
import { AxiosResponse } from 'axios';
import { AxiosResponse, HttpStatusCode } from 'axios';
import { APPS, getAppName } from 'constants/apps';
import { addLocalLog } from 'utils/logging';
import { convertBase64ToBuffer, convertBufferToBase64 } from 'utils/user';
@ -224,7 +224,10 @@ export const isTokenValid = async (token: string) => {
return true;
} catch (e) {
logError(e, 'session-validity api call failed');
if (e.status?.toString() === ServerErrorCodes.SESSION_EXPIRED) {
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.Unauthorized
) {
return false;
} else {
return true;
@ -554,11 +557,8 @@ export const getSRPAttributes = async (
});
return (resp.data as GetSRPAttributesResponse).attributes;
} catch (e) {
if (e.status?.toString() === ServerErrorCodes.NOT_FOUND) {
return null;
}
logError(e, 'failed to get SRP attributes');
throw e;
return null;
}
};
@ -730,7 +730,14 @@ export const verifySRPSession = async (
return resp.data as UserVerificationResponse;
} catch (e) {
logError(e, 'verifySRPSession failed');
throw e;
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.Forbidden
) {
throw Error(CustomError.INCORRECT_PASSWORD);
} else {
throw e;
}
}
};

View file

@ -1,15 +1,24 @@
export const ServerErrorCodes = {
SESSION_EXPIRED: '401',
NO_ACTIVE_SUBSCRIPTION: '402',
FORBIDDEN: '403',
STORAGE_LIMIT_EXCEEDED: '426',
FILE_TOO_LARGE: '413',
TOKEN_EXPIRED: '410',
TOO_MANY_REQUEST: '429',
BAD_REQUEST: '400',
PAYMENT_REQUIRED: '402',
NOT_FOUND: '404',
};
import { HttpStatusCode } from 'axios';
export class ApiErrorResponse {
code: string;
message: string;
}
export class ApiError extends Error {
httpStatusCode: number;
errCode: string;
constructor(message: string, errCode: string, httpStatus: number) {
super(message);
this.name = 'ApiError';
this.errCode = errCode;
this.httpStatusCode = httpStatus;
}
}
export function isApiErrorResponse(object: any): object is ApiErrorResponse {
return object && 'code' in object && 'message' in object;
}
export const CustomError = {
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
@ -72,34 +81,10 @@ export const CustomError = {
PROCESSING_FAILED: 'processing failed',
EXPORT_RECORD_JSON_PARSING_FAILED: 'export record json parsing failed',
TWO_FACTOR_ENABLED: 'two factor enabled',
CLIENT_ERROR: 'client error',
ServerError: 'server error',
};
export function parseUploadErrorCodes(error) {
let parsedMessage = null;
if (error?.status) {
const errorCode = error.status.toString();
switch (errorCode) {
case ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION:
parsedMessage = CustomError.SUBSCRIPTION_EXPIRED;
break;
case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED:
parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED;
break;
case ServerErrorCodes.SESSION_EXPIRED:
parsedMessage = CustomError.SESSION_EXPIRED;
break;
case ServerErrorCodes.FILE_TOO_LARGE:
parsedMessage = CustomError.FILE_TOO_LARGE;
break;
default:
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`;
}
} else {
parsedMessage = error.message;
}
return new Error(parsedMessage);
}
export function handleUploadError(error): Error {
const parsedError = parseUploadErrorCodes(error);
@ -123,29 +108,53 @@ export function errorWithContext(originalError: Error, context: string) {
return errorWithContext;
}
export function parseUploadErrorCodes(error) {
let parsedMessage = null;
if (error instanceof ApiError) {
switch (error.httpStatusCode) {
case HttpStatusCode.PaymentRequired:
parsedMessage = CustomError.SUBSCRIPTION_EXPIRED;
break;
case HttpStatusCode.UpgradeRequired:
parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED;
break;
case HttpStatusCode.Unauthorized:
parsedMessage = CustomError.SESSION_EXPIRED;
break;
case HttpStatusCode.PayloadTooLarge:
parsedMessage = CustomError.FILE_TOO_LARGE;
break;
default:
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
}
} else {
parsedMessage = error.message;
}
return new Error(parsedMessage);
}
export const parseSharingErrorCodes = (error) => {
let parsedMessage = null;
if (error?.status) {
const errorCode = error.status.toString();
switch (errorCode) {
case ServerErrorCodes.BAD_REQUEST:
if (error instanceof ApiError) {
switch (error.httpStatusCode) {
case HttpStatusCode.BadRequest:
parsedMessage = CustomError.BAD_REQUEST;
break;
case ServerErrorCodes.PAYMENT_REQUIRED:
case HttpStatusCode.PaymentRequired:
parsedMessage = CustomError.SUBSCRIPTION_NEEDED;
break;
case ServerErrorCodes.NOT_FOUND:
case HttpStatusCode.NotFound:
parsedMessage = CustomError.NOT_FOUND;
break;
case ServerErrorCodes.SESSION_EXPIRED:
case ServerErrorCodes.TOKEN_EXPIRED:
case HttpStatusCode.Unauthorized:
case HttpStatusCode.Gone:
parsedMessage = CustomError.TOKEN_EXPIRED;
break;
case ServerErrorCodes.TOO_MANY_REQUEST:
case HttpStatusCode.TooManyRequests:
parsedMessage = CustomError.TOO_MANY_REQUESTS;
break;
default:
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`;
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
}
} else {
parsedMessage = error.message;

View file

@ -3,6 +3,7 @@ import { addLocalLog, addLogLine } from 'utils/logging';
import { getSentryUserID } from 'utils/user';
import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
import { getHasOptedOutOfCrashReports } from 'utils/storage';
import { ApiError } from 'utils/error';
export const logError = async (
error: any,
@ -12,11 +13,20 @@ export const logError = async (
) => {
const err = errorWithContext(error, msg);
if (!skipAddLogLine) {
addLogLine(
`error: ${error?.name} ${error?.message} ${
error?.stack
} msg: ${msg} ${info ? `info: ${JSON.stringify(info)}` : ''}`
);
if (error instanceof ApiError) {
addLogLine(`error: ${error?.name} ${error?.message}
msg: ${msg} errorCode: ${JSON.stringify(error?.errCode)}
httpStatusCode: ${JSON.stringify(error?.httpStatusCode)} ${
info ? `info: ${JSON.stringify(info)}` : ''
}
${error?.stack}`);
} else {
addLogLine(
`error: ${error?.name} ${error?.message}
msg: ${msg} ${info ? `info: ${JSON.stringify(info)}` : ''}
${error?.stack}`
);
}
}
if (!InMemoryStore.has(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) {
const optedOutOfCrashReports = getHasOptedOutOfCrashReports();