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:
Nathan.fooo
2025-09-08 16:47:11 +08:00
committed by GitHub
7 changed files with 99 additions and 1094 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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,
};
}

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -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.',
});
}

View File

@@ -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,
};
}