idempotency

package
v0.30.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 8 Imported by: 0

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

View Source
const HeaderName = "Idempotency-Key"

HeaderName is the standard HTTP header for idempotency keys.

View Source
const KeyMaxLength = 255

KeyMaxLength is the maximum allowed length for an idempotency key. This prevents abuse via excessively long keys.

View Source
const KeyMinLength = 1

KeyMinLength is the minimum allowed length for an idempotency key.

Variables

View Source
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.

View Source
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.

View Source
var ErrFingerprintMismatch = errors.New("idempotency: fingerprint mismatch")

ErrFingerprintMismatch indicates the request fingerprint does not match the stored fingerprint for the given idempotency key.

View Source
var ErrInvalidKey = errors.New("idempotency: invalid key")

ErrInvalidKey indicates the idempotency key is malformed.

View Source
var ErrKeyEmpty = errors.New("idempotency: empty key")

ErrKeyEmpty indicates the idempotency key is empty.

View Source
var ErrKeyTooLong = errors.New("idempotency: key too long")

ErrKeyTooLong indicates the idempotency key exceeds the maximum length.

Functions

func IsSafeMethod

func IsSafeMethod(method string) bool

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

func RequiresIdempotencyKey(method string) bool

RequiresIdempotencyKey reports whether the HTTP method requires an idempotency key for safe retry. POST and PATCH return true; all others return false.

func SafeHeaders

func SafeHeaders(h http.Header) http.Header

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

func ValidateKey(key string) error

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.

func (*Record) IsExpired

func (r *Record) IsExpired(now time.Time) bool

IsExpired reports whether the record has expired relative to now.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL