diff --git a/backend/src/api/skins_routes.py b/backend/src/api/skins_routes.py index e014a078..903c214b 100644 --- a/backend/src/api/skins_routes.py +++ b/backend/src/api/skins_routes.py @@ -797,23 +797,25 @@ def get_thumbnail( _OWNER_HIDDEN_FILES = frozenset({"meta.json", "review_sheet.png", "name_preview.png"}) -@skins_router.get("/submissions/{submission_id}/files/{filename}") +# The name is a query parameter: Cloudflare caches paths ending in .png by URL alone, +# which would hand one response (a 401 or 404 included) to every later request. +@skins_router.get("/submissions/{submission_id}/files") def owner_file( submission_id: str, - filename: str, + name: str, authorization: str | None = Header(default=None), ): """A texture or model the owner uploaded, for the 3D viewer on their skin's page.""" session = _session_from_auth(authorization) - name = filename.lower() - if name in _OWNER_HIDDEN_FILES or not name.endswith(_OWNER_FILE_SUFFIXES): + lowered = name.lower() + if lowered in _OWNER_HIDDEN_FILES or not lowered.endswith(_OWNER_FILE_SUFFIXES): raise HTTPException(status_code=404, detail="File not found") if get_submission_for_owner(submission_id, session["player_uuid"]) is None: raise HTTPException(status_code=404, detail="Submission not found") - path = _resolve_submission_file_safely(submission_id, filename) + path = _resolve_submission_file_safely(submission_id, name) if path is None: raise HTTPException(status_code=404, detail="File not found") - media = "image/png" if name.endswith(".png") else "application/json" + media = "image/png" if lowered.endswith(".png") else "application/json" return FileResponse(path, media_type=media, headers={"Cache-Control": "private, max-age=300"}) diff --git a/backend/src/api/test_skin_owner_files.py b/backend/src/api/test_skin_owner_files.py index 02c38db0..915c9cc9 100644 --- a/backend/src/api/test_skin_owner_files.py +++ b/backend/src/api/test_skin_owner_files.py @@ -1,4 +1,4 @@ -"""GET /skins/submissions/{id}/files/{name} serves a skin's uploads to its owner only.""" +"""GET /skins/submissions/{id}/files?name= serves a skin's uploads to its owner only.""" from __future__ import annotations diff --git a/frontend/lib/skins/api.ts b/frontend/lib/skins/api.ts index d8e3b06d..4b868790 100644 --- a/frontend/lib/skins/api.ts +++ b/frontend/lib/skins/api.ts @@ -431,7 +431,7 @@ export async function getSkinThumbnail(id: string, sessionToken: string): Promis /** One of the owner's uploaded textures or models, or null when the skin has no such file. */ export async function getSubmissionFile(id: string, filename: string, sessionToken: string): Promise { const res = await apiFetch( - `${getApiBase()}/skins/submissions/${encodeURIComponent(id)}/files/${encodeURIComponent(filename)}`, + `${getApiBase()}/skins/submissions/${encodeURIComponent(id)}/files?name=${encodeURIComponent(filename)}`, { headers: { Authorization: `Bearer ${sessionToken}` } }, ); if (!res.ok) return null;