Fix WebSocket disconnection when uploading large files (#9504)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
sp.wack 2025-07-03 16:28:30 +04:00 committed by GitHub
parent ac2947b7ff
commit b3c8b7c089
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 11 deletions

View File

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

View File

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

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

View File

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