fix: make screenshot retry idempotent
CI / backend-and-migrations (push) Waiting to run
CI / astro-build (push) Waiting to run
CI / dependency-audit (push) Waiting to run
CI / compose-e2e (push) Waiting to run

This commit is contained in:
ik
2026-09-22 08:17:47 +07:00
parent b1d6d8d122
commit 89dcf5f48e
3 changed files with 28 additions and 8 deletions
+15 -5
View File
@@ -19,7 +19,7 @@ from ..dependencies import Db
from ..importer import normalize
from ..models import Bait, BaitKind, CatchReport, Fish, ModerationStatus, SourceType, Spot, SubmissionAttempt, Waterbody
from ..schemas import CatchReportAccepted, CatchReportCreate
from ..storage import ScreenshotError, upload_screenshot
from ..storage import ScreenshotError, delete_screenshot, upload_screenshot
from ..submission_security import check_rate_limit
from ..tackle_components import replace_tackle_components
from rf4_research.gear_components import from_catch_fields
@@ -77,21 +77,31 @@ def create_catch_report(payload: CatchReportCreate, request: Request, db: Db, id
@router.post("/api/v1/catch-reports/{report_id}/screenshot", status_code=204, response_class=Response)
def add_screenshot(report_id: UUID, db: Db, screenshot: UploadFile = File(), upload_token: Annotated[str | None, Header(alias="X-Upload-Token")] = None) -> Response:
report = db.get(CatchReport, report_id)
report_query = select(CatchReport).where(CatchReport.id == report_id)
if db.bind is not None and db.bind.dialect.name == "postgresql":
report_query = report_query.with_for_update()
report = db.scalar(report_query)
if report is None or report.source_type != SourceType.user or report.moderation_status != ModerationStatus.pending:
raise HTTPException(status_code=404, detail="pending catch report not found")
supplied_hash = hashlib.sha256((upload_token or "").encode()).hexdigest()
if not report.screenshot_upload_token_hash or not hmac.compare_digest(report.screenshot_upload_token_hash, supplied_hash):
raise HTTPException(status_code=401, detail="invalid screenshot upload token")
if report.screenshot_key:
raise HTTPException(status_code=409, detail="screenshot already uploaded")
return Response(status_code=204)
raw = screenshot.file.read(settings.screenshot_max_bytes + 1)
try:
report.screenshot_key = upload_screenshot(raw, filename=screenshot.filename, content_type=screenshot.content_type)
except ScreenshotError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
report.screenshot_upload_token_hash = None
db.commit()
try:
db.commit()
except Exception as exc:
db.rollback()
try:
delete_screenshot(report.screenshot_key)
except Exception:
logger.exception("failed to remove orphaned screenshot", extra={"screenshot_key": report.screenshot_key})
raise HTTPException(status_code=503, detail="screenshot could not be saved; please retry") from exc
return Response(status_code=204)