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.
This commit is contained in:
zydo 2026-06-11 07:28:59 -07:00
parent b1d40f3409
commit f1420338f2
4 changed files with 70 additions and 1 deletions

View file

@ -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.'

View file

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

View file

@ -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'),
)

View file

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