Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The idempotency middleware ensures that repeated requests with the same idempotency key return the same response, preventing duplicate operations like double charges or duplicate records.
Use it when you need:
- Safe payment processing retries
- Duplicate request prevention
- Reliable webhook handling
Installation
import "github.com/go-mizu/mizu/middlewares/idempotency"
Quick Start
app := mizu.New()
// Enable idempotency
app.Use(idempotency.New())
app.Post("/payments", func(c *mizu.Ctx) error {
// This will only execute once per idempotency key
result := processPayment()
return c.JSON(200, result)
})
Client sends:
POST /payments
Idempotency-Key: unique-key-123
Configuration
Options
| Option | Type | Default | Description |
|---|
KeyHeader | string | "Idempotency-Key" | Header name for key |
Methods | []string | ["POST", "PUT", "PATCH"] | Methods to track |
TTL | time.Duration | 24h | How long to store responses |
KeyGenerator | func(string, *mizu.Ctx) string | Identity | Custom key generation |
Examples
Basic Usage
app.Use(idempotency.New())
app.Use(idempotency.WithOptions(idempotency.Options{
KeyHeader: "X-Request-Id",
}))
Specific Methods Only
app.Use(idempotency.WithOptions(idempotency.Options{
Methods: []string{http.MethodPost}, // Only POST
}))
User-Scoped Keys
app.Use(idempotency.WithOptions(idempotency.Options{
KeyGenerator: func(key string, c *mizu.Ctx) string {
userID := c.Request().Header.Get("X-User-ID")
return userID + ":" + key
},
}))
Custom Store
store := idempotency.NewMemoryStore()
defer store.Close()
app.Use(idempotency.WithStore(store, idempotency.Options{}))
How It Works
- Client sends request with
Idempotency-Key header
- Middleware checks if key exists in store
- If exists: returns cached response with
Idempotent-Replayed: true
- If not: executes handler, caches response, returns normally
- Subsequent requests with same key get cached response
API Reference
Functions
// New creates idempotency middleware
func New() mizu.Middleware
// WithOptions creates middleware with configuration
func WithOptions(opts Options) mizu.Middleware
// WithStore creates middleware with custom store
func WithStore(store Store, opts Options) mizu.Middleware
// NewMemoryStore creates in-memory response store
func NewMemoryStore() *MemoryStore
Store Interface
type Store interface {
Get(key string) (*Response, error)
Set(key string, resp *Response) error
Delete(key string) error
}
type Response struct {
StatusCode int
Headers http.Header
Body []byte
ExpiresAt time.Time
}
| Header | Description |
|---|
Idempotent-Replayed: true | Response was replayed from cache |
Technical Details
Cache Key Generation
By default, the middleware generates cache keys using SHA-256 hashing of:
- The idempotency key from the header
- The HTTP method (POST, PUT, etc.)
- The request URL path
This ensures that the same idempotency key can be safely reused across different endpoints and methods.
Response Capture
The middleware uses a custom responseCapture wrapper that:
- Captures the response status code (defaults to 200 OK)
- Clones all response headers
- Buffers the response body in memory
- Writes everything to both the buffer and the underlying ResponseWriter
Memory Store Implementation
The built-in MemoryStore:
- Uses
sync.RWMutex for thread-safe concurrent access
- Runs a background cleanup goroutine that removes expired entries every 10 minutes
- Stores responses with their expiration timestamps
- Returns
nil for expired entries automatically
Request Flow
- Method Check: Verifies the HTTP method is in the configured Methods list
- Key Extraction: Retrieves the idempotency key from the specified header
- Cache Lookup: Checks if a response exists for the generated cache key
- Cache Hit: If found and not expired, replays the cached response with
Idempotent-Replayed: true header
- Cache Miss: Wraps the ResponseWriter, executes the handler, captures the response, and stores it in the cache
- Expiration: Responses are stored with a TTL and automatically cleaned up
Best Practices
- Always use idempotency keys for payment operations
- Generate unique keys client-side (UUIDs work well)
- Include user context in key generation for multi-tenant apps
- Set appropriate TTL based on your use case
- Use distributed store (Redis) for multi-instance deployments
Testing
Test Coverage
| Test Case | Description | Expected Behavior |
|---|
TestNew | Basic idempotency with default configuration | First request executes handler, second request with same key returns cached response with Idempotent-Replayed: true header |
TestWithOptions_NoKey | Requests without idempotency key | All requests execute handler normally (no caching) |
TestWithOptions_DifferentKeys | Multiple requests with different keys | Each unique key executes handler independently |
TestWithOptions_CustomHeader | Custom header name configuration | Middleware uses custom header (e.g., X-Request-Id) for idempotency key |
TestWithOptions_Methods | Method filtering (POST only) | POST requests are cached, PUT requests are not cached |
TestWithOptions_GET | GET requests with idempotency key | GET requests bypass middleware (not cached by default) |
TestWithOptions_ResponseHeaders | Custom response headers | Cached response includes all custom headers from original response |
TestWithStore | Custom store integration | Middleware works with custom Store implementation |
TestMemoryStore | Memory store operations | Set, Get, and Delete operations work correctly |
TestMemoryStore_Expiry | Expired entries | Store returns nil for expired entries |
TestWithOptions_CustomKeyGenerator | User-scoped keys | Same idempotency key with different user IDs creates separate cache entries |
TestWithOptions_ResponseBody | JSON response caching | Complete response body (including JSON) is cached and replayed |
Client Example
// JavaScript client
const response = await fetch('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify({ amount: 100 })
});