Upload: clearer proxy 413 error, clean up after interrupted uploads

- Show a readable message when a reverse proxy rejects the archive with 413
- Delete the freshly created game if its archive upload fails
- Turn ClientDisconnect during upload into a short 400 instead of a traceback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-25 03:30:43 +03:00
1 parent a97b46079e
commit 5d9ddb6376
3 files changed
+12 -3

No files matched your search

+5 -1
View File
@@ -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)
+3 -1
View File
@@ -98,7 +98,9 @@ export function uploadRaw<T>(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);
+4 -1
View File
@@ -16,8 +16,9 @@ export function NewGame() {
if (!file) return;
setError("");
setProgress(0);
let game: Game | null = null;
try {
const game = await api.post<Game>("/api/games", {
game = await api.post<Game>("/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);
}