mirror of https://github.com/gofiber/fiber.git
🐛 [Bug]: cache middleware: runtime error: index out of range [0] with length 0 (#3075)
Resolves #3072 Signed-off-by: brunodmartins <bdm2943@icloud.com>pull/3086/head
parent
a57b3c00c4
commit
f413bfef99
|
@ -117,8 +117,10 @@ func New(config ...Config) fiber.Handler {
|
||||||
// Get timestamp
|
// Get timestamp
|
||||||
ts := atomic.LoadUint64(×tamp)
|
ts := atomic.LoadUint64(×tamp)
|
||||||
|
|
||||||
|
// Cache Entry not found
|
||||||
|
if e != nil {
|
||||||
// Invalidate cache if requested
|
// Invalidate cache if requested
|
||||||
if cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) && e != nil {
|
if cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) {
|
||||||
e.exp = ts - 1
|
e.exp = ts - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +160,7 @@ func New(config ...Config) fiber.Handler {
|
||||||
// Return response
|
// Return response
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// make sure we're not blocking concurrent requests - do unlock
|
// make sure we're not blocking concurrent requests - do unlock
|
||||||
mux.Unlock()
|
mux.Unlock()
|
||||||
|
@ -193,6 +196,7 @@ func New(config ...Config) fiber.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e = manager.acquire()
|
||||||
// Cache response
|
// Cache response
|
||||||
e.body = utils.CopyBytes(c.Response().Body())
|
e.body = utils.CopyBytes(c.Response().Body())
|
||||||
e.status = c.Response().StatusCode()
|
e.status = c.Response().StatusCode()
|
||||||
|
|
|
@ -47,9 +47,10 @@ func Test_Cache_Expired(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
app.Use(New(Config{Expiration: 2 * time.Second}))
|
app.Use(New(Config{Expiration: 2 * time.Second}))
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
return c.SendString(strconv.FormatInt(time.Now().UnixNano(), 10))
|
count++
|
||||||
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
@ -86,9 +87,10 @@ func Test_Cache(t *testing.T) {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
app.Use(New())
|
app.Use(New())
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
now := strconv.FormatInt(time.Now().UnixNano(), 10)
|
count++
|
||||||
return c.SendString(now)
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
|
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
|
||||||
|
@ -305,9 +307,10 @@ func Test_Cache_Invalid_Expiration(t *testing.T) {
|
||||||
cache := New(Config{Expiration: 0 * time.Second})
|
cache := New(Config{Expiration: 0 * time.Second})
|
||||||
app.Use(cache)
|
app.Use(cache)
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
now := strconv.FormatInt(time.Now().UnixNano(), 10)
|
count++
|
||||||
return c.SendString(now)
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
|
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
|
||||||
|
@ -414,8 +417,10 @@ func Test_Cache_NothingToCache(t *testing.T) {
|
||||||
|
|
||||||
app.Use(New(Config{Expiration: -(time.Second * 1)}))
|
app.Use(New(Config{Expiration: -(time.Second * 1)}))
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
return c.SendString(time.Now().String())
|
count++
|
||||||
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
@ -447,12 +452,16 @@ func Test_Cache_CustomNext(t *testing.T) {
|
||||||
CacheControl: true,
|
CacheControl: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
return c.SendString(time.Now().String())
|
count++
|
||||||
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
errorCount := 0
|
||||||
app.Get("/error", func(c fiber.Ctx) error {
|
app.Get("/error", func(c fiber.Ctx) error {
|
||||||
return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String())
|
errorCount++
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString(strconv.Itoa(errorCount))
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
@ -508,9 +517,11 @@ func Test_CustomExpiration(t *testing.T) {
|
||||||
return time.Second * time.Duration(newCacheTime)
|
return time.Second * time.Duration(newCacheTime)
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
|
count++
|
||||||
c.Response().Header.Add("Cache-Time", "1")
|
c.Response().Header.Add("Cache-Time", "1")
|
||||||
return c.SendString(strconv.FormatInt(time.Now().UnixNano(), 10))
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
@ -588,8 +599,11 @@ func Test_CacheHeader(t *testing.T) {
|
||||||
return c.SendString(fiber.Query[string](c, "cache"))
|
return c.SendString(fiber.Query[string](c, "cache"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/error", func(c fiber.Ctx) error {
|
app.Get("/error", func(c fiber.Ctx) error {
|
||||||
return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String())
|
count++
|
||||||
|
c.Response().Header.Add("Cache-Time", "1")
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
@ -615,10 +629,13 @@ func Test_Cache_WithHead(t *testing.T) {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
app.Use(New())
|
app.Use(New())
|
||||||
|
|
||||||
|
count := 0
|
||||||
handler := func(c fiber.Ctx) error {
|
handler := func(c fiber.Ctx) error {
|
||||||
now := strconv.FormatInt(time.Now().UnixNano(), 10)
|
count++
|
||||||
return c.SendString(now)
|
c.Response().Header.Add("Cache-Time", "1")
|
||||||
|
return c.SendString(strconv.Itoa(count))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Route("/").Get(handler).Head(handler)
|
app.Route("/").Get(handler).Head(handler)
|
||||||
|
|
||||||
req := httptest.NewRequest(fiber.MethodHead, "/", nil)
|
req := httptest.NewRequest(fiber.MethodHead, "/", nil)
|
||||||
|
@ -708,8 +725,10 @@ func Test_CacheInvalidation(t *testing.T) {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
count := 0
|
||||||
app.Get("/", func(c fiber.Ctx) error {
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
return c.SendString(time.Now().String())
|
count++
|
||||||
|
return c.SendString(strconv.Itoa(count))
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
@ -731,6 +750,93 @@ func Test_CacheInvalidation(t *testing.T) {
|
||||||
require.NotEqual(t, body, bodyInvalidate)
|
require.NotEqual(t, body, bodyInvalidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CacheInvalidation_noCacheEntry(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("Cache Invalidator should not be called if no cache entry exist ", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
app := fiber.New()
|
||||||
|
cacheInvalidatorExecuted := false
|
||||||
|
app.Use(New(Config{
|
||||||
|
CacheControl: true,
|
||||||
|
CacheInvalidator: func(c fiber.Ctx) bool {
|
||||||
|
cacheInvalidatorExecuted = true
|
||||||
|
return fiber.Query[bool](c, "invalidate")
|
||||||
|
},
|
||||||
|
MaxBytes: 10 * 1024 * 1024,
|
||||||
|
}))
|
||||||
|
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?invalidate=true", nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, cacheInvalidatorExecuted)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CacheInvalidation_removeFromHeap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("Invalidate and remove from the heap", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(New(Config{
|
||||||
|
CacheControl: true,
|
||||||
|
CacheInvalidator: func(c fiber.Ctx) bool {
|
||||||
|
return fiber.Query[bool](c, "invalidate")
|
||||||
|
},
|
||||||
|
MaxBytes: 10 * 1024 * 1024,
|
||||||
|
}))
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
|
count++
|
||||||
|
return c.SendString(strconv.Itoa(count))
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
bodyCached, err := io.ReadAll(respCached.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, bytes.Equal(body, bodyCached))
|
||||||
|
require.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))
|
||||||
|
|
||||||
|
respInvalidate, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?invalidate=true", nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
bodyInvalidate, err := io.ReadAll(respInvalidate.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, body, bodyInvalidate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CacheStorage_CustomHeaders(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(New(Config{
|
||||||
|
CacheControl: true,
|
||||||
|
Storage: memory.New(),
|
||||||
|
MaxBytes: 10 * 1024 * 1024,
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
|
c.Response().Header.Set("Content-Type", "text/xml")
|
||||||
|
c.Response().Header.Set("Content-Encoding", "utf8")
|
||||||
|
return c.Send([]byte("<xml><value>Test</value></xml>"))
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
bodyCached, err := io.ReadAll(respCached.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, bytes.Equal(body, bodyCached))
|
||||||
|
require.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))
|
||||||
|
}
|
||||||
|
|
||||||
// Because time points are updated once every X milliseconds, entries in tests can often have
|
// Because time points are updated once every X milliseconds, entries in tests can often have
|
||||||
// equal expiration times and thus be in an random order. This closure hands out increasing
|
// equal expiration times and thus be in an random order. This closure hands out increasing
|
||||||
// time intervals to maintain strong ascending order of expiration
|
// time intervals to maintain strong ascending order of expiration
|
||||||
|
|
|
@ -15,7 +15,7 @@ type heapEntry struct {
|
||||||
// elements in constant time. It does so by handing out special indices
|
// elements in constant time. It does so by handing out special indices
|
||||||
// and tracking entry movement.
|
// and tracking entry movement.
|
||||||
//
|
//
|
||||||
// indexdedHeap is used for quickly finding entries with the lowest
|
// indexedHeap is used for quickly finding entries with the lowest
|
||||||
// expiration timestamp and deleting arbitrary entries.
|
// expiration timestamp and deleting arbitrary entries.
|
||||||
type indexedHeap struct {
|
type indexedHeap struct {
|
||||||
// Slice the heap is built on
|
// Slice the heap is built on
|
||||||
|
|
|
@ -83,8 +83,7 @@ func (m *manager) get(key string) *item {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool
|
if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool
|
||||||
it = m.acquire()
|
return nil
|
||||||
return it
|
|
||||||
}
|
}
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/utils/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_manager_get(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cacheManager := newManager(nil)
|
||||||
|
t.Run("Item not found in cache", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.Nil(t, cacheManager.get(utils.UUID()))
|
||||||
|
})
|
||||||
|
t.Run("Item found in cache", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
id := utils.UUID()
|
||||||
|
cacheItem := cacheManager.acquire()
|
||||||
|
cacheItem.body = []byte("test-body")
|
||||||
|
cacheManager.set(id, cacheItem, 10*time.Second)
|
||||||
|
assert.NotNil(t, cacheManager.get(id))
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue