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
Functions
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.
Extract the idempotency key from the request's Idempotency-Key header. Returns Some(key_string) or None if the header is absent.
Drop a cached entry, allowing the next request with that key to run the handler again.
BastionIdempotency.invalidate("idempotency_cache", "key-123", "")Look up the current state for a cache key. Returns Some(IdempotencyState) or None if no entry exists.
BastionIdempotency.lookup("idempotency_cache", "key-123", "")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.
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.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.