Skip to content

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.

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.

# ❌ 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 result
  • POST/PUT endpoints that create or modify resources
  • Payment processing endpoints
  • Order creation endpoints
  • Any state-modifying endpoint without idempotency handling

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
pass
# Idempotency key middleware
from fastapi import Request
import 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)