fix(upload): surface payload-too-large errors, reject empty files, use toast not chat-log
Root cause of "uploads silently land as 0-byte __file": aiohttp default body limit is 1 MiB; PDFs almost always exceed it, request.json() raises HTTPRequestEntityTooLarge, the generic read_json() swallows it as bare Exception, upload_handler then sees an empty dict and happily writes an empty file with name='file', returning ok=true. Front-end thinks the upload worked. Bridge: - Set web.Application(client_max_size=1MB) explicitly so the limit is visible at the call site (kept at 1MB intentionally for now; bump later if larger uploads are needed) - upload_handler reads body itself instead of read_json so it can catch HTTPRequestEntityTooLarge and HTTPBadRequest and return a real error to the client instead of silently writing 0 bytes - Reject blob == b'' as a second line of defence Frontend: - addFiles skips files with size === 0 (handles drag-folder ghost File and edge-case clipboard items) with a dedicated 'upload.empty' toast - 4x showSystem(...) in addFiles -> showChanToast(title, detail, 'err'). Upload failures should be transient toasts, not permanent system messages glued into the chat transcript.
This commit is contained in:
1 parent
22d4654e85
commit
e2a67f0803
2 files changed
+19
-7
No files matched your search
@@ -269,7 +269,7 @@ const I18N = {
|
||||
'slash.unknown': '未知命令',
|
||||
'upload.hint': '上传文件:选择 / 拖拽 / 粘贴',
|
||||
'upload.button': '上传文件',
|
||||
'upload.tooLarge': '文件过大或数量超限',
|
||||
'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件',
|
||||
'upload.failed': '上传失败',
|
||||
'upload.removeTitle': '移除',
|
||||
'upload.dropHint': '松开以上传文件',
|
||||
@@ -362,7 +362,7 @@ const I18N = {
|
||||
'slash.unknown': 'Unknown command',
|
||||
'upload.hint': 'Upload file: pick / drag / paste',
|
||||
'upload.button': 'Upload file',
|
||||
'upload.tooLarge': 'File too large or limit reached',
|
||||
'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file',
|
||||
'upload.failed': 'Upload failed',
|
||||
'upload.removeTitle': 'Remove',
|
||||
'upload.dropHint': 'Drop to upload files',
|
||||
@@ -1572,14 +1572,17 @@ async function addFiles(fileList) {
|
||||
const files = Array.from(fileList || []);
|
||||
if (files.length === 0) return;
|
||||
let skipped = false;
|
||||
let emptyHit = false;
|
||||
const accepted = [];
|
||||
for (const f of files) {
|
||||
if (!f || f.size === 0) { emptyHit = true; continue; }
|
||||
if (f.size > MAX_UPLOAD_BYTES) { skipped = true; continue; }
|
||||
if (state.pendingFiles.length + accepted.length >= MAX_UPLOAD_FILES) { skipped = true; break; }
|
||||
accepted.push(f);
|
||||
}
|
||||
if (emptyHit) showChanToast(t('upload.empty'), '', 'err');
|
||||
if (accepted.length === 0) {
|
||||
if (skipped) showSystem(t('upload.tooLarge'));
|
||||
if (skipped) showChanToast(t('upload.tooLarge'), '', 'err');
|
||||
return;
|
||||
}
|
||||
for (const f of accepted) {
|
||||
@@ -1602,10 +1605,10 @@ async function addFiles(fileList) {
|
||||
insertPlaceholderInComposer(placeholderFor(entry));
|
||||
renderThumbStrip();
|
||||
} catch (e) {
|
||||
showSystem(t('upload.failed') + ': ' + (e.message || e));
|
||||
showChanToast(t('upload.failed'), e.message || String(e), 'err');
|
||||
}
|
||||
}
|
||||
if (skipped) showSystem(t('upload.tooLarge'));
|
||||
if (skipped) showChanToast(t('upload.tooLarge'), '', 'err');
|
||||
}
|
||||
|
||||
if (uploadBtn && imgInput) uploadBtn.addEventListener('click', (e) => {
|
||||
|
||||
@@ -1081,7 +1081,14 @@ async def upload_handler(request):
|
||||
Body: {name: "<original filename>", dataUrl: "data:<mime>;base64,<...>"}
|
||||
Returns: {ok: true, path: "<abs path>"}
|
||||
"""
|
||||
data = await read_json(request)
|
||||
try:
|
||||
data = await request.json()
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except web.HTTPRequestEntityTooLarge:
|
||||
return json_ok({"ok": False, "error": "file too large for bridge body limit"})
|
||||
except Exception as e:
|
||||
return json_ok({"ok": False, "error": f"invalid request: {e}"})
|
||||
name = (data.get("name") or "file").strip().replace("/", "_").replace("\\", "_")
|
||||
data_url = data.get("dataUrl") or ""
|
||||
if "," in data_url:
|
||||
@@ -1092,6 +1099,8 @@ async def upload_handler(request):
|
||||
blob = base64.b64decode(b64)
|
||||
except Exception as e:
|
||||
return json_ok({"ok": False, "error": f"decode failed: {e}"})
|
||||
if not blob:
|
||||
return json_ok({"ok": False, "error": "empty file"})
|
||||
safe_name = name or "file"
|
||||
fpath = _WEB_UPLOAD_DIR / f"{uuid.uuid4().hex[:12]}__{safe_name}"
|
||||
fpath.write_bytes(blob)
|
||||
@@ -1216,7 +1225,7 @@ async def token_stats_handler(request):
|
||||
|
||||
|
||||
def create_app():
|
||||
app = web.Application(middlewares=[cors_middleware])
|
||||
app = web.Application(middlewares=[cors_middleware], client_max_size=1 * 1024 * 1024)
|
||||
app.router.add_get("/ws", ws_handler)
|
||||
app.router.add_get("/status", status_handler)
|
||||
app.router.add_get("/config", get_config_handler)
|
||||
|
||||
Reference in new issue
Block a user