Documentation
¶
Overview ¶
Package idempotency implements the HTTP Idempotency-Key request pattern.
Semantics ¶
The middleware looks up the header named by [Config.KeyHeader] (default "Idempotency-Key") and:
- Validates the key is 1..MaxKeyLength printable ASCII.
- Attempts to atomically acquire a lock via store.SetNXer.
- On acquisition, runs the handler and stores the captured response under the key with [Config.TTL] expiry. The lock entry is replaced by the completed entry atomically.
- On contention with an in-flight duplicate, returns 409 Conflict via [Config.OnConflict] (no wait-and-retry).
- On a subsequent request that finds a completed entry, replays the stored response without running the handler.
Body hash ¶
[Config.BodyHash] enables SHA-256 hashing of the request body (up to [Config.MaxBodyBytes]) and compares it to the stored hash on replay. Mismatches return 422 Unprocessable Entity — catching clients that reuse a key with different payloads, which the IETF draft recommends rejecting. Disabled by default because hashing large bodies costs memory.
Store requirements ¶
The store must implement both store.KV and store.SetNXer. The default NewMemoryStore is in-memory and sharded, suitable for single-instance deployments. For multi-instance deployments, wire a middleware/session/redisstore.Store — it satisfies both interfaces. (Wrap with store.Prefixed to namespace alongside session data.)
Lock expiry ¶
A crashed handler leaves the lock entry behind. Once [Config.LockTimeout] passes, the key becomes acquirable again. Choose LockTimeout ≥ the worst-case handler latency plus network margin to avoid a second execution of a still-running request.
Package idempotency implements the HTTP Idempotency-Key pattern.
When a request carries an Idempotency-Key header, the middleware attempts to acquire an atomic lock on the key via store.SetNXer. The first request to acquire the lock runs the handler, persists the response under the same key, and releases the lock. Subsequent requests with the same key replay the stored response.
Concurrent duplicates that arrive while the original is still in-flight return 409 Conflict (configurable via [Config.OnConflict]). Crashed handlers leak a lock entry; the lock expires after [Config.LockTimeout] so the next request can retry.
When [Config.BodyHash] is enabled, the request body hash is stored alongside the response; mismatches on replay return 422 Unprocessable Entity to catch clients that reuse a key with different payloads.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrStoreMissingSetNX = errors.New("idempotency: store must implement store.SetNXer")
ErrStoreMissingSetNX is returned by New when Config.Store is set but does not satisfy KVStore.
Functions ¶
Types ¶
type Config ¶
type Config struct {
// Store persists completed responses and lock entries. Default:
// [NewMemoryStore]. Must implement [store.KV] + [store.SetNXer].
Store KVStore
// KeyHeader is the request header carrying the client-supplied
// idempotency key. Default: "Idempotency-Key".
KeyHeader string
// TTL is the lifetime of a completed response entry. Default: 24h.
TTL time.Duration
// LockTimeout is the maximum duration a lock is held before it
// expires (recovers after a crashed handler). Default: 30s.
LockTimeout time.Duration
// Methods lists HTTP methods the middleware applies to. Default:
// POST, PUT, PATCH, DELETE. Methods outside this list pass through.
Methods []string
// OnConflict runs when a duplicate request arrives while the
// original is still in-flight (lock held, no completed entry yet).
// Default: respond with 409 Conflict.
OnConflict func(*celeris.Context) error
// MaxKeyLength is the upper bound for the key header value.
// Default: 255.
MaxKeyLength int
// BodyHash, when true, hashes the request body (up to MaxBodyBytes)
// and compares it to the stored hash on replay. Mismatches return
// 422 Unprocessable Entity. Default: false (hash checking disabled).
BodyHash bool
// MaxBodyBytes caps the bytes hashed for BodyHash and the bytes
// stored in a completed response. Default: 1 MiB.
MaxBodyBytes int
// Skip defines a function to skip this middleware for certain
// requests.
Skip func(*celeris.Context) bool
// SkipPaths lists paths to skip (exact match).
SkipPaths []string
// CleanupContext stops the MemoryStore cleanup goroutine (no effect
// on Redis/Postgres stores).
CleanupContext context.Context
}
Config defines the idempotency middleware configuration.
Example ¶
ExampleConfig — minimum useful configuration. Clients send `Idempotency-Key: <uuid>` on POST/PUT/DELETE; retries within the TTL replay the cached response and concurrent duplicates while the original is still in-flight return 409 Conflict. `store.MemoryKV` satisfies idempotency.KVStore (KV + SetNXer) out of the box.
package main
import (
"time"
"github.com/goceleris/celeris/middleware/idempotency"
"github.com/goceleris/celeris/middleware/store"
)
func main() {
_ = idempotency.Config{
Store: store.NewMemoryKV(),
TTL: 5 * time.Minute,
}
}
Output:
type KVStore ¶
KVStore combines store.KV with store.SetNXer — idempotency requires both. The in-memory default satisfies this out of the box. Backends without SetNX cannot be used as an idempotency store.
func NewMemoryStore ¶
func NewMemoryStore(config ...MemoryStoreConfig) KVStore
NewMemoryStore returns an in-memory idempotency store. It wraps store.MemoryKV, which implements both store.KV and store.SetNXer — the minimum surface New needs.
type MemoryStoreConfig ¶
type MemoryStoreConfig = store.MemoryKVConfig
MemoryStoreConfig is a type alias for store.MemoryKVConfig provided for symmetry with other middleware packages.