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_lib-0.0.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_lib-0.0.1-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file mobius_idempotence_lib-0.0.1.tar.gz.

File metadata

  • Download URL: mobius_idempotence_lib-0.0.1.tar.gz
  • Upload date:
  • Size: 18.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for mobius_idempotence_lib-0.0.1.tar.gz
Algorithm Hash digest
SHA256 949049e60e16dbbd0bcb0bb25477113b522beb083db6966be7d0ba676bca6642
MD5 2831d1b76bc243b5936a410f105ccd17
BLAKE2b-256 7d049885c6ded438c378eaadfe5b3bf5b7b239ee9dea6749c9555c9dea32aee0

See more details on using hashes here.

File details

Details for the file mobius_idempotence_lib-0.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for mobius_idempotence_lib-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6e64063c0002f3646414359d7e3e130c5c536b141247c32a5242f5ed9428eefc
MD5 7984acfe830c4d47b54c8641dd982d25
BLAKE2b-256 f18cc9f8aab1f741c7ab1e2d536320379b8395d169bca53c59b1e68d8249839d

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