python.missing_idempotency_key
Correctness
Medium
Detects HTTP POST/PUT endpoints that modify state without idempotency key handling, which can cause duplicate operations on retries.
Why It Matters
Section titled “Why It Matters”Without idempotency keys, retried requests can cause:
- Duplicate transactions — Payment charged multiple times
- Duplicate records — Same order created twice
- Data inconsistency — State modified multiple times
- User frustration — Actions appear to happen multiple times
Network issues cause retries; idempotency keys ensure “exactly-once” semantics.
Example
Section titled “Example”# ❌ Before (no idempotency)@app.post("/payments")async def create_payment(payment: PaymentRequest): return await payment_service.charge(payment)# ✅ After (with idempotency key)@app.post("/payments")async def create_payment( payment: PaymentRequest, idempotency_key: str = Header(..., alias="Idempotency-Key")): # Check if we've seen this key before existing = await cache.get(f"idempotency:{idempotency_key}") if existing: return existing
# Process the payment result = await payment_service.charge(payment)
# Store result for future retries (with expiry) await cache.set(f"idempotency:{idempotency_key}", result, ex=86400)
return resultWhat Unfault Detects
Section titled “What Unfault Detects”- POST/PUT endpoints that create or modify resources
- Payment processing endpoints
- Order creation endpoints
- Any state-modifying endpoint without idempotency handling
Auto-Fix
Section titled “Auto-Fix”Unfault generates patches that add idempotency key header parameters:
from fastapi import Header
@app.post("/orders")async def create_order( order: OrderRequest, idempotency_key: str = Header(..., alias="Idempotency-Key")): # Implementation with idempotency check passBest Practices
Section titled “Best Practices”# Idempotency key middlewarefrom fastapi import Requestimport hashlib
async def idempotency_middleware(request: Request, call_next): if request.method in ("POST", "PUT", "PATCH"): key = request.headers.get("Idempotency-Key") if not key: # Generate from request body hash for safety body = await request.body() key = hashlib.sha256(body).hexdigest()[:16]
# Check cache, process, store result...
return await call_next(request)