In a recent Gin server project, I wanted to incorporate API caching effortlessly. So, I decided to build a middleware for it, thinking it would be a breeze. But with Gin, things got a bit tricky. Let me walk you through this.
Request Flow
Middleware Triggered
Upon the arrival of a request, the middleware is the initial point of contact.
Cached Response Check
The middleware promptly examines whether the response for the request is already cached.
data, err := cache.RedisClient.Get(c, cacheKey)
if err == nil {
c.JSON(200, data)
c.abort()
return
}
It is necessary to call c.abort() to stop the execution of remaining handlers in chain.
Cached Response Found
If a cached response exists, it is immediately returned. Subsequent handlers for that request are skipped to enhance efficiency.
No Cached Response
If no cached response is found, control is handed over to the next handler in line. We have to replace the gin writer with our writer since we can not access the response after the main handler has written over it. This writer just writes the response to buffer for us to access later and calls the main gin writer.
// custom writer
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = w
c.Next()
Return to Middleware:
After the handler's execution, the control returns to the cache middleware. Here we read the buffer for response and write it to the cache.
response := w.body.String()
responseStatus := c.Writer.Status()
if err := cache.RedisClient.Set(c, cacheKey, response, expiry); err != nil {
log.Printf("failed to set cache %v", err)
}
Recheck and Cache:
The middleware reevaluates the response, caching it for potential future requests. This ensures subsequent requests can benefit from the now-cached data.
Router
Simply add this middleware to the route.
route.GET(
"/api/event",
APICacheParam("cache_prefix", 10*time.Minute),
handler,
)
Code
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
func APICacheParam(cachePrefix string, expiry time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
data, err := cache.RedisClient.Get(c, cachePrefix)
if err == nil {
c.JSON(200, data)
c.abort()
return
}
// using separate writer to capture response
w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = w
c.Next()
response := w.body.String()
responseStatus := c.Writer.Status()
if responseStatus == http.StatusOK {
if err := cache.RedisClient.Set(c, cacheKey, response, expiry); err != nil {
log.Printf("failed to set cache %v", err)
}
}
}
}
Happy Coding