Gin: Middleware for API Caching

Gin: Middleware for API Caching

ยท

2 min read

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

๐Ÿค“

Did you find this article valuable?

Support Dhairya's Blog by becoming a sponsor. Any amount is appreciated!