go 74 lines · 10 steps

Idempotent requests in Gin with Redis

A Gin middleware that replays cached responses for repeated Idempotency-Key headers and locks concurrent duplicates.

Explained by highlit
1package middleware
2 
3import (
4 "bytes"
5 "encoding/json"
6 "net/http"
7 "time"
8 
9 "github.com/gin-gonic/gin"
10 "github.com/redis/go-redis/v9"
11)
12 
13type cachedResponse struct {
14 Status int `json:"status"`
15 Headers map[string][]string `json:"headers"`
16 Body []byte `json:"body"`
17}
18 
19type bodyWriter struct {
20 gin.ResponseWriter
21 buf *bytes.Buffer
22}
23 
24func (w *bodyWriter) Write(b []byte) (int, error) {
25 w.buf.Write(b)
26 return w.ResponseWriter.Write(b)
27}
28 
29func Idempotency(rdb *redis.Client, ttl time.Duration) gin.HandlerFunc {
30 return func(c *gin.Context) {
31 key := c.GetHeader("Idempotency-Key")
32 if key == "" {
33 c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Idempotency-Key header required"})
34 return
35 }
36 
37 cacheKey := "idem:" + c.FullPath() + ":" + key
38 ctx := c.Request.Context()
39 
40 if raw, err := rdb.Get(ctx, cacheKey).Bytes(); err == nil {
41 var cached cachedResponse
42 if json.Unmarshal(raw, &cached) == nil {
43 for h, vals := range cached.Headers {
44 for _, v := range vals {
45 c.Writer.Header().Add(h, v)
46 }
47 }
48 c.Writer.Header().Set("Idempotent-Replay", "true")
49 c.Data(cached.Status, "application/json", cached.Body)
50 c.Abort()
51 return
52 }
53 }
54 
55 if ok, _ := rdb.SetNX(ctx, cacheKey+":lock", "1", 30*time.Second).Result(); !ok {
56 c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "request with this Idempotency-Key is in progress"})
57 return
58 }
59 
60 bw := &bodyWriter{ResponseWriter: c.Writer, buf: &bytes.Buffer{}}
61 c.Writer = bw
62 c.Next()
63 
64 if c.Writer.Status() >= 200 && c.Writer.Status() < 300 {
65 payload, _ := json.Marshal(cachedResponse{
66 Status: c.Writer.Status(),
67 Headers: c.Writer.Header(),
68 Body: bw.buf.Bytes(),
69 })
70 rdb.Set(ctx, cacheKey, payload, ttl)
71 }
72 rdb.Del(ctx, cacheKey+":lock")
73 }
74}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Storing the full response — status, headers, and body — lets you replay a prior result byte-for-byte on retries.
  2. 2A short-lived SetNX lock turns two in-flight duplicates into a clean 409 instead of a double side effect.
  3. 3Wrapping the ResponseWriter is how you capture a handler's output without changing the handler itself.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent requests in Gin with Redis — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code