From fc963101b362e56e05709a57c5b9a5186e2c9b23 Mon Sep 17 00:00:00 2001 From: IK Date: Fri, 11 Sep 2026 07:46:50 +0700 Subject: [PATCH] fix: handle concurrent idempotency key conflicts --- apps/api/app/main.py | 12 +++++++++++- apps/api/app/models.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/api/app/main.py b/apps/api/app/main.py index 46ec460..a75941d 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -18,6 +18,7 @@ from fastapi import Depends, FastAPI, File, Header, HTTPException, Query, Reques from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from sqlalchemy import delete, func, select, text +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload from .activity import activity_rows @@ -514,7 +515,16 @@ def create_catch_report( if idempotency_key: key_hash = hmac.new(settings.rate_limit_secret.encode(), idempotency_key.encode(), hashlib.sha256).hexdigest() db.add(SubmissionAttempt(client_hash="", idempotency_key=key_hash, catch_report_id=report.id, payload_hash=payload_hash, created_at=datetime.now(timezone.utc))) - db.commit() + try: + db.commit() + except IntegrityError: + # Another request won the same idempotency key race. + db.rollback() + winner = db.scalar(select(SubmissionAttempt).where(SubmissionAttempt.idempotency_key == key_hash)) + if winner and winner.catch_report: + replay_token = hmac.new(settings.rate_limit_secret.encode(), (key_hash + ":upload").encode(), hashlib.sha256).hexdigest() + return JSONResponse(status_code=200, content={"id": str(winner.catch_report.id), "moderation_status": winner.catch_report.moderation_status.value, "screenshot_upload_token": replay_token, "idempotent": True}) + raise logger.info("idempotency key stored", extra={"idempotency_key": idempotency_key[:8]}) return CatchReportAccepted(id=report.id, moderation_status=report.moderation_status.value, screenshot_upload_token=upload_token, idempotent=False) diff --git a/apps/api/app/models.py b/apps/api/app/models.py index db6b354..4ef0cce 100644 --- a/apps/api/app/models.py +++ b/apps/api/app/models.py @@ -136,7 +136,7 @@ class SubmissionAttempt(Base): __tablename__ = "submission_attempt" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) client_hash: Mapped[str] = mapped_column(String(64), index=True) - idempotency_key: Mapped[str | None] = mapped_column(String(128), index=True) + idempotency_key: Mapped[str | None] = mapped_column(String(128), unique=True, index=True) catch_report_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("catch_report.id"), nullable=True) payload_hash: Mapped[str | None] = mapped_column(String(64), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)