diff --git a/middleware/cache/README.md b/middleware/cache/README.md index a5f3921a7a..373446486b 100644 --- a/middleware/cache/README.md +++ b/middleware/cache/README.md @@ -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) diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index a8485f450f..f6db49e2cf 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -4,6 +4,7 @@ package cache import ( "strconv" + "strings" "sync" "sync/atomic" "time" @@ -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, @@ -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 { @@ -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 { @@ -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) +} diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 78aeab4f71..3216a7c48a 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -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" @@ -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()