mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Fix WebSocket disconnection when uploading large files (#9504)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
ac2947b7ff
commit
b3c8b7c089
@ -32,6 +32,7 @@ import { ErrorMessageBanner } from "./error-message-banner";
|
||||
import { shouldRenderEvent } from "./event-content-helpers/should-render-event";
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
@ -92,9 +93,12 @@ export function ChatInterface() {
|
||||
|
||||
const handleSendMessage = async (
|
||||
content: string,
|
||||
images: File[],
|
||||
files: File[],
|
||||
originalImages: File[],
|
||||
originalFiles: File[],
|
||||
) => {
|
||||
// Create mutable copies of the arrays
|
||||
const images = [...originalImages];
|
||||
const files = [...originalFiles];
|
||||
if (events.length === 0) {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
entry_point: getEntryPoint(
|
||||
@ -110,6 +114,16 @@ export function ChatInterface() {
|
||||
current_message_length: content.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate file sizes before any processing
|
||||
const allFiles = [...images, ...files];
|
||||
const validation = validateFiles(allFiles);
|
||||
|
||||
if (!validation.isValid) {
|
||||
displayErrorToast(`Error: ${validation.errorMessage}`);
|
||||
return; // Stop processing if validation fails
|
||||
}
|
||||
|
||||
const promises = images.map((image) => convertImageToBase64(image));
|
||||
const imageUrls = await Promise.all(promises);
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import { ImageCarousel } from "../images/image-carousel";
|
||||
import { UploadImageInput } from "../images/upload-image-input";
|
||||
import { FileList } from "../files/file-list";
|
||||
import { isFileImage } from "#/utils/is-file-image";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
|
||||
interface InteractiveChatBoxProps {
|
||||
isDisabled?: boolean;
|
||||
@ -27,14 +29,20 @@ export function InteractiveChatBox({
|
||||
const [files, setFiles] = React.useState<File[]>([]);
|
||||
|
||||
const handleUpload = (selectedFiles: File[]) => {
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
...selectedFiles.filter((f) => !isFileImage(f)),
|
||||
]);
|
||||
setImages((prevImages) => [
|
||||
...prevImages,
|
||||
...selectedFiles.filter((f) => isFileImage(f)),
|
||||
]);
|
||||
// Validate files before adding them
|
||||
const validation = validateFiles(selectedFiles, [...images, ...files]);
|
||||
|
||||
if (!validation.isValid) {
|
||||
displayErrorToast(`Error: ${validation.errorMessage}`);
|
||||
return; // Don't add any files if validation fails
|
||||
}
|
||||
|
||||
// Filter valid files by type
|
||||
const validFiles = selectedFiles.filter((f) => !isFileImage(f));
|
||||
const validImages = selectedFiles.filter((f) => isFileImage(f));
|
||||
|
||||
setFiles((prevFiles) => [...prevFiles, ...validFiles]);
|
||||
setImages((prevImages) => [...prevImages, ...validImages]);
|
||||
};
|
||||
|
||||
const removeElementByIndex = (array: Array<File>, index: number) => {
|
||||
|
||||
70
frontend/src/utils/file-validation.ts
Normal file
70
frontend/src/utils/file-validation.ts
Normal file
@ -0,0 +1,70 @@
|
||||
export const MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB maximum file size
|
||||
export const MAX_TOTAL_SIZE = 3 * 1024 * 1024; // 3MB maximum total size for all files combined
|
||||
|
||||
export interface FileValidationResult {
|
||||
isValid: boolean;
|
||||
errorMessage?: string;
|
||||
oversizedFiles?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates individual file sizes
|
||||
*/
|
||||
export function validateIndividualFileSizes(
|
||||
files: File[],
|
||||
): FileValidationResult {
|
||||
const oversizedFiles = files.filter((file) => file.size > MAX_FILE_SIZE);
|
||||
|
||||
if (oversizedFiles.length > 0) {
|
||||
const fileNames = oversizedFiles.map((f) => f.name);
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: `Files exceeding 3MB are not allowed: ${fileNames.join(", ")}`,
|
||||
oversizedFiles: fileNames,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates total file size including existing files
|
||||
*/
|
||||
export function validateTotalFileSize(
|
||||
newFiles: File[],
|
||||
existingFiles: File[] = [],
|
||||
): FileValidationResult {
|
||||
const currentTotalSize = existingFiles.reduce(
|
||||
(sum, file) => sum + file.size,
|
||||
0,
|
||||
);
|
||||
const newFilesSize = newFiles.reduce((sum, file) => sum + file.size, 0);
|
||||
const totalSize = currentTotalSize + newFilesSize;
|
||||
|
||||
if (totalSize > MAX_TOTAL_SIZE) {
|
||||
const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(1);
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: `Total file size would be ${totalSizeMB}MB, exceeding the 3MB limit. Please select fewer or smaller files.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates both individual and total file sizes
|
||||
*/
|
||||
export function validateFiles(
|
||||
newFiles: File[],
|
||||
existingFiles: File[] = [],
|
||||
): FileValidationResult {
|
||||
// First check individual file sizes
|
||||
const individualValidation = validateIndividualFileSizes(newFiles);
|
||||
if (!individualValidation.isValid) {
|
||||
return individualValidation;
|
||||
}
|
||||
|
||||
// Then check total size
|
||||
return validateTotalFileSize(newFiles, existingFiles);
|
||||
}
|
||||
@ -43,7 +43,11 @@ if redis_host:
|
||||
|
||||
|
||||
sio = socketio.AsyncServer(
|
||||
async_mode='asgi', cors_allowed_origins='*', client_manager=client_manager
|
||||
async_mode='asgi',
|
||||
cors_allowed_origins='*',
|
||||
client_manager=client_manager,
|
||||
# Increase buffer size to 4MB (to handle 3MB files with base64 overhead)
|
||||
max_http_buffer_size=4 * 1024 * 1024,
|
||||
)
|
||||
|
||||
MonitoringListenerImpl = get_impl(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user