Skip to main content

Idempotency-Key middleware for FastAPI, with pluggable Redis / in-memory storage backends.

Project description

mobius-idempotence-library

Idempotency-Key middleware for FastAPI, with pluggable storage backends.

Protects POST/PATCH-style endpoints (payments, order creation, anything non-idempotent by nature) against duplicate side effects when a client retries a request -- due to a timeout, a dropped connection, or an at-least-once delivery mechanism upstream.

Features

  • Idempotency-Key enforcement following the shape of the common Idempotency-Key header convention used by Stripe and similar APIs.
  • Request fingerprinting: a key reused with a different request body is rejected (422), not silently replayed.
  • In-flight collision detection: a second concurrent request with the same key while the first is still processing gets 409, not a race.
  • Pluggable backends: ships with RedisBackend (distributed, atomic via single-round-trip Lua scripts) and MemoryBackend (in-process, for tests or single-instance deployments). Implement StorageBackend for anything else (Postgres, DynamoDB, ...).
  • Fail-open or fail-closed, your choice, when the backend is unreachable.
  • Fully async, doesn't block the event loop.

Install

pip install mobius-idempotence-library          # in-memory backend only
pip install mobius-idempotence-library[redis]   # + Redis backend

Quick start

from fastapi import FastAPI
from pydantic import BaseModel

from mobius_idempotence import IdempotencyConfig, IdempotencyManager, idempotent
from mobius_idempotence.backends.redis import RedisBackend

app = FastAPI()

backend = RedisBackend("redis://localhost:6379/0")
manager = IdempotencyManager(backend, IdempotencyConfig(fail_open=False))


class PaymentRequest(BaseModel):
    amount: float
    currency: str


@app.post("/payments", status_code=201)
@idempotent(manager, status_code=201)
async def create_payment(payload: PaymentRequest):
    return await charge_the_card(payload)  # your business logic, nothing else


@app.on_event("shutdown")
async def shutdown():
    await manager.aclose()

That's the entire integration. @idempotent(manager) handles:

  • validating the Idempotency-Key header
  • fingerprinting the request body, so a key reused with a different body is rejected (422)
  • detecting an in-flight duplicate request (409)
  • replaying the cached response verbatim on retry, with no re-run of your handler
  • releasing the lock if your handler raises, so the client can safely retry with the same key

No Depends(...), no manual cache lookup, no manual save/release calls in the handler.

Callers must send the same Idempotency-Key header (and the same request body) on retry:

curl -X POST http://localhost:8000/payments \
  -H "Idempotency-Key: 3f29c1a2-..." \
  -H "Content-Type: application/json" \
  -d '{"amount": 10.5, "currency": "USD"}'

What gets cached: the handler's return value (dict, list, Pydantic model, or dataclass), encoded the same way FastAPI itself would encode it. If you need to return a raw Response / StreamingResponse, or want custom caching logic, use the lower-level IdempotencyGuard dependency instead -- see examples/payments_app_manual.py.

Writing your own backend

Subclass mobius_idempotence.StorageBackend and implement five async methods: check_or_lock, save_response, release, ping, aclose. See src/mobius_idempotence/backends/memory.py for the simplest possible reference implementation.

Configuration

IdempotencyConfig is a plain dataclass -- wire it up to whatever config system your app already uses (env vars, Pydantic Settings, a YAML file):

Field Default Meaning
lock_ttl 60 Seconds an in-progress lock lives before being considered abandoned
response_ttl 86400 Seconds a finished response stays cached and replayable
key_prefix "idemp:" Storage key namespace
fail_open True If the backend is unreachable: process anyway (True) or 503 (False)
max_key_len 255 Reject longer Idempotency-Key headers

Logging

The library never calls logging.basicConfig() or attaches its own handlers -- like any well-behaved library, it just emits log records and leaves handler/format/output configuration to your app. If you see nothing, that's almost always because your app hasn't configured logging at all, or has the root level above DEBUG/INFO.

Logger names:

  • mobius_idempotence -- the core decision log (see below)
  • mobius_idempotence.redis -- Redis connection-level errors only

What gets logged, at what level:

Level Event
DEBUG New lock acquired (processing as a new request)
DEBUG Cache hit -- replaying a previously finished response
DEBUG Response successfully cached / lock successfully released
INFO 409 -- an identical request is already in progress
WARNING 422 -- key reused with a different request body
WARNING save_response/release was a no-op (stale token -- lock expired or another caller took over)
WARNING Backend unreachable, fail_open=True -- proceeding without idempotency protection
ERROR Backend unreachable, fail_open=False -- request rejected with 503

Turn it on without touching your app's global logging config:

import logging

logging.getLogger("mobius_idempotence").setLevel(logging.DEBUG)
logging.getLogger("mobius_idempotence").addHandler(logging.StreamHandler())

Or, if your app already calls logging.basicConfig(...) / dictConfig(...), just make sure the level for this logger (or the root logger) is DEBUG or INFO -- no extra wiring needed:

logging.basicConfig(level=logging.INFO)  # mobius_idempotence INFO/WARNING/ERROR now visible

Every log line includes the idempotency key so you can grep/filter a single request's lifecycle across a busy log stream.

Testing

pip install -e ".[dev]"
pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mobius_idempotence_library-0.2.1.tar.gz (18.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mobius_idempotence_library-0.2.1-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

File details

Details for the file mobius_idempotence_library-0.2.1.tar.gz.

File metadata

File hashes

Hashes for mobius_idempotence_library-0.2.1.tar.gz
Algorithm Hash digest
SHA256 0f9d360e006b2ce18adfb2a4cbb54a1906dbe63a7f4c6c8335015316ee65fbaf
MD5 0e4ffcdfff900215008fd45bb4b499d9
BLAKE2b-256 6c0b751df8798aa976755111b972c2f0e0329e11a2f8105d6aed225ca2ef3857

See more details on using hashes here.

File details

Details for the file mobius_idempotence_library-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for mobius_idempotence_library-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b07852ddb6d8714ab7901ae309d644895b982e698440ecd414ef4e8a31fa814b
MD5 8117c0bad3982134d2899f684b0ac9ee
BLAKE2b-256 13bb1243e43249950eef4bb6607855d46d3d92511de4ffce8484eafb0ca8fe38

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page