mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: allow zero-byte files and add business hooks for error handling (#11283)
This commit is contained in:
9
src/business/client/hooks/useBusinessErrorAlertConfig.ts
Normal file
9
src/business/client/hooks/useBusinessErrorAlertConfig.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { ErrorType } from '@lobechat/types';
|
||||
import type { AlertProps } from '@lobehub/ui';
|
||||
|
||||
export default function useBusinessErrorAlertConfig(
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
errorType?: ErrorType,
|
||||
): AlertProps | undefined {
|
||||
return undefined;
|
||||
}
|
||||
13
src/business/client/hooks/useBusinessErrorContent.ts
Normal file
13
src/business/client/hooks/useBusinessErrorContent.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ErrorType } from '@lobechat/types';
|
||||
|
||||
export interface BusinessErrorContentResult {
|
||||
errorType?: string;
|
||||
hideMessage?: boolean;
|
||||
}
|
||||
|
||||
export default function useBusinessErrorContent(
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
errorType?: ErrorType | string,
|
||||
): BusinessErrorContentResult {
|
||||
return {};
|
||||
}
|
||||
12
src/business/server/lambda-routers/file.ts
Normal file
12
src/business/server/lambda-routers/file.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface BusinessFileUploadCheckParams {
|
||||
actualSize: number;
|
||||
clientIp?: string;
|
||||
inputSize: number;
|
||||
url: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export async function businessFileUploadCheck(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
params: BusinessFileUploadCheckParams,
|
||||
): Promise<void> {}
|
||||
@@ -7,6 +7,8 @@ import dynamic from 'next/dynamic';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import useBusinessErrorAlertConfig from '@/business/client/hooks/useBusinessErrorAlertConfig';
|
||||
import useBusinessErrorContent from '@/business/client/hooks/useBusinessErrorContent';
|
||||
import useRenderBusinessChatErrorMessageExtra from '@/business/client/hooks/useRenderBusinessChatErrorMessageExtra';
|
||||
import ErrorContent from '@/features/Conversation/ChatItem/components/ErrorContent';
|
||||
import { useProviderName } from '@/hooks/useProviderName';
|
||||
@@ -85,18 +87,26 @@ const getErrorAlertConfig = (
|
||||
export const useErrorContent = (error: any) => {
|
||||
const { t } = useTranslation('error');
|
||||
const providerName = useProviderName(error?.body?.provider || '');
|
||||
const businessAlertConfig = useBusinessErrorAlertConfig(error?.type);
|
||||
const { errorType: businessErrorType, hideMessage } = useBusinessErrorContent(error?.type);
|
||||
|
||||
return useMemo<AlertProps | undefined>(() => {
|
||||
if (!error) return;
|
||||
const messageError = error;
|
||||
|
||||
const alertConfig = getErrorAlertConfig(messageError.type);
|
||||
// Use business alert config if provided, otherwise fall back to default
|
||||
const alertConfig = businessAlertConfig ?? getErrorAlertConfig(messageError.type);
|
||||
|
||||
// Use business error type if provided, otherwise use original
|
||||
const finalErrorType = businessErrorType ?? messageError.type;
|
||||
|
||||
return {
|
||||
message: t(`response.${messageError.type}` as any, { provider: providerName }),
|
||||
message: hideMessage
|
||||
? undefined
|
||||
: t(`response.${finalErrorType}` as any, { provider: providerName }),
|
||||
...alertConfig,
|
||||
};
|
||||
}, [error]);
|
||||
}, [businessAlertConfig, businessErrorType, error, hideMessage, providerName, t]);
|
||||
};
|
||||
|
||||
interface ErrorExtraProps {
|
||||
|
||||
@@ -273,7 +273,7 @@ describe('fileRouter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when getFileMetadata fails and input size is less than 1', async () => {
|
||||
it('should throw error when getFileMetadata fails and input size is negative', async () => {
|
||||
mockFileModelCheckHash.mockResolvedValue({ isExist: false });
|
||||
mockFileServiceGetFileMetadata.mockRejectedValue(new Error('File not found in S3'));
|
||||
|
||||
@@ -282,11 +282,11 @@ describe('fileRouter', () => {
|
||||
hash: 'test-hash',
|
||||
fileType: 'text',
|
||||
name: 'test.txt',
|
||||
size: 0,
|
||||
size: -1,
|
||||
url: 'files/non-existent.txt',
|
||||
metadata: {},
|
||||
}),
|
||||
).rejects.toThrow('File size must be at least 1 byte');
|
||||
).rejects.toThrow('File size cannot be negative');
|
||||
});
|
||||
|
||||
it('should use input size when getFileMetadata returns contentLength less than 1', async () => {
|
||||
@@ -315,10 +315,10 @@ describe('fileRouter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when both getFileMetadata contentLength and input size are less than 1', async () => {
|
||||
it('should throw error when both getFileMetadata contentLength and input size are negative', async () => {
|
||||
mockFileModelCheckHash.mockResolvedValue({ isExist: false });
|
||||
mockFileServiceGetFileMetadata.mockResolvedValue({
|
||||
contentLength: 0,
|
||||
contentLength: -1,
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
@@ -327,11 +327,11 @@ describe('fileRouter', () => {
|
||||
hash: 'test-hash',
|
||||
fileType: 'text',
|
||||
name: 'test.txt',
|
||||
size: 0,
|
||||
size: -1,
|
||||
url: 'files/test.txt',
|
||||
metadata: {},
|
||||
}),
|
||||
).rejects.toThrow('File size must be at least 1 byte');
|
||||
).rejects.toThrow('File size cannot be negative');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { businessFileUploadCheck } from '@/business/server/lambda-routers/file';
|
||||
import { checkFileStorageUsage } from '@/business/server/trpc-middlewares/lambda';
|
||||
import { serverDBEnv } from '@/config/db';
|
||||
import { AsyncTaskModel } from '@/database/models/asyncTask';
|
||||
@@ -74,8 +75,16 @@ export const fileRouter = router({
|
||||
// If metadata fetch fails, use original size from input
|
||||
}
|
||||
|
||||
if (actualSize < 1) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'File size must be at least 1 byte' });
|
||||
await businessFileUploadCheck({
|
||||
actualSize,
|
||||
clientIp: ctx.clientIp ?? undefined,
|
||||
inputSize: input.size,
|
||||
url: input.url,
|
||||
userId: ctx.userId,
|
||||
});
|
||||
|
||||
if (actualSize < 0) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'File size cannot be negative' });
|
||||
}
|
||||
|
||||
const { id } = await ctx.fileModel.create(
|
||||
@@ -367,7 +376,7 @@ export const fileRouter = router({
|
||||
|
||||
if (!file) return;
|
||||
|
||||
// delele the file from remove from S3 if it is not used by other files
|
||||
// delete the file from S3 if it is not used by other files
|
||||
await ctx.fileService.deleteFile(file.url!);
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user