Skip to content

Idempotency — API Reference ​

python
from fastapi_viewsets.middleware.idempotency import (
    IDEMPOTENCY_KEY_HEADER,
    Idempotency,
    parse_idempotency_key,
    read_idempotency_key,
    request_fingerprint,
)
from fastapi_viewsets.exceptions import IdempotencyKeyInvalidError, IdempotencyKeyReusedError

The rules are described in Idempotent Calls.

Idempotency ​

python
class Idempotency(Middleware):
    def __init__(self, redis_client: Redis | None = None, key_prefix: str = "idempotency:"): ...

A Middleware.

ParameterTypeDefaultDescription
redis_clientredis.asyncio.Redis | NoneNoneWhere the records live; None keeps them in the instance
key_prefixstr"idempotency:"Prefix of the record key, the :attached key and the channel
MethodDescription
async depends(request, cls, context) -> NoneRaises IdempotencyKeyInvalidError for an invalid Idempotency-Key
async __call__(request, viewset, context, call_next) -> ViewSetResultRuns call_next() under the key, attaches to the running call or returns the stored outcome; raises IdempotencyKeyReusedError for a key recorded with a different fingerprint, and raises a stored error outcome again

modifies_response_shape is False.

Settings ​

AttributeTypeDefaultDescription
settings.viewsets_idempotency_ttlfloat86400Seconds a stored outcome is kept
settings.viewsets_idempotency_lock_ttlfloat600Seconds a running record is kept after it was last extended; the process running the call extends it every third of this time

Functions ​

NameDescription
IDEMPOTENCY_KEY_HEADER"Idempotency-Key"
parse_idempotency_key(value: str) -> strThe key carried by a header value that is a Structured Field string; surrounding spaces and tabs are ignored. Raises IdempotencyKeyInvalidError for a token, a parameter after the string, an escape other than \" and \\, a character outside printable ASCII, or an empty string
read_idempotency_key(request: Request) -> str | NoneThe key of the request's Idempotency-Key header, None without one. Raises IdempotencyKeyInvalidError for more than one such header or an invalid value
async request_fingerprint(request: Request) -> strHex SHA-256 over the method, path, query string and the hex SHA-256 of the body, joined with \n; a body read as a form is hashed as its sorted fields, a file field as <filename>:<size>

Errors ​

ClassStatuscodeparams
IdempotencyKeyInvalidError()400idempotency_key_invalid—
IdempotencyKeyReusedError(key)422idempotency_key_reused{key}

Both are DfViewSetErrors; see Error Codes.

Frontend ​

ts
import {
  type CallEvent,
  IDEMPOTENCY_KEY_HEADER,
  idempotencyKeyHeader,
  issueCall,
  newCallKey,
  onCallActivity,
  type PendingCall,
  pendingCallCount,
  pendingCalls,
  type RepeatOptions,
  retryNow,
  type ViewSetActionCall,
  type ViewSetCall,
  type ViewSetCalls,
} from '@dynamicforms/fastapi-viewsets';

The behaviour is described in Idempotent Calls → Frontend.

Types ​

ts
/** The promise a call returns, with the call's Idempotency-Key. */
type ViewSetCall<R> = Promise<R> & { readonly key: string };

/** What a declared action returns in a ViewSet's type; `key` is optional so an override may return a Promise. */
type ViewSetActionCall<R> = Promise<R> & { readonly key?: string };

/** `M` with every method that returns `Promise<R>` returning `ViewSetCall<R>`; other members unchanged. */
type ViewSetCalls<M> = {
  [P in keyof M]: M[P] extends (...args: infer A) => Promise<infer R> ? (...args: A) => ViewSetCall<R> : M[P];
};
WhereReturn type
request() (protected)ViewSetCall<R>
the actions of the mixin interfaces, ViewSetProxyBase, RestProxyImpl, MuxwsProxyImpl and factory-built ViewSetsViewSetActionCall<R>
route_rest<M>() with an options object, route_muxws<M>()RestProxy<M>, MuxwsProxy<M>: ViewSetCalls<M> and watch()
route_rest<M>() with positional argumentsViewSetCalls<M>
a ViewSet's own methodas declared; key is set at run time when the method issues a request before its first await

RepeatOptions ​

ts
interface RepeatOptions {
  withinMs?: number;
  delayMs?: number;
}
FieldDefaultDescription
withinMs600000Milliseconds after the first attempt within which a repeat may be sent; past it the call rejects with the error of its last attempt. 0 turns repeating off
delayMs1000Milliseconds before a repeat after a lost connection or a gateway status, doubled after each such repeat, at most 30000. A repeat after an attempt's own timeout is sent without a delay

repeat?: RepeatOptions is an option of route_rest, route_muxws, restViewSet and muxwsViewSet ViewSets and of RequestOptions, where it overrides the proxy's for one call. attemptTimeoutMs?: number (default 30000, 0 for none) is a REST option: the axios timeout of one attempt.

CallEvent ​

ts
type CallEvent =
  | { readonly type: 'start'; readonly key: string; readonly background: boolean }
  | { readonly type: 'retry'; readonly key: string; readonly failure: 'timeout' | 'lost'; readonly attempt: number; readonly delayMs: number }
  | { readonly type: 'settle'; readonly key: string; readonly ok: boolean };
EventWhenFields
startThe call is issued, synchronously in request()background: RequestOptions.background
retryAn attempt ended without an answer and a repeat will be sentfailure: how it ended; attempt: the number of the attempt that ended, 1 for the first; delayMs: the wait before the repeat
settleThe call's promise settledok: false when it rejected

PendingCall ​

ts
interface PendingCall {
  readonly key: string;
  readonly background: boolean;
  readonly startedAt: number; // Date.now() when the call was issued
  readonly unanswered: number; // attempts that ended without an answer and were repeated
}

RequestOptions.key, RequestOptions.repeat and RequestOptions.background are listed with RequestOptions.

Functions ​

NameDescription
IDEMPOTENCY_KEY_HEADER'Idempotency-Key'
newCallKey(): stringA version 4 UUID: crypto.randomUUID(), or built from crypto.getRandomValues() where randomUUID is absent (an insecure context)
idempotencyKeyHeader(key: string): stringkey as a Structured Field string: quoted, " and \ escaped. Throws TypeError for an empty key or one with a character outside printable ASCII
issueCall<R>(promise: Promise<R>, key: string, background = false): ViewSetCall<R>Sets key on promise and returns it. Called synchronously from a transport's request(), it also gives key to the promise of the proxy method running around it. Reports start (with background) to the onCallActivity listeners at once and settle when promise settles
onCallActivity(listener: (event: CallEvent) => void): () => voidCalls listener for every CallEvent of every call issued through a proxy, REST and muxws alike; returns the function that removes it. A listener that throws is logged with console.error and affects neither the call nor the other listeners
pendingCalls(): readonly PendingCall[]Every call issued through a proxy that has not settled, oldest first; a snapshot
pendingCallCount(includeBackground = false): numberThe number of unsettled calls issued through a proxy; background calls are counted only with includeBackground
retryNow(): voidEnds the delay of every call waiting to send a repeat, so each sends it now; the delay of such a call's next repeat starts again from repeat.delayMs. A muxws call waiting for its peer to reconnect keeps waiting

Released under the MIT License.