March Docs

BastionIdempotency

BastionIdempotency: idempotency-key middleware for Bastion web apps.

Caches request responses in Vault by the value of the Idempotency-Key request header. Subsequent requests carrying the same key receive the cached response without re-executing the handler.

Concurrency safety: An InProgress sentinel is written before the handler runs so that concurrent duplicate requests receive 409 Conflict rather than running the handler twice.

5xx responses are never cached — only successful or client-error responses are stored so transient server failures can be retried.

Typical usage:

BastionIdempotency.protect(conn, fn c -> MyHandler.create(c), opts)

Where opts is a List((String, String)) with optional keys: "vault_table" — Vault table name (default "idempotency_cache") "scope" — prefix for cache keys (default "")

Types

typeIdempotencyStateIdempotencyState#
typeCachedResponseCachedResponse = CachedResponse(#

Functions

fncomplete_entrycomplete_entry(vault_table_name, key, scope, status, headers, body)#

Store a completed response in the Vault cache. status is the HTTP status code, headers is List((String, String)), body is the response body string.

fnget_keyget_key(conn)#

Extract the idempotency key from the request's Idempotency-Key header. Returns Some(key_string) or None if the header is absent.

fninvalidateinvalidate(vault_table_name, key, scope)#

Drop a cached entry, allowing the next request with that key to run the handler again.

  BastionIdempotency.invalidate("idempotency_cache", "key-123", "")
fnlookuplookup(vault_table_name, key, scope)#

Look up the current state for a cache key. Returns Some(IdempotencyState) or None if no entry exists.

  BastionIdempotency.lookup("idempotency_cache", "key-123", "")
fnmark_in_progressmark_in_progress(vault_table_name, key, scope) : Bool#

Atomically claim a key as InProgress. Returns true if this call won the race (the caller should run the handler), false if another request already claimed it (caller should respond 409).

Uses Vault.put_new — a single test-and-set under the shard lock — so two concurrent requests cannot both see true.

fnprotectprotect(conn, handler, opts)#

Full idempotency middleware. Wraps handler with cache check/store logic.

  BastionIdempotency.protect(conn, fn c -> MyHandler.create(c), opts)

opts keys (all optional): "vault_table" — Vault table name (default "idempotency_cache") "scope" — key scope prefix (default "")

Behaviour:

  • No Idempotency-Key header -> calls handler directly (pass-through).
  • Key found, InProgress -> returns 409 Conflict.
  • Key found, Completed -> replays cached response.
  • Key not found -> marks InProgress, runs handler, caches
                                result (unless 5xx), returns result.
fnreplay_responsereplay_response(conn, cached_state)#

Apply a cached Completed response to conn, adding the x-idempotent-replayed: true header.

  BastionIdempotency.replay_response(conn, cached_state)

cached_state must be a Completed variant.