diff --git a/backend/app/routers/versions.py b/backend/app/routers/versions.py index 3c0ed6f..00f3557 100644 --- a/backend/app/routers/versions.py +++ b/backend/app/routers/versions.py @@ -2,6 +2,7 @@ from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy import select +from starlette.requests import ClientDisconnect from app.deps import DB, CurrentUser, has_perm, require from app.models import GameVersion, Save, User @@ -57,10 +58,13 @@ async def upload_version( async for chunk in request.stream(): f.write(chunk) size += len(chunk) - except Exception: + except Exception as e: remove_upload(version.id) await db.delete(version) await db.commit() + if isinstance(e, ClientDisconnect): + # usually a proxy in front cut the body off (e.g. its own 413) + raise HTTPException(status.HTTP_400_BAD_REQUEST, "Загрузка прервана") from None raise if size == 0: remove_upload(version.id) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 334ff4f..da30927 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -98,7 +98,9 @@ export function uploadRaw(method: string, url: string, file: Blob, /* empty */ } if (xhr.status >= 200 && xhr.status < 300) resolve(data); - else reject(new ApiError(xhr.status, data?.detail ?? xhr.statusText)); + else if (xhr.status === 413 && !data?.detail) + reject(new ApiError(413, "Файл слишком большой для прокси перед сервером (client_max_body_size)")); + else reject(new ApiError(xhr.status, data?.detail ?? (xhr.statusText || `Ошибка ${xhr.status}`))); }; xhr.onerror = () => reject(new ApiError(0, "Сетевая ошибка")); xhr.send(file); diff --git a/frontend/src/pages/NewGame.tsx b/frontend/src/pages/NewGame.tsx index cdd4d60..45de561 100644 --- a/frontend/src/pages/NewGame.tsx +++ b/frontend/src/pages/NewGame.tsx @@ -16,8 +16,9 @@ export function NewGame() { if (!file) return; setError(""); setProgress(0); + let game: Game | null = null; try { - const game = await api.post("/api/games", { + game = await api.post("/api/games", { title: title.trim() || file.name.replace(/\.(zip|7z|rar|tar.*)$/i, ""), description, tags: tags.split(",").map((t) => t.trim()).filter(Boolean), @@ -26,6 +27,8 @@ export function NewGame() { file, setProgress); navigate(`/games/${game.id}`); } catch (err) { + // don't leave an empty game card behind when the archive never arrived + if (game) await api.del(`/api/games/${game.id}`).catch(() => {}); setError((err as Error).message); setProgress(null); }