mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-30 19:37:55 +08:00
Merge pull request #55 from AppFlowy-IO/revert-46-error_code_integrate
Revert "chore: Introduce ErrrorCode, do not send update profile when metadata is empty"
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { getAxiosInstance } from './http_api';
|
||||
|
||||
/**
|
||||
* Type-safe API client wrapper that guarantees response data exists
|
||||
* These functions work with the interceptor to provide clean API access
|
||||
*/
|
||||
|
||||
function ensureAxiosInstance() {
|
||||
const instance = getAxiosInstance();
|
||||
|
||||
if (!instance) {
|
||||
throw new Error('Axios instance not initialized');
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request with guaranteed response data
|
||||
*/
|
||||
export async function apiGet<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.get<T>(url, config);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request with guaranteed response data
|
||||
*/
|
||||
export async function apiPost<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.post<T>(url, data, config);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request with guaranteed response data
|
||||
*/
|
||||
export async function apiPut<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.put<T>(url, data, config);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH request with guaranteed response data
|
||||
*/
|
||||
export async function apiPatch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.patch<T>(url, data, config);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request with guaranteed response data
|
||||
*/
|
||||
export async function apiDelete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.delete<T>(url, config);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HEAD request with guaranteed response data
|
||||
*/
|
||||
export async function apiHead<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.head<T>(url, config);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* For void responses (no data expected)
|
||||
*/
|
||||
export async function apiGetVoid(url: string, config?: AxiosRequestConfig): Promise<void> {
|
||||
await apiGet<void>(url, config);
|
||||
}
|
||||
|
||||
export async function apiPostVoid(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<void> {
|
||||
await apiPost<void>(url, data, config);
|
||||
}
|
||||
|
||||
export async function apiPutVoid(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<void> {
|
||||
await apiPut<void>(url, data, config);
|
||||
}
|
||||
|
||||
export async function apiPatchVoid(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<void> {
|
||||
await apiPatch<void>(url, data, config);
|
||||
}
|
||||
|
||||
export async function apiDeleteVoid(url: string, config?: AxiosRequestConfig): Promise<void> {
|
||||
await apiDelete<void>(url, config);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { AppResponseError, ErrorCode, apiErrorHandler } from './error-handler';
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: ErrorCode;
|
||||
data?: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process API response and handle errors consistently
|
||||
*/
|
||||
export async function processApiResponse<T>(
|
||||
response: ApiResponse<T>,
|
||||
options?: {
|
||||
showNotification?: boolean;
|
||||
throwError?: boolean;
|
||||
customMessage?: string;
|
||||
onAuthError?: () => void;
|
||||
onPermissionError?: () => void;
|
||||
}
|
||||
): Promise<T> {
|
||||
if (response.code === ErrorCode.Ok) {
|
||||
if (response.data === undefined) {
|
||||
const error: AppResponseError = {
|
||||
code: ErrorCode.MissingPayload,
|
||||
message: 'Response data is missing'
|
||||
};
|
||||
|
||||
await apiErrorHandler.handleError(error, options);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
const error: AppResponseError = {
|
||||
code: Object.values(ErrorCode).includes(response.code) ? response.code : ErrorCode.Unhandled,
|
||||
message: response.message || 'An error occurred'
|
||||
};
|
||||
|
||||
await apiErrorHandler.handleError(error, options);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process void API response (no data expected)
|
||||
*/
|
||||
export async function processVoidApiResponse(
|
||||
response: ApiResponse<unknown>,
|
||||
options?: Parameters<typeof processApiResponse>[1]
|
||||
): Promise<void> {
|
||||
if (response.code === ErrorCode.Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const error: AppResponseError = {
|
||||
code: Object.values(ErrorCode).includes(response.code) ? response.code : ErrorCode.Unhandled,
|
||||
message: response.message || 'An error occurred'
|
||||
};
|
||||
|
||||
await apiErrorHandler.handleError(error, options);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal API call wrapper with automatic error handling
|
||||
*/
|
||||
async function processApiCall<T>(
|
||||
fn: () => Promise<ApiResponse<T>>,
|
||||
options?: Parameters<typeof processApiResponse>[1]
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await fn();
|
||||
|
||||
return await processApiResponse(response, options);
|
||||
} catch (error: unknown) {
|
||||
// If it's already an AppResponseError, just re-throw
|
||||
if ((error as AppResponseError)?.code !== undefined) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Otherwise, wrap in a generic error
|
||||
const apiError: AppResponseError = {
|
||||
code: ErrorCode.Unhandled,
|
||||
message: (error as Error)?.message || 'An unexpected error occurred'
|
||||
};
|
||||
|
||||
await apiErrorHandler.handleError(apiError, options);
|
||||
throw apiError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API call with mapping function for easy data transformation
|
||||
*/
|
||||
export async function apiCall<T, R>(
|
||||
fn: () => Promise<ApiResponse<T> | undefined>,
|
||||
mapper: (data: T) => R,
|
||||
options?: Parameters<typeof processApiResponse>[1]
|
||||
): Promise<R>;
|
||||
|
||||
/**
|
||||
* API call for void responses (no mapper needed)
|
||||
*/
|
||||
export async function apiCall<T>(
|
||||
fn: () => Promise<ApiResponse<T> | undefined>,
|
||||
options?: Parameters<typeof processApiResponse>[1]
|
||||
): Promise<void>;
|
||||
|
||||
export async function apiCall<T, R>(
|
||||
fn: () => Promise<ApiResponse<T> | undefined>,
|
||||
mapperOrOptions?: ((data: T) => R) | Parameters<typeof processApiResponse>[1],
|
||||
options?: Parameters<typeof processApiResponse>[1]
|
||||
): Promise<R | void> {
|
||||
const wrappedFn = async (): Promise<ApiResponse<T>> => {
|
||||
const response = await fn();
|
||||
|
||||
if (!response) {
|
||||
throw new Error('No response data');
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// If second parameter is a function, it's a mapper
|
||||
if (typeof mapperOrOptions === 'function') {
|
||||
const data = await processApiCall(wrappedFn, options);
|
||||
|
||||
return mapperOrOptions(data);
|
||||
}
|
||||
|
||||
// Otherwise, it's options (void response)
|
||||
await processApiCall(wrappedFn, mapperOrOptions);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
|
||||
/**
|
||||
* Extended axios config that includes custom options
|
||||
*/
|
||||
export interface ExtendedAxiosConfig extends AxiosRequestConfig {
|
||||
/**
|
||||
* Whether to show error notifications for this request
|
||||
* Default: false
|
||||
*/
|
||||
showNotification?: boolean;
|
||||
|
||||
/**
|
||||
* Custom error message to show instead of the default API error message
|
||||
*/
|
||||
customErrorMessage?: string;
|
||||
|
||||
/**
|
||||
* Whether to throw errors or silently handle them
|
||||
* Default: true
|
||||
*/
|
||||
throwError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create axios config with notification enabled
|
||||
*/
|
||||
export function withNotification(config?: ExtendedAxiosConfig): ExtendedAxiosConfig {
|
||||
return {
|
||||
...config,
|
||||
showNotification: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create axios config with custom error message
|
||||
*/
|
||||
export function withCustomError(message: string, config?: ExtendedAxiosConfig): ExtendedAxiosConfig {
|
||||
return {
|
||||
...config,
|
||||
customErrorMessage: message,
|
||||
showNotification: true,
|
||||
};
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { getAxiosInstance } from './http_api';
|
||||
|
||||
/**
|
||||
* Raw axios access for endpoints that don't return ApiResponse format
|
||||
* These bypass the ApiResponse processing in the interceptor
|
||||
*/
|
||||
|
||||
function ensureAxiosInstance() {
|
||||
const instance = getAxiosInstance();
|
||||
|
||||
if (!instance) {
|
||||
throw new Error('Axios instance not initialized');
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw GET request - returns full axios response without ApiResponse processing
|
||||
* Use this for endpoints that return raw data, blobs, or non-standard formats
|
||||
*/
|
||||
export async function rawGet<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
const instance = ensureAxiosInstance();
|
||||
|
||||
return await instance.get<T>(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw POST request - returns full axios response without ApiResponse processing
|
||||
*/
|
||||
export async function rawPost<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
const instance = ensureAxiosInstance();
|
||||
|
||||
return await instance.post<T>(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw PUT request - returns full axios response without ApiResponse processing
|
||||
*/
|
||||
export async function rawPut<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
const instance = ensureAxiosInstance();
|
||||
|
||||
return await instance.put<T>(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw PATCH request - returns full axios response without ApiResponse processing
|
||||
*/
|
||||
export async function rawPatch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
const instance = ensureAxiosInstance();
|
||||
|
||||
return await instance.patch<T>(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw DELETE request - returns full axios response without ApiResponse processing
|
||||
*/
|
||||
export async function rawDelete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||
const instance = ensureAxiosInstance();
|
||||
|
||||
return await instance.delete<T>(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file as blob
|
||||
*/
|
||||
export async function downloadFile(url: string, config?: AxiosRequestConfig): Promise<Blob> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.get(url, {
|
||||
...config,
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw text response
|
||||
*/
|
||||
export async function getRawText(url: string, config?: AxiosRequestConfig): Promise<string> {
|
||||
const instance = ensureAxiosInstance();
|
||||
const response = await instance.get(url, {
|
||||
...config,
|
||||
responseType: 'text'
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
import { invalidToken } from '@/application/session/token';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
// Error codes from AppFlowy-Cloud-Premium
|
||||
export enum ErrorCode {
|
||||
Ok = 0,
|
||||
Unhandled = -1,
|
||||
RecordNotFound = -2,
|
||||
RecordAlreadyExists = -3,
|
||||
RecordDeleted = -4,
|
||||
RetryLater = -5,
|
||||
InvalidEmail = 1001,
|
||||
InvalidPassword = 1002,
|
||||
OAuthError = 1003,
|
||||
MissingPayload = 1004,
|
||||
DBError = 1005,
|
||||
OpenError = 1006,
|
||||
InvalidUrl = 1007,
|
||||
InvalidRequest = 1008,
|
||||
InvalidOAuthProvider = 1009,
|
||||
NotLoggedIn = 1011,
|
||||
NotEnoughPermissions = 1012,
|
||||
StorageSpaceNotEnough = 1015,
|
||||
PayloadTooLarge = 1016,
|
||||
Internal = 1017,
|
||||
UuidError = 1018,
|
||||
IOError = 1019,
|
||||
SqlxError = 1020,
|
||||
S3ResponseError = 1021,
|
||||
SerdeError = 1022,
|
||||
NetworkError = 1023,
|
||||
UserUnAuthorized = 1024,
|
||||
NoRequiredData = 1025,
|
||||
WorkspaceLimitExceeded = 1026,
|
||||
WorkspaceMemberLimitExceeded = 1027,
|
||||
FileStorageLimitExceeded = 1028,
|
||||
OverrideWithIncorrectData = 1029,
|
||||
PublishNamespaceNotSet = 1030,
|
||||
PublishNamespaceAlreadyTaken = 1031,
|
||||
AIServiceUnavailable = 1032,
|
||||
AIResponseLimitExceeded = 1033,
|
||||
StringLengthLimitReached = 1034,
|
||||
SqlxArgEncodingError = 1035,
|
||||
InvalidContentType = 1036,
|
||||
SingleUploadLimitExceeded = 1037,
|
||||
AppleRevokeTokenError = 1038,
|
||||
InvalidPublishedOutline = 1039,
|
||||
InvalidFolderView = 1040,
|
||||
NotInviteeOfWorkspaceInvitation = 1041,
|
||||
MissingView = 1042,
|
||||
AccessRequestAlreadyExists = 1043,
|
||||
CustomNamespaceDisabled = 1044,
|
||||
CustomNamespaceDisallowed = 1045,
|
||||
TooManyImportTask = 1046,
|
||||
CustomNamespaceTooShort = 1047,
|
||||
CustomNamespaceTooLong = 1048,
|
||||
CustomNamespaceReserved = 1049,
|
||||
PublishNameAlreadyExists = 1050,
|
||||
PublishNameInvalidCharacter = 1051,
|
||||
PublishNameTooLong = 1052,
|
||||
CustomNamespaceInvalidCharacter = 1053,
|
||||
ServiceTemporaryUnavailable = 1054,
|
||||
DecodeUpdateError = 1055,
|
||||
ApplyUpdateError = 1056,
|
||||
ActionTimeout = 1057,
|
||||
AIImageResponseLimitExceeded = 1058,
|
||||
MailerError = 1059,
|
||||
LicenseError = 1060,
|
||||
AIMaxRequired = 1061,
|
||||
InvalidPageData = 1062,
|
||||
MemberNotFound = 1063,
|
||||
InvalidBlock = 1064,
|
||||
RequestTimeout = 1065,
|
||||
AIResponseError = 1066,
|
||||
FeatureNotAvailable = 1067,
|
||||
InvalidInvitationCode = 1068,
|
||||
InvalidGuest = 1069,
|
||||
FreePlanGuestLimitExceeded = 1070,
|
||||
PaidPlanGuestLimitExceeded = 1071,
|
||||
CommercialError = 1072,
|
||||
TooManyExportTask = 1073,
|
||||
InvalidSubscriptionPlan = 1076,
|
||||
AlreadySubscribed = 1077,
|
||||
UserIsNotCustomer = 1078,
|
||||
TooManyRequests = 1079,
|
||||
JsonWebTokenError = 1081,
|
||||
LicenseExpired = 1082,
|
||||
LicenseDeleted = 1083,
|
||||
LicenseReachLimit = 1084,
|
||||
ImportError = 1085,
|
||||
ExportError = 1086,
|
||||
UploadFileNotFound = 1087,
|
||||
UploadFileExpired = 1088,
|
||||
UploadFileTooLarge = 1089,
|
||||
UpgradeRequired = 1090,
|
||||
UnzipError = 1091,
|
||||
CannotOpenWorkspace = 1092,
|
||||
StreamGroupNotExist = 1093,
|
||||
S3ServiceUnavailable = 1094,
|
||||
ImportCollabError = 1095,
|
||||
RealtimeProtocolError = 1096,
|
||||
CollabAwarenessError = 1097,
|
||||
RealtimeDecodingError = 1098,
|
||||
UnexpectedRealtimeData = 1099,
|
||||
ExpectInitSync = 1100,
|
||||
CollabError = 1101,
|
||||
NotEnoughPermissionToWrite = 1102,
|
||||
NotEnoughPermissionToRead = 1103,
|
||||
RealtimeUserNotFound = 1104,
|
||||
GroupNotFound = 1105,
|
||||
CreateGroupWorkspaceIdMismatch = 1106,
|
||||
CreateGroupCannotGetCollabData = 1107,
|
||||
NoRequiredCollabData = 1108,
|
||||
TooManyRealtimeMessages = 1109,
|
||||
LockTimeout = 1110,
|
||||
CollabStreamError = 1111,
|
||||
CannotCreateGroup = 1112,
|
||||
BincodeCollabError = 1113,
|
||||
CreateSnapshotFailed = 1114,
|
||||
GetLatestSnapshotFailed = 1115,
|
||||
CollabSchemaError = 1116,
|
||||
LeaseError = 1117,
|
||||
SendWSMessageFailed = 1118,
|
||||
IndexerStreamGroupNotExist = 1119,
|
||||
LicenseLimitHit = 1120,
|
||||
}
|
||||
|
||||
export interface AppResponseError {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
// Error categories for better handling
|
||||
export const isAuthError = (code: ErrorCode): boolean => {
|
||||
return [
|
||||
ErrorCode.UserUnAuthorized,
|
||||
ErrorCode.NotLoggedIn,
|
||||
ErrorCode.OAuthError,
|
||||
ErrorCode.InvalidPassword,
|
||||
ErrorCode.AppleRevokeTokenError,
|
||||
ErrorCode.JsonWebTokenError,
|
||||
].includes(code);
|
||||
};
|
||||
|
||||
export const isPermissionError = (code: ErrorCode): boolean => {
|
||||
return [
|
||||
ErrorCode.NotEnoughPermissions,
|
||||
ErrorCode.NotEnoughPermissionToWrite,
|
||||
ErrorCode.NotEnoughPermissionToRead,
|
||||
ErrorCode.NotInviteeOfWorkspaceInvitation,
|
||||
ErrorCode.InvalidGuest,
|
||||
].includes(code);
|
||||
};
|
||||
|
||||
export const isResourceError = (code: ErrorCode): boolean => {
|
||||
return [
|
||||
ErrorCode.RecordNotFound,
|
||||
ErrorCode.RecordDeleted,
|
||||
ErrorCode.MissingView,
|
||||
ErrorCode.UploadFileNotFound,
|
||||
ErrorCode.MemberNotFound,
|
||||
].includes(code);
|
||||
};
|
||||
|
||||
export const isDuplicateError = (code: ErrorCode): boolean => {
|
||||
return [
|
||||
ErrorCode.RecordAlreadyExists,
|
||||
ErrorCode.PublishNameAlreadyExists,
|
||||
ErrorCode.PublishNamespaceAlreadyTaken,
|
||||
ErrorCode.AccessRequestAlreadyExists,
|
||||
ErrorCode.AlreadySubscribed,
|
||||
].includes(code);
|
||||
};
|
||||
|
||||
export const isOk = (code: ErrorCode): boolean => {
|
||||
return code === ErrorCode.Ok;
|
||||
};
|
||||
|
||||
export const isLimitError = (code: ErrorCode): boolean => {
|
||||
return [
|
||||
ErrorCode.StorageSpaceNotEnough,
|
||||
ErrorCode.PayloadTooLarge,
|
||||
ErrorCode.WorkspaceLimitExceeded,
|
||||
ErrorCode.WorkspaceMemberLimitExceeded,
|
||||
ErrorCode.FileStorageLimitExceeded,
|
||||
ErrorCode.StringLengthLimitReached,
|
||||
ErrorCode.SingleUploadLimitExceeded,
|
||||
ErrorCode.FreePlanGuestLimitExceeded,
|
||||
ErrorCode.PaidPlanGuestLimitExceeded,
|
||||
ErrorCode.UploadFileTooLarge,
|
||||
ErrorCode.TooManyImportTask,
|
||||
ErrorCode.TooManyExportTask,
|
||||
ErrorCode.AIResponseLimitExceeded,
|
||||
ErrorCode.AIImageResponseLimitExceeded,
|
||||
ErrorCode.LicenseReachLimit,
|
||||
ErrorCode.LicenseLimitHit,
|
||||
].includes(code);
|
||||
};
|
||||
|
||||
|
||||
export const isValidationError = (code: ErrorCode): boolean => {
|
||||
return [
|
||||
ErrorCode.InvalidEmail,
|
||||
ErrorCode.InvalidPassword,
|
||||
ErrorCode.InvalidRequest,
|
||||
ErrorCode.InvalidUrl,
|
||||
ErrorCode.InvalidContentType,
|
||||
ErrorCode.InvalidPageData,
|
||||
ErrorCode.InvalidBlock,
|
||||
ErrorCode.InvalidFolderView,
|
||||
ErrorCode.InvalidPublishedOutline,
|
||||
ErrorCode.PublishNameInvalidCharacter,
|
||||
ErrorCode.PublishNameTooLong,
|
||||
ErrorCode.CustomNamespaceInvalidCharacter,
|
||||
ErrorCode.CustomNamespaceTooShort,
|
||||
ErrorCode.CustomNamespaceTooLong,
|
||||
ErrorCode.InvalidInvitationCode,
|
||||
ErrorCode.InvalidSubscriptionPlan,
|
||||
].includes(code);
|
||||
};
|
||||
|
||||
// Get user-friendly error message
|
||||
export const getUserFriendlyMessage = (error: AppResponseError): string => {
|
||||
// Authentication errors
|
||||
if (isAuthError(error.code)) {
|
||||
switch (error.code) {
|
||||
case ErrorCode.UserUnAuthorized:
|
||||
return 'Your session has expired. Please sign in again.';
|
||||
case ErrorCode.NotLoggedIn:
|
||||
return 'Please sign in to continue.';
|
||||
case ErrorCode.InvalidPassword:
|
||||
return 'Invalid password. Please try again.';
|
||||
default:
|
||||
return 'Authentication failed. Please sign in again.';
|
||||
}
|
||||
}
|
||||
|
||||
// Permission errors
|
||||
if (isPermissionError(error.code)) {
|
||||
switch (error.code) {
|
||||
case ErrorCode.NotEnoughPermissions:
|
||||
return 'You do not have permission to perform this action.';
|
||||
case ErrorCode.NotEnoughPermissionToWrite:
|
||||
return 'You have read-only access to this resource.';
|
||||
case ErrorCode.NotEnoughPermissionToRead:
|
||||
return 'You do not have permission to view this resource.';
|
||||
default:
|
||||
return 'Access denied. Insufficient permissions.';
|
||||
}
|
||||
}
|
||||
|
||||
// Resource errors
|
||||
if (isResourceError(error.code)) {
|
||||
switch (error.code) {
|
||||
case ErrorCode.RecordNotFound:
|
||||
return 'The requested item could not be found.';
|
||||
case ErrorCode.RecordDeleted:
|
||||
return 'This item has been deleted.';
|
||||
case ErrorCode.MissingView:
|
||||
return 'The page or view could not be found.';
|
||||
default:
|
||||
return 'Resource not available.';
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate errors
|
||||
if (isDuplicateError(error.code)) {
|
||||
switch (error.code) {
|
||||
case ErrorCode.RecordAlreadyExists:
|
||||
return 'This item already exists.';
|
||||
case ErrorCode.PublishNameAlreadyExists:
|
||||
return 'This publish name is already taken. Please choose another.';
|
||||
case ErrorCode.AlreadySubscribed:
|
||||
return 'You are already subscribed to this plan.';
|
||||
default:
|
||||
return 'This action would create a duplicate.';
|
||||
}
|
||||
}
|
||||
|
||||
// Limit errors
|
||||
if (isLimitError(error.code)) {
|
||||
switch (error.code) {
|
||||
case ErrorCode.StorageSpaceNotEnough:
|
||||
return 'Storage space limit reached. Please upgrade your plan.';
|
||||
case ErrorCode.PayloadTooLarge:
|
||||
return 'The file or content is too large.';
|
||||
case ErrorCode.UploadFileTooLarge:
|
||||
return 'File size exceeds the maximum allowed. Please upgrade for larger uploads.';
|
||||
case ErrorCode.WorkspaceLimitExceeded:
|
||||
return 'Workspace limit reached. Please upgrade your plan.';
|
||||
case ErrorCode.FreePlanGuestLimitExceeded:
|
||||
return 'Guest limit reached for free plan. Please upgrade to add more guests.';
|
||||
default:
|
||||
return 'Limit exceeded. Please upgrade your plan or reduce usage.';
|
||||
}
|
||||
}
|
||||
|
||||
// Validation errors
|
||||
if (isValidationError(error.code)) {
|
||||
switch (error.code) {
|
||||
case ErrorCode.InvalidEmail:
|
||||
return 'Please enter a valid email address.';
|
||||
case ErrorCode.InvalidRequest:
|
||||
return 'Invalid request. Please check your input.';
|
||||
case ErrorCode.InvalidInvitationCode:
|
||||
return 'Invalid or expired invitation code.';
|
||||
case ErrorCode.PublishNameTooLong:
|
||||
return 'The publish name is too long. Please use a shorter name.';
|
||||
default:
|
||||
return 'Invalid input. Please check and try again.';
|
||||
}
|
||||
}
|
||||
|
||||
// Service/Network errors
|
||||
switch (error.code) {
|
||||
case ErrorCode.ServiceTemporaryUnavailable:
|
||||
return 'Service temporarily unavailable. Please try again later.';
|
||||
case ErrorCode.TooManyRequests:
|
||||
return 'Too many requests. Please wait a moment and try again.';
|
||||
case ErrorCode.RequestTimeout:
|
||||
return 'Request timed out. Please try again.';
|
||||
case ErrorCode.NetworkError:
|
||||
return 'Network error. Please check your connection.';
|
||||
}
|
||||
|
||||
// Special cases
|
||||
switch (error.code) {
|
||||
case ErrorCode.UpgradeRequired:
|
||||
return 'Please upgrade to the latest version to continue.';
|
||||
case ErrorCode.LicenseExpired:
|
||||
return 'Your license has expired. Please renew to continue.';
|
||||
case ErrorCode.FeatureNotAvailable:
|
||||
return 'This feature is not available in your current plan.';
|
||||
case ErrorCode.AIServiceUnavailable:
|
||||
return 'AI service is currently unavailable. Please try again later.';
|
||||
case ErrorCode.ImportError:
|
||||
return 'Import failed. Please check your file and try again.';
|
||||
case ErrorCode.ExportError:
|
||||
return 'Export failed. Please try again.';
|
||||
default:
|
||||
// Fall back to server message if available, otherwise generic message
|
||||
return error.message || 'An unexpected error occurred. Please try again.';
|
||||
}
|
||||
};
|
||||
|
||||
// Main error handler
|
||||
export class ApiErrorHandler {
|
||||
private static instance: ApiErrorHandler;
|
||||
|
||||
static getInstance(): ApiErrorHandler {
|
||||
if (!ApiErrorHandler.instance) {
|
||||
ApiErrorHandler.instance = new ApiErrorHandler();
|
||||
}
|
||||
|
||||
return ApiErrorHandler.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API error response
|
||||
*/
|
||||
async handleError(
|
||||
error: AppResponseError,
|
||||
options: {
|
||||
showNotification?: boolean;
|
||||
throwError?: boolean;
|
||||
customMessage?: string;
|
||||
onAuthError?: () => void;
|
||||
onPermissionError?: () => void;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const {
|
||||
showNotification = true,
|
||||
throwError = true,
|
||||
customMessage,
|
||||
onAuthError,
|
||||
onPermissionError,
|
||||
} = options;
|
||||
|
||||
// Log error for debugging
|
||||
console.error(`API Error [${error.code}]: ${error.message}`);
|
||||
|
||||
// Handle authentication errors
|
||||
if (isAuthError(error.code)) {
|
||||
invalidToken();
|
||||
if (onAuthError) {
|
||||
onAuthError();
|
||||
} else {
|
||||
// Default: redirect to login
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle permission errors
|
||||
if (isPermissionError(error.code)) {
|
||||
if (onPermissionError) {
|
||||
onPermissionError();
|
||||
}
|
||||
|
||||
if (showNotification) {
|
||||
notify.error(customMessage || getUserFriendlyMessage(error));
|
||||
}
|
||||
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Show user-friendly notification
|
||||
if (showNotification) {
|
||||
const message = customMessage || getUserFriendlyMessage(error);
|
||||
|
||||
if (isLimitError(error.code)) {
|
||||
// For limit errors, show warning with upgrade prompt
|
||||
notify.warning(message);
|
||||
} else if (isValidationError(error.code)) {
|
||||
// For validation errors, show as warning
|
||||
notify.warning(message);
|
||||
} else {
|
||||
// For other errors, show as error
|
||||
notify.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Throw error if requested
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap API call with error handling
|
||||
*/
|
||||
async wrapApiCall<T>(
|
||||
apiCall: () => Promise<T>,
|
||||
options?: Parameters<typeof this.handleError>[1]
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await apiCall();
|
||||
} catch (error: unknown) {
|
||||
// Check if it's an API response error
|
||||
if ((error as AppResponseError)?.code !== undefined) {
|
||||
await this.handleError(error as AppResponseError, options);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiErrorHandler = ApiErrorHandler.getInstance();
|
||||
|
||||
// Helper function for easy use
|
||||
export const handleApiError = (
|
||||
error: AppResponseError,
|
||||
options?: Parameters<ApiErrorHandler['handleError']>[1]
|
||||
) => {
|
||||
return apiErrorHandler.handleError(error, options);
|
||||
};
|
||||
@@ -1,13 +1,10 @@
|
||||
import { RepeatedChatMessage } from '@appflowyinc/ai-chat';
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
import { omit } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { GlobalComment, Reaction } from '@/application/comment.type';
|
||||
import { apiGet, apiPostVoid } from '@/application/services/js-services/http/api-client';
|
||||
import { ApiResponse } from '@/application/services/js-services/http/api-utils';
|
||||
import { apiErrorHandler, AppResponseError, ErrorCode } from '@/application/services/js-services/http/error-handler';
|
||||
import { initGrantService, refreshToken } from '@/application/services/js-services/http/gotrue';
|
||||
import { blobToBytes } from '@/application/services/js-services/http/utils';
|
||||
import { AFCloudConfig } from '@/application/services/services.type';
|
||||
@@ -60,6 +57,7 @@ import {
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
} from '@/application/types';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
export * from './gotrue';
|
||||
|
||||
@@ -120,117 +118,28 @@ export function initAPIService(config: AFCloudConfig) {
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
async (response) => {
|
||||
const status = response.status;
|
||||
axiosInstance.interceptors.response.use(async (response) => {
|
||||
const status = response.status;
|
||||
|
||||
if (status === 401) {
|
||||
const token = getTokenParsed();
|
||||
if (status === 401) {
|
||||
const token = getTokenParsed();
|
||||
|
||||
if (!token) {
|
||||
invalidToken();
|
||||
return response;
|
||||
}
|
||||
|
||||
const refresh_token = token.refresh_token;
|
||||
|
||||
try {
|
||||
await refreshToken(refresh_token);
|
||||
} catch (e) {
|
||||
invalidToken();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this looks like an ApiResponse
|
||||
// ApiResponse should have a 'code' field that's a number
|
||||
const responseData = response.data;
|
||||
const isApiResponse = responseData &&
|
||||
typeof responseData === 'object' &&
|
||||
'code' in responseData &&
|
||||
typeof responseData.code === 'number';
|
||||
|
||||
if (!isApiResponse) {
|
||||
// Not an API response, check HTTP status to determine success
|
||||
if (response.status !== 200) {
|
||||
// Non-200 status for non-API response
|
||||
const error: AppResponseError = {
|
||||
code: ErrorCode.Unhandled,
|
||||
message: `HTTP ${response.status}: ${response.statusText}`
|
||||
};
|
||||
|
||||
const showNotification = (response.config as AxiosRequestConfig & { showNotification?: boolean })?.showNotification ?? false;
|
||||
|
||||
await apiErrorHandler.handleError(error, { showNotification });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Status 200 - success, return data as-is
|
||||
response.data = responseData;
|
||||
if (!token) {
|
||||
invalidToken();
|
||||
return response;
|
||||
}
|
||||
|
||||
// Now we know it's an ApiResponse
|
||||
const apiResponse = responseData as ApiResponse;
|
||||
const refresh_token = token.refresh_token;
|
||||
|
||||
// Handle error codes
|
||||
if (apiResponse.code !== ErrorCode.Ok) {
|
||||
const error: AppResponseError = {
|
||||
code: Object.values(ErrorCode).includes(apiResponse.code) ? apiResponse.code : ErrorCode.Unhandled,
|
||||
message: apiResponse.message || 'An error occurred'
|
||||
};
|
||||
|
||||
// Check if this request wants to show notifications
|
||||
const showNotification = (response.config as AxiosRequestConfig & { showNotification?: boolean })?.showNotification ?? false;
|
||||
|
||||
await apiErrorHandler.handleError(error, { showNotification });
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Success - return just the data portion
|
||||
// For void responses, data will be undefined which is fine
|
||||
response.data = apiResponse.data;
|
||||
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
// Handle network errors or non-2xx status codes
|
||||
if (error.response?.status === 401) {
|
||||
try {
|
||||
await refreshToken(refresh_token);
|
||||
} catch (e) {
|
||||
invalidToken();
|
||||
}
|
||||
|
||||
const showNotification = (error.config as AxiosRequestConfig & { showNotification?: boolean })?.showNotification ?? false;
|
||||
|
||||
// If we got a response, try to parse it as ApiResponse
|
||||
if (error.response?.data) {
|
||||
const responseData = error.response.data;
|
||||
const isApiResponse = responseData &&
|
||||
typeof responseData === 'object' &&
|
||||
'code' in responseData &&
|
||||
typeof responseData.code === 'number';
|
||||
|
||||
if (isApiResponse) {
|
||||
const apiResponse = responseData;
|
||||
const appError: AppResponseError = {
|
||||
code: Object.values(ErrorCode).includes(apiResponse.code) ? apiResponse.code : ErrorCode.Unhandled,
|
||||
message: apiResponse.message || 'An error occurred'
|
||||
};
|
||||
|
||||
await apiErrorHandler.handleError(appError, { showNotification });
|
||||
return Promise.reject(appError);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for network errors or unparseable responses
|
||||
const appError: AppResponseError = {
|
||||
code: ErrorCode.NetworkError,
|
||||
message: error.message || 'Network error occurred'
|
||||
};
|
||||
|
||||
await apiErrorHandler.handleError(appError, { showNotification });
|
||||
return Promise.reject(appError);
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
export async function signInWithUrl(url: string) {
|
||||
@@ -272,45 +181,76 @@ export async function signInWithUrl(url: string) {
|
||||
|
||||
export async function verifyToken(accessToken: string) {
|
||||
const url = `/api/user/verify/${accessToken}`;
|
||||
|
||||
return await apiGet<{ is_new: boolean }>(url);
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
is_new: boolean;
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0 && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<User> {
|
||||
const url = '/api/user/profile';
|
||||
|
||||
const data = await apiGet<{
|
||||
uid: number;
|
||||
uuid: string;
|
||||
email: string;
|
||||
name: string;
|
||||
metadata: Record<string, unknown>;
|
||||
latest_workspace_id: string;
|
||||
updated_at: number;
|
||||
const response = await axiosInstance?.get<{
|
||||
code: number;
|
||||
data?: {
|
||||
uid: number;
|
||||
uuid: string;
|
||||
email: string;
|
||||
name: string;
|
||||
metadata: Record<string, unknown>;
|
||||
encryption_sign: null;
|
||||
latest_workspace_id: string;
|
||||
updated_at: number;
|
||||
};
|
||||
message: string;
|
||||
}>(url);
|
||||
|
||||
const { uid, uuid, email, name, metadata } = data;
|
||||
const data = response?.data;
|
||||
|
||||
return {
|
||||
uid: String(uid),
|
||||
uuid,
|
||||
email,
|
||||
name,
|
||||
avatar: (metadata?.icon_url as string) || null,
|
||||
latestWorkspaceId: data.latest_workspace_id,
|
||||
metadata: metadata || {},
|
||||
};
|
||||
if (data?.code === 0 && data.data) {
|
||||
const { uid, uuid, email, name, metadata } = data.data;
|
||||
|
||||
return {
|
||||
uid: String(uid),
|
||||
uuid,
|
||||
email,
|
||||
name,
|
||||
avatar: (metadata?.icon_url as string) || null,
|
||||
latestWorkspaceId: data.data.latest_workspace_id,
|
||||
metadata: metadata || {},
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
|
||||
export async function updateUserProfile(metadata: Record<string, unknown>): Promise<void> {
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
const url = 'api/user/update';
|
||||
const response = await axiosInstance?.post<{
|
||||
code: number;
|
||||
message: string;
|
||||
}>(url, {
|
||||
metadata
|
||||
});
|
||||
|
||||
const data = response?.data;
|
||||
|
||||
if (data?.code === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = 'api/user/update';
|
||||
|
||||
await apiPostVoid(url, { metadata });
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
interface AFWorkspace {
|
||||
@@ -1755,10 +1695,28 @@ export async function uploadFile(
|
||||
) {
|
||||
const url = `/api/file_storage/${workspaceId}/v1/blob/${viewId}`;
|
||||
|
||||
// Check file size, if over 7MB, check subscription plan
|
||||
if (file.size > 7 * 1024 * 1024) {
|
||||
const plan = await getActiveSubscription(workspaceId);
|
||||
|
||||
if (plan?.length === 0 || plan?.[0] === SubscriptionPlan.Free) {
|
||||
notify.error('Your file is over 7 MB limit of the Free plan. Upgrade for unlimited uploads.');
|
||||
|
||||
return Promise.reject({
|
||||
code: 413,
|
||||
message: 'File size is too large. Please upgrade your plan for unlimited uploads.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const axiosResponse = await axiosInstance?.put<ApiResponse<{
|
||||
file_id: string;
|
||||
}>>(url, file, {
|
||||
const response = await axiosInstance?.put<{
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
file_id: string;
|
||||
};
|
||||
}>(url, file, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const { progress = 0 } = progressEvent;
|
||||
|
||||
@@ -1769,28 +1727,26 @@ export async function uploadFile(
|
||||
},
|
||||
});
|
||||
|
||||
const response = axiosResponse?.data;
|
||||
|
||||
if (response?.code === ErrorCode.Ok && response?.data) {
|
||||
if (response?.data.code === 0) {
|
||||
const baseURL = axiosInstance?.defaults.baseURL;
|
||||
const fileUrl = `${baseURL}/api/file_storage/${workspaceId}/v1/blob/${viewId}/${response.data.file_id}`;
|
||||
const url = `${baseURL}/api/file_storage/${workspaceId}/v1/blob/${viewId}/${response?.data.data.file_id}`;
|
||||
|
||||
return fileUrl;
|
||||
return url;
|
||||
}
|
||||
|
||||
return Promise.reject(response);
|
||||
return Promise.reject(response?.data);
|
||||
// eslint-disable-next-line
|
||||
} catch (e: any) {
|
||||
if (e.response?.status === 413) {
|
||||
if (e.response.status === 413) {
|
||||
return Promise.reject({
|
||||
code: ErrorCode.PayloadTooLarge,
|
||||
code: 413,
|
||||
message: 'File size is too large. Please upgrade your plan for unlimited uploads.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject({
|
||||
code: ErrorCode.Unhandled,
|
||||
code: -1,
|
||||
message: 'Upload file failed.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AppResponseError, ErrorCode, apiErrorHandler } from './error-handler';
|
||||
|
||||
/**
|
||||
* React hook for handling API errors in components
|
||||
*/
|
||||
export function useApiError() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleError = useCallback(
|
||||
async (
|
||||
error: AppResponseError,
|
||||
options?: {
|
||||
showNotification?: boolean;
|
||||
customMessage?: string;
|
||||
}
|
||||
) => {
|
||||
return apiErrorHandler.handleError(error, {
|
||||
...options,
|
||||
onAuthError: () => {
|
||||
// Redirect to login on auth errors
|
||||
navigate('/login');
|
||||
},
|
||||
onPermissionError: () => {
|
||||
// Could show upgrade modal or permission denied page
|
||||
console.warn('Permission denied:', error.message);
|
||||
},
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const wrapApiCall = useCallback(
|
||||
async <T,>(
|
||||
apiCall: () => Promise<T>,
|
||||
options?: Parameters<typeof handleError>[1]
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await apiCall();
|
||||
} catch (error: unknown) {
|
||||
if ((error as AppResponseError)?.code !== undefined) {
|
||||
await handleError(error as AppResponseError, options);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[handleError]
|
||||
);
|
||||
|
||||
return {
|
||||
handleError,
|
||||
wrapApiCall,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for handling specific error types
|
||||
*/
|
||||
export function useApiErrorHandler() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAuthError = useCallback(() => {
|
||||
navigate('/login');
|
||||
}, [navigate]);
|
||||
|
||||
const handlePermissionError = useCallback(() => {
|
||||
// Show permission denied message or upgrade prompt
|
||||
console.warn('Permission denied');
|
||||
}, []);
|
||||
|
||||
const handleResourceNotFound = useCallback(() => {
|
||||
navigate('/404');
|
||||
}, [navigate]);
|
||||
|
||||
const handleLimitExceeded = useCallback(() => {
|
||||
// Show upgrade modal
|
||||
console.info('Limit exceeded - show upgrade prompt');
|
||||
}, []);
|
||||
|
||||
const handleApiError = useCallback(
|
||||
(error: AppResponseError) => {
|
||||
switch (error.code) {
|
||||
case ErrorCode.UserUnAuthorized:
|
||||
case ErrorCode.NotLoggedIn:
|
||||
handleAuthError();
|
||||
break;
|
||||
case ErrorCode.NotEnoughPermissions:
|
||||
case ErrorCode.NotEnoughPermissionToWrite:
|
||||
case ErrorCode.NotEnoughPermissionToRead:
|
||||
handlePermissionError();
|
||||
break;
|
||||
case ErrorCode.RecordNotFound:
|
||||
case ErrorCode.MissingView:
|
||||
handleResourceNotFound();
|
||||
break;
|
||||
case ErrorCode.StorageSpaceNotEnough:
|
||||
case ErrorCode.WorkspaceLimitExceeded:
|
||||
case ErrorCode.PayloadTooLarge:
|
||||
case ErrorCode.UploadFileTooLarge:
|
||||
handleLimitExceeded();
|
||||
break;
|
||||
default:
|
||||
// Let the global error handler deal with it
|
||||
void apiErrorHandler.handleError(error);
|
||||
}
|
||||
},
|
||||
[handleAuthError, handlePermissionError, handleResourceNotFound, handleLimitExceeded]
|
||||
);
|
||||
|
||||
return {
|
||||
handleApiError,
|
||||
handleAuthError,
|
||||
handlePermissionError,
|
||||
handleResourceNotFound,
|
||||
handleLimitExceeded,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user