From f1420338f20318d9c417c4f9a98f20e2dd375c31 Mon Sep 17 00:00:00 2001 From: zydo Date: Thu, 11 Jun 2026 07:28:59 -0700 Subject: [PATCH] fix: surface user-readable upload errors instead of opaque stubs Uploading a file whose name exceeds the 255-byte filesystem limit failed with a generic "Error uploading file" while the real cause (ENAMETOOLONG) was only visible in server logs. Validate filename byte-length up front, translate recognized OS errors (ENOSPC, EACCES, ...) into safe messages, and add central handlers so unhandled exceptions return a loggable reference ID instead of leaking internals. --- backend/open_webui/constants.py | 4 ++++ backend/open_webui/main.py | 23 +++++++++++++++++++++ backend/open_webui/routers/files.py | 12 ++++++++++- backend/open_webui/utils/errors.py | 32 +++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 backend/open_webui/utils/errors.py diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index 132f3ac19a..9887e2c1e4 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -86,6 +86,10 @@ class ERROR_MESSAGES(str, Enum): f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}." ) + FILE_NAME_TOO_LONG = lambda max_bytes='': ( + f'File name is too long (maximum {max_bytes} bytes). Please rename the file and try again.' + ) + DUPLICATE_CONTENT = 'Duplicate content detected. Please provide unique content to proceed.' FILE_NOT_PROCESSED = ( 'Extracted content is not available for this file. Please ensure that the file is processed before proceeding.' diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 07d7583002..07487b4605 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -560,6 +560,7 @@ from open_webui.utils.chat import ( generate_chat_completion as chat_completion_handler, ) from open_webui.utils.embeddings import generate_embeddings +from open_webui.utils.errors import UserFacingError, translate_exception from open_webui.utils.logger import start_logger from open_webui.utils.middleware import ( build_chat_response_context, @@ -757,6 +758,28 @@ app = FastAPI( # Used by readiness checks to gate traffic until startup work is done. app.state.startup_complete = False + +@app.exception_handler(UserFacingError) +async def user_facing_error_handler(request: Request, exc: UserFacingError): + return JSONResponse( + status_code=exc.status_code, + content={'detail': exc.message}, + ) + + +@app.exception_handler(Exception) +async def unhandled_exception_handler(request: Request, exc: Exception): + # Full traceback stays in server logs; users get a safe message plus a + # reference ID an admin can grep for. + ref_id = uuid4().hex[:8] + log.exception(f'Unhandled error [ref: {ref_id}] on {request.method} {request.url.path}') + + detail = translate_exception(exc) or f'Something went wrong (ref: {ref_id})' + return JSONResponse( + status_code=500, + content={'detail': detail}, + ) + # For Open WebUI OIDC/OAuth2 oauth_manager = OAuthManager(app) app.state.oauth_manager = oauth_manager diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index dbf1ccb885..79e8b3931c 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -42,6 +42,7 @@ from open_webui.routers.audio import transcribe from open_webui.routers.retrieval import ProcessFileForm, process_file from open_webui.storage.provider import Storage from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.errors import translate_exception from open_webui.utils.misc import strict_match_mime_type from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession @@ -279,6 +280,15 @@ async def upload_file_handler( detail=ERROR_MESSAGES.DEFAULT(f'File type {file_extension} is not allowed'), ) + # Filesystem filename components are capped at 255 bytes; reserve room + # for the uuid4 prefix added below (36 chars + "_"). + max_filename_bytes = 255 - 37 + if len(filename.encode('utf-8')) > max_filename_bytes: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_NAME_TOO_LONG(max_filename_bytes), + ) + # replace filename with uuid id = str(uuid.uuid4()) name = filename @@ -364,7 +374,7 @@ async def upload_file_handler( log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT('Error uploading file'), + detail=ERROR_MESSAGES.DEFAULT(translate_exception(e) or 'Error uploading file'), ) diff --git a/backend/open_webui/utils/errors.py b/backend/open_webui/utils/errors.py new file mode 100644 index 0000000000..2a5b6947b0 --- /dev/null +++ b/backend/open_webui/utils/errors.py @@ -0,0 +1,32 @@ +import errno +from typing import Optional + + +class UserFacingError(Exception): + """An anticipated error whose message is safe to show to the user verbatim.""" + + def __init__(self, message: str, status_code: int = 400): + super().__init__(message) + self.message = message + self.status_code = status_code + + +OS_ERRNO_MESSAGES = { + errno.ENAMETOOLONG: 'File name is too long.', + errno.ENOSPC: 'The server is out of storage space.', + errno.EDQUOT: 'Server storage quota exceeded.', + errno.EACCES: 'Server storage is not writable.', + errno.EPERM: 'Server storage is not writable.', + errno.EROFS: 'Server storage is not writable.', +} + + +def translate_exception(e: Exception) -> Optional[str]: + """Map a recognized internal exception to a user-safe message. + + Returns None when the exception is not recognized; callers should then + fall back to a generic message and keep details in server logs only. + """ + if isinstance(e, OSError) and e.errno in OS_ERRNO_MESSAGES: + return OS_ERRNO_MESSAGES[e.errno] + return None