Skip to content

Commit

Permalink
🚀 [Feature]: Cache-Control: no-cache (#2159)
Browse files Browse the repository at this point in the history
* Added noCache field

Check if the request header Cache-Control contains no-cache

* Update cache.go

* Update config.go

* Update cache.go

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1

* patch-1
  • Loading branch information
marcmartin13 authored Oct 21, 2022
1 parent 5fb93fd commit c187c6a
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 1 deletion.
4 changes: 4 additions & 0 deletions middleware/cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Cache middleware for [Fiber](https://github.com/gofiber/fiber) designed to intercept responses and cache them. This middleware will cache the `Body`, `Content-Type` and `StatusCode` using the `c.Path()` (or a string returned by the Key function) as unique identifier. Special thanks to [@codemicro](https://github.com/codemicro/fiber-cache) for creating this middleware for Fiber core!

Request Directives
The no-cache request directive will return the up-to-date response but still caches it. You will always get a "miss" cache status.
The no-store request directive will refrain from caching. You will always get the up-to-date response.

## Table of Contents

- [Cache Middleware](#cache-middleware)
Expand Down
19 changes: 18 additions & 1 deletion middleware/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package cache

import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
Expand All @@ -27,6 +28,12 @@ const (
cacheMiss = "miss"
)

// directives
const (
noCache = "no-cache"
noStore = "no-store"
)

var ignoreHeaders = map[string]interface{}{
"Connection": nil,
"Keep-Alive": nil,
Expand Down Expand Up @@ -83,6 +90,11 @@ func New(config ...Config) fiber.Handler {

// Return new handler
return func(c *fiber.Ctx) error {
// Refrain from caching
if hasRequestDirective(c, noStore) {
return c.Next()
}

// Only cache selected methods
var isExists bool
for _, method := range cfg.Methods {
Expand Down Expand Up @@ -116,7 +128,7 @@ func New(config ...Config) fiber.Handler {
_, size := heap.remove(e.heapidx)
storedBytes -= size
}
} else if e.exp != 0 {
} else if e.exp != 0 && !hasRequestDirective(c, noCache) {
// Separate body value to avoid msgp serialization
// We can store raw bytes with Storage 👍
if cfg.Storage != nil {
Expand Down Expand Up @@ -235,3 +247,8 @@ func New(config ...Config) fiber.Handler {
return nil
}
}

// Check if request has directive
func hasRequestDirective(c *fiber.Ctx, directive string) bool {
return strings.Contains(c.Get(fiber.HeaderCacheControl), directive)
}
154 changes: 154 additions & 0 deletions middleware/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/etag"
"github.com/gofiber/fiber/v2/internal/storage/memory"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
Expand Down Expand Up @@ -107,6 +108,159 @@ func Test_Cache(t *testing.T) {
utils.AssertEqual(t, cachedBody, body)
}

// go test -run Test_Cache_WithNoCacheRequestDirective
func Test_Cache_WithNoCacheRequestDirective(t *testing.T) {
t.Parallel()

app := fiber.New()
app.Use(New())

app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("id", "1"))
})

// Request id = 1
req := httptest.NewRequest("GET", "/", nil)
resp, err := app.Test(req)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache"))
utils.AssertEqual(t, []byte("1"), body)
// Response cached, entry id = 1

// Request id = 2 without Cache-Control: no-cache
cachedReq := httptest.NewRequest("GET", "/?id=2", nil)
cachedResp, err := app.Test(cachedReq)
defer cachedResp.Body.Close()
cachedBody, _ := ioutil.ReadAll(cachedResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheHit, cachedResp.Header.Get("X-Cache"))
utils.AssertEqual(t, []byte("1"), cachedBody)
// Response not cached, returns cached response, entry id = 1

// Request id = 2 with Cache-Control: no-cache
noCacheReq := httptest.NewRequest("GET", "/?id=2", nil)
noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache)
noCacheResp, err := app.Test(noCacheReq)
defer noCacheResp.Body.Close()
noCacheBody, _ := ioutil.ReadAll(noCacheResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, noCacheResp.Header.Get("X-Cache"))
utils.AssertEqual(t, []byte("2"), noCacheBody)
// Response cached, returns updated response, entry = 2

/* Check Test_Cache_WithETagAndNoCacheRequestDirective */
// Request id = 2 with Cache-Control: no-cache again
noCacheReq1 := httptest.NewRequest("GET", "/?id=2", nil)
noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache)
noCacheResp1, err := app.Test(noCacheReq1)
defer noCacheResp1.Body.Close()
noCacheBody1, _ := ioutil.ReadAll(noCacheResp1.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, noCacheResp1.Header.Get("X-Cache"))
utils.AssertEqual(t, []byte("2"), noCacheBody1)
// Response cached, returns updated response, entry = 2

// Request id = 1 without Cache-Control: no-cache
cachedReq1 := httptest.NewRequest("GET", "/", nil)
cachedResp1, err := app.Test(cachedReq1)
defer cachedResp1.Body.Close()
cachedBody1, _ := ioutil.ReadAll(cachedResp1.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheHit, cachedResp1.Header.Get("X-Cache"))
utils.AssertEqual(t, []byte("2"), cachedBody1)
// Response not cached, returns cached response, entry id = 2
}

// go test -run Test_Cache_WithETagAndNoCacheRequestDirective
func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) {
t.Parallel()

app := fiber.New()
app.Use(
etag.New(),
New(),
)

app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("id", "1"))
})

