Skip to content

Integrity, resilience and errors

What signs and encrypts entries, RFC 8785 canonical JSON, the backends' circuit breaker, and the exceptions. In fastapi_alcazaba.core.

In the guide: Security model, Encrypted entries, Resilience.

fastapi_alcazaba.core.Signer

Signer(keys: Secrets)

Signs what the cache stores and only accepts what it signed itself.

Whoever can write to the backend (a shared Redis, a leaked password) can't plant data, nor a pickle that runs code when it's read, nor copy one user's entry under another user's key: the tag covers the key too. The first key signs; the others are only checked, so a key can be rotated without losing the cache: put the new one first and drop the old one later.

unsign

unsign(cache_key: str, data: bytes) -> bytes | None

The payload, or None when data wasn't signed by us for this key.

fastapi_alcazaba.core.canonical_json

canonical_json(value: Any) -> bytes

value (dicts with string keys, lists, tuples, strings, integers, floats, booleans and None) in its canonical form, as UTF-8. Raises ValueError for what I-JSON leaves out (integers a double can't hold exactly, NaN, infinities, lone surrogates) and TypeError for what isn't JSON.

fastapi_alcazaba.core.Encrypter

Encrypter(keys: Secrets)

Encrypts what the cache stores and only accepts what it encrypted itself.

On top of what signing gives (whoever can write to the backend can't plant data, run a pickle or move one user's entry under another user's key), a dump or a backup of Redis doesn't reveal the cached responses. The cache key is the associated data, so an entry only decrypts under its own key. Each entry is encrypted with its own AES-256 key, derived with HKDF-SHA256 from the secret and a random salt, so random 96-bit nonces never get near the limits of AES-GCM however many entries are written.

The first secret encrypts; all of them decrypt, so a secret can be rotated without losing the cache: put the new one first and drop the old one later.

Layout: b"fastapi_alcazaba/e1" | key id (4) | salt (16) | nonce (12) | ciphertext and GCM tag (16).

open

open(cache_key: str, data: bytes) -> bytes | None

The payload, or None when data wasn't encrypted by us for cache_key.

fastapi_alcazaba.core.CircuitBreaker

CircuitBreaker(failure_threshold: int = 5, reset_timeout: float = 30.0)

Stops calling a failing backend for a while.

Closed: calls go through, and failure_threshold consecutive failures open it. Open: calls are refused for reset_timeout seconds. Then a single trial call goes through (half open): a success closes it, a failure opens it again. One breaker per process.

retry_after property

retry_after: float

Seconds until a trial call may go through; 0 unless it's open.

allow

allow() -> str | None

The state the call goes through in: "closed", or "half-open" when it's the single trial; None when it's refused.

reset

reset() -> None

Close it, whatever its state: the failures counted so far are forgotten.

abandon_trial

abandon_trial() -> None

The trial call ended without a verdict (cancelled): let the next call try. Only for the call allow() let through "half-open": another one would let a second trial through.

fastapi_alcazaba.core.errors

Warnings and exceptions of Alcazaba.

UnsignedEntriesWarning

Bases: UserWarning

A coder whose decoding can run code (PickleCoder) is used without a signing key.

UnstableCacheKeyWarning

Bases: UserWarning

An argument in the cache key has a repr() that changes with every call (it shows a memory address), so the entry is never found again: leave it out with exclude_params.

BackendUnavailable

Bases: Exception

The backend failed, timed out or its circuit breaker is open. The decorator treats it as "no cache" and runs the function.

CircuitOpen

Bases: BackendUnavailable

The circuit breaker is open: the backend isn't called for a while.