🐛 fix: allow zero-byte files and add business hooks for error handling (#11283)

This commit is contained in:
YuTengjing
2026-01-06 21:15:45 +08:00
committed by GitHub
parent 71dd9c7a02
commit 38f5b78e2a
6 changed files with 66 additions and 13 deletions

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

View 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 {};
}

View 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> {}

View File

@@ -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 {

View File

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

View File

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