// Request id = 1
req := httptest.NewRequest("GET", "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache"))
utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode)
// Response cached, entry id = 1

// If response status 200
etagToken := resp.Header.Get("Etag")

// Request id = 2 with ETag but without Cache-Control: no-cache
cachedReq := httptest.NewRequest("GET", "/?id=2", nil)
cachedReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken)
cachedResp, err := app.Test(cachedReq)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheHit, cachedResp.Header.Get("X-Cache"))
utils.AssertEqual(t, fiber.StatusNotModified, cachedResp.StatusCode)
// Response not cached, returns cached response, entry id = 1, status not modified

// Request id = 2 with ETag and Cache-Control: no-cache
noCacheReq := httptest.NewRequest("GET", "/?id=2", nil)
noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache)
noCacheReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken)
noCacheResp, err := app.Test(noCacheReq)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, noCacheResp.Header.Get("X-Cache"))
utils.AssertEqual(t, fiber.StatusOK, noCacheResp.StatusCode)
// Response cached, returns updated response, entry id = 2

// If response status 200
etagToken = noCacheResp.Header.Get("Etag")

// Request id = 2 with ETag and Cache-Control: no-cache again
noCacheReq1 := httptest.NewRequest("GET", "/?id=2", nil)
noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache)
noCacheReq1.Header.Set(fiber.HeaderIfNoneMatch, etagToken)
noCacheResp1, err := app.Test(noCacheReq1)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, noCacheResp1.Header.Get("X-Cache"))
utils.AssertEqual(t, fiber.StatusNotModified, noCacheResp1.StatusCode)
// Response cached, returns updated response, entry id = 2, status not modified

// Request id = 1 without ETag and Cache-Control: no-cache
cachedReq1 := httptest.NewRequest("GET", "/", nil)
cachedResp1, err := app.Test(cachedReq1)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheHit, cachedResp1.Header.Get("X-Cache"))
utils.AssertEqual(t, fiber.StatusOK, cachedResp1.StatusCode)
// Response not cached, returns cached response, entry id = 2
}

// go test -run Test_Cache_WithNoStoreRequestDirective
func Test_Cache_WithNoStoreRequestDirective(t *testing.T) {
t.Parallel()

app := fiber.New()
app.Use(New())

app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("id", "1"))
})

// Request id = 2
noStoreReq := httptest.NewRequest("GET", "/?id=2", nil)
noStoreReq.Header.Set(fiber.HeaderCacheControl, noStore)
noStoreResp, err := app.Test(noStoreReq)
defer noStoreResp.Body.Close()
noStoreBody, _ := ioutil.ReadAll(noStoreResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, []byte("2"), noStoreBody)
// Response not cached, returns updated response
}

func Test_Cache_WithSeveralRequests(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit c187c6a

Please sign in to comment.