Documentation
¶
Overview ¶
Package idempotency provides reusable contracts for HTTP idempotency key handling, request fingerprinting, and replay-safe response capture. The package defines interfaces and helpers that allow applications to implement durable idempotency storage while the reusable library provides validation, fingerprinting, and safe response capture.
The application retains ownership of:
- Durable storage (Redis, database, etc.)
- Transaction boundaries
- Replay policy (TTL, eviction)
- Concurrency control implementation
The reusable library provides:
- Key validation (format, length, allowed characters)
- Request fingerprinting (deterministic hash of method, path, body)
- Response capture contracts (status, headers, body)
- Safe replay (never expose raw keys, never replay partial writes)
- Conflict detection interfaces
Index ¶
- Constants
- Variables
- func IsSafeMethod(method string) bool
- func RequiresIdempotencyKey(method string) bool
- func SafeHeaders(h http.Header) http.Header
- func ValidateKey(key string) error
- type MemoryStore
- func (s *MemoryStore) Cleanup()
- func (s *MemoryStore) Complete(key string, status int, headers map[string][]string, body []byte) error
- func (s *MemoryStore) Fail(key string) error
- func (s *MemoryStore) Get(key string) (*Record, error)
- func (s *MemoryStore) Len() int
- func (s *MemoryStore) Reserve(key string, fingerprint RequestFingerprint, ttl time.Duration) (*Record, error)
- type Record
- type RecordState
- type RequestFingerprint
- type Store
Constants ¶
const HeaderName = "Idempotency-Key"
HeaderName is the standard HTTP header for idempotency keys.
const KeyMaxLength = 255
KeyMaxLength is the maximum allowed length for an idempotency key. This prevents abuse via excessively long keys.
const KeyMinLength = 1
KeyMinLength is the minimum allowed length for an idempotency key.
Variables ¶
var ( // ErrConflict indicates a record is in-progress for the given key. ErrConflict = errors.New("idempotency: conflict (in-progress)") // ErrReplayAvailable indicates a completed record is available // for replay. ErrReplayAvailable = errors.New("idempotency: replay available") )
Sentinel errors returned by Store implementations.
var ( // ErrNotFound indicates the record was not found. ErrNotFound = errNotFound{} // ErrNotInProgress indicates the record is not in-progress. ErrNotInProgress = errNotInProgress{} )
Additional sentinel errors for Store operations.
var ErrFingerprintMismatch = errors.New("idempotency: fingerprint mismatch")
ErrFingerprintMismatch indicates the request fingerprint does not match the stored fingerprint for the given idempotency key.
var ErrInvalidKey = errors.New("idempotency: invalid key")
ErrInvalidKey indicates the idempotency key is malformed.
var ErrKeyEmpty = errors.New("idempotency: empty key")
ErrKeyEmpty indicates the idempotency key is empty.
var ErrKeyTooLong = errors.New("idempotency: key too long")
ErrKeyTooLong indicates the idempotency key exceeds the maximum length.
Functions ¶
func IsSafeMethod ¶
IsSafeMethod reports whether the HTTP method is idempotent by default (GET, HEAD, PUT, DELETE). POST and PATCH require an idempotency key to be safely retried.
func RequiresIdempotencyKey ¶
RequiresIdempotencyKey reports whether the HTTP method requires an idempotency key for safe retry. POST and PATCH return true; all others return false.
func SafeHeaders ¶
SafeHeaders returns a copy of the given headers with sensitive headers removed. This should be called before storing response headers in an idempotency record to prevent replaying credentials or session tokens.
func ValidateKey ¶
ValidateKey validates an idempotency key. The key must be non-empty, no longer than KeyMaxLength, and contain only printable ASCII characters (no control characters, no whitespace-only). This prevents injection and abuse.
Types ¶
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory implementation of Store, suitable for testing and single-process development. It is NOT suitable for production use because it does not survive restarts and does not work across multiple process instances.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore creates a new in-memory idempotency store.
func (*MemoryStore) Cleanup ¶
func (s *MemoryStore) Cleanup()
Cleanup removes expired records. This is a maintenance operation that should be called periodically.
func (*MemoryStore) Complete ¶
func (s *MemoryStore) Complete(key string, status int, headers map[string][]string, body []byte) error
Complete marks an in-progress record as completed.
func (*MemoryStore) Fail ¶
func (s *MemoryStore) Fail(key string) error
Fail marks an in-progress record as failed.
func (*MemoryStore) Get ¶
func (s *MemoryStore) Get(key string) (*Record, error)
Get retrieves the record for the given key.
func (*MemoryStore) Len ¶
func (s *MemoryStore) Len() int
Len returns the number of records in the store.
func (*MemoryStore) Reserve ¶
func (s *MemoryStore) Reserve(key string, fingerprint RequestFingerprint, ttl time.Duration) (*Record, error)
Reserve attempts to create an in-progress record for the given key.
type Record ¶
type Record struct {
// Key is the idempotency key (already validated).
Key string
// Fingerprint is the request fingerprint.
Fingerprint RequestFingerprint
// State is the current lifecycle state.
State RecordState
// ResponseStatus is the HTTP status of the completed response.
// Valid only when State == StateCompleted.
ResponseStatus int
// ResponseHeaders are the non-sensitive response headers to replay.
// Sensitive headers (Set-Cookie, Authorization) should be filtered
// by the application before storage.
ResponseHeaders http.Header
// ResponseBody is the response body to replay.
// Valid only when State == StateCompleted.
ResponseBody []byte
// CreatedAt is when the record was created.
CreatedAt time.Time
// ExpiresAt is when the record should be evicted.
ExpiresAt time.Time
}
Record represents a stored idempotency record. The application is responsible for persisting this; the reusable library defines the contract.
type RecordState ¶
type RecordState int
RecordState represents the lifecycle state of an idempotency record.
const ( // StateInProgress indicates the request is being processed. // A concurrent request with the same key should receive 409 Conflict. StateInProgress RecordState = iota // StateCompleted indicates the request finished successfully and // the stored response should be replayed. StateCompleted // StateFailed indicates the request failed and should not be // replayed (the client should retry with the same key). StateFailed )
type RequestFingerprint ¶
type RequestFingerprint struct {
// Method is the HTTP method (uppercase).
Method string
// Path is the request path (including query string).
Path string
// BodyHash is the SHA-256 hash of the request body (hex-encoded).
BodyHash string
}
RequestFingerprint is a deterministic hash of the request method, path, and body. It is used to detect mismatched requests reusing the same idempotency key.
func FingerprintRequest ¶
func FingerprintRequest(method, path string, body []byte) RequestFingerprint
FingerprintRequest creates a RequestFingerprint from the HTTP method, path, and body. The body is hashed (SHA-256) so the fingerprint does not retain the raw body.
func (RequestFingerprint) Hash ¶
func (f RequestFingerprint) Hash() string
Hash returns a deterministic hash of the fingerprint, suitable for use as a storage key or comparison value.
type Store ¶
type Store interface {
// Reserve attempts to create an in-progress record for the given
// key and fingerprint. If a record already exists:
// - If it is in-progress, return the existing record and
// ErrConflict.
// - If it is completed, return the existing record and
// ErrReplayAvailable.
// - If it is expired, the implementation should evict it and
// allow the reservation.
// - If the fingerprint does not match, return
// ErrFingerprintMismatch.
Reserve(key string, fingerprint RequestFingerprint, ttl time.Duration) (existing *Record, err error)
// Complete marks an in-progress record as completed with the
// given response. The response headers and body are stored for
// replay. Sensitive headers must be filtered by the caller before
// storage.
Complete(key string, status int, headers http.Header, body []byte) error
// Fail marks an in-progress record as failed. The record may be
// retried by the client with the same key.
Fail(key string) error
// Get retrieves the record for the given key. Returns nil, nil if
// the key does not exist.
Get(key string) (*Record, error)
}
Store is the contract for idempotency record storage. The application implements this interface using its preferred durable storage (Redis, database, etc.). The reusable library does not provide a production Store implementation.
All methods must be safe for concurrent use. The Reserve operation must be atomic to prevent races.