Skip to content

Idempotency keys

The middleware that replays the first response to a retried request. In fastapi_alcazaba.idempotency.

In the guide: Idempotency keys. In an app: the example.

fastapi_alcazaba.idempotency.IdempotencyMiddleware

IdempotencyMiddleware(app: ASGIApp, *, cache: Cache | None = None, methods: Collection[str] = ('POST', 'PATCH'), expire: int = 86400, identifier: Identifier | None = None, max_body_size: int = 1048576, max_response_size: int = 1048576, lock_ttl: float = 60.0, fail_open: bool = False)

ASGI middleware that makes requests with an Idempotency-Key header run once.

app.add_middleware(IdempotencyMiddleware)

Requests without the header go through untouched (require_idempotency_key makes it mandatory on a route). The stored responses go through the cache's signing or encryption: use an encryption_key when they carry personal data. They're kept without Set-Cookie.

A retry gets the stored response before the route's dependencies run: authentication included. With the default identifier, whoever has a copy of the request (its token, its key) gets the response again until it expires, even after the token is revoked; an identifier that checks the credentials (async, returning the user's id, raising a 401 otherwise) closes that.

Parameters:

Name Type Description Default
cache Cache | None

where responses and locks are kept; the app's (Cache.install) or FastAPICache's by default. Its backend must support locks.

None
methods Collection[str]

the methods it applies to; PUT and DELETE are idempotent already.

('POST', 'PATCH')
expire int

seconds a response is kept for retries.

86400
identifier Identifier | None

who the client is (authorization by default). Return the user's id with cookie sessions, and with tokens that get refreshed: a retry sent with a fresh token would run again. Requests without credentials share one space of keys, unguessable as long as clients make them random (UUIDs).

None
max_body_size int

larger request bodies get a 413: they're read whole to fingerprint them.

1048576
max_response_size int

larger responses aren't kept (a retry would run again).

1048576
lock_ttl float

seconds the lock outlives a process that dies while running the request.

60.0
fail_open bool

when the backend fails, run requests without idempotency instead of answering 503. Off by default: running a payment twice is worse than refusing it.

False

fastapi_alcazaba.idempotency.require_idempotency_key

require_idempotency_key(request: Request) -> str

FastAPI dependency for routes that must not run without an Idempotency-Key:

@app.post("/payments", dependencies=[Depends(require_idempotency_key)])

A request without one gets a 400; it returns the key.

fastapi_alcazaba.idempotency.authorization

authorization(connection: HTTPConnection) -> str

The default identifier: the Authorization header (hashed before it's stored), so that one user never gets another's response, whatever key they send.