mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-05 02:41:34 +00:00
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:
parent
b1d40f3409
commit
f1420338f2
4 changed files with 70 additions and 1 deletions
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
32
backend/open_webui/utils/errors.py
Normal file
32
backend/open_webui/utils/errors.py
Normal 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
|
||||
Loading…
Add table
Reference in a new issue