1
0
Fork 0

Adding upstream version 2.52.6.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-17 06:50:16 +02:00
parent a960158181
commit 6d002e9543
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
441 changed files with 95392 additions and 0 deletions

252
middleware/cache/cache.go vendored Normal file
View file

@ -0,0 +1,252 @@
// Special thanks to @codemicro for moving this to fiber core
// Original middleware: github.com/codemicro/fiber-cache
package cache
import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
)
// timestampUpdatePeriod is the period which is used to check the cache expiration.
// It should not be too long to provide more or less acceptable expiration error, and in the same
// time it should not be too short to avoid overwhelming of the system
const timestampUpdatePeriod = 300 * time.Millisecond
// cache status
// unreachable: when cache is bypass, or invalid
// hit: cache is served
// miss: do not have cache record
const (
cacheUnreachable = "unreachable"
cacheHit = "hit"
cacheMiss = "miss"
)
// directives
const (
noCache = "no-cache"
noStore = "no-store"
)
var ignoreHeaders = map[string]interface{}{
"Connection": nil,
"Keep-Alive": nil,
"Proxy-Authenticate": nil,
"Proxy-Authorization": nil,
"TE": nil,
"Trailers": nil,
"Transfer-Encoding": nil,
"Upgrade": nil,
"Content-Type": nil, // already stored explicitly by the cache manager
"Content-Encoding": nil, // already stored explicitly by the cache manager
}
// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
// Set default config
cfg := configDefault(config...)
// Nothing to cache
if int(cfg.Expiration.Seconds()) < 0 {
return func(c *fiber.Ctx) error {
return c.Next()
}
}
var (
// Cache settings
mux = &sync.RWMutex{}
timestamp = uint64(time.Now().Unix())
)
// Create manager to simplify storage operations ( see manager.go )
manager := newManager(cfg.Storage)
// Create indexed heap for tracking expirations ( see heap.go )
heap := &indexedHeap{}
// count stored bytes (sizes of response bodies)
var storedBytes uint
// Update timestamp in the configured interval
go func() {
for {
atomic.StoreUint64(&timestamp, uint64(time.Now().Unix()))
time.Sleep(timestampUpdatePeriod)
}
}()
// Delete key from both manager and storage
deleteKey := func(dkey string) {
manager.del(dkey)
// External storage saves body data with different key
if cfg.Storage != nil {
manager.del(dkey + "_body")
}
}
// 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 {
if c.Method() == method {
isExists = true
}
}
if !isExists {
c.Set(cfg.CacheHeader, cacheUnreachable)
return c.Next()
}
// Get key from request
// TODO(allocation optimization): try to minimize the allocation from 2 to 1
key := cfg.KeyGenerator(c) + "_" + c.Method()
// Get entry from pool
e := manager.get(key)
// Lock entry
mux.Lock()
// Get timestamp
ts := atomic.LoadUint64(&timestamp)
// Check if entry is expired
if e.exp != 0 && ts >= e.exp {
deleteKey(key)
if cfg.MaxBytes > 0 {
_, size := heap.remove(e.heapidx)
storedBytes -= size
}
} 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 {
e.body = manager.getRaw(key + "_body")
}
// Set response headers from cache
c.Response().SetBodyRaw(e.body)
c.Response().SetStatusCode(e.status)
c.Response().Header.SetContentTypeBytes(e.ctype)
if len(e.cencoding) > 0 {
c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding)
}
for k, v := range e.headers {
c.Response().Header.SetBytesV(k, v)
}
// Set Cache-Control header if enabled
if cfg.CacheControl {
maxAge := strconv.FormatUint(e.exp-ts, 10)
c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge)
}
c.Set(cfg.CacheHeader, cacheHit)
mux.Unlock()
// Return response
return nil
}
// make sure we're not blocking concurrent requests - do unlock
mux.Unlock()
// Continue stack, return err to Fiber if exist
if err := c.Next(); err != nil {
return err
}
// lock entry back and unlock on finish
mux.Lock()
defer mux.Unlock()
// Don't cache response if Next returns true
if cfg.Next != nil && cfg.Next(c) {
c.Set(cfg.CacheHeader, cacheUnreachable)
return nil
}
// Don't try to cache if body won't fit into cache
bodySize := uint(len(c.Response().Body()))
if cfg.MaxBytes > 0 && bodySize > cfg.MaxBytes {
c.Set(cfg.CacheHeader, cacheUnreachable)
return nil
}
// Remove oldest to make room for new
if cfg.MaxBytes > 0 {
for storedBytes+bodySize > cfg.MaxBytes {
key, size := heap.removeFirst()
deleteKey(key)
storedBytes -= size
}
}
// Cache response
e.body = utils.CopyBytes(c.Response().Body())
e.status = c.Response().StatusCode()
e.ctype = utils.CopyBytes(c.Response().Header.ContentType())
e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding))
// Store all response headers
// (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1)
if cfg.StoreResponseHeaders {
e.headers = make(map[string][]byte)
c.Response().Header.VisitAll(
func(key, value []byte) {
// create real copy
keyS := string(key)
if _, ok := ignoreHeaders[keyS]; !ok {
e.headers[keyS] = utils.CopyBytes(value)
}
},
)
}
// default cache expiration
expiration := cfg.Expiration
// Calculate expiration by response header or other setting
if cfg.ExpirationGenerator != nil {
expiration = cfg.ExpirationGenerator(c, &cfg)
}
e.exp = ts + uint64(expiration.Seconds())
// Store entry in heap
if cfg.MaxBytes > 0 {
e.heapidx = heap.put(key, e.exp, bodySize)
storedBytes += bodySize
}
// For external Storage we store raw body separated
if cfg.Storage != nil {
manager.setRaw(key+"_body", e.body, expiration)
// avoid body msgp encoding
e.body = nil
manager.set(key, e, expiration)
manager.release(e)
} else {
// Store entry in memory
manager.set(key, e, expiration)
}
c.Set(cfg.CacheHeader, cacheMiss)
// Finish response
return nil
}
}
// Check if request has directive
func hasRequestDirective(c *fiber.Ctx, directive string) bool {
return strings.Contains(c.Get(fiber.HeaderCacheControl), directive)
}

901
middleware/cache/cache_test.go vendored Normal file
View file

@ -0,0 +1,901 @@
// Special thanks to @codemicro for moving this to fiber core
// Original middleware: github.com/codemicro/fiber-cache
package cache
import (
"bytes"
"fmt"
"io"
"math"
"net/http/httptest"
"os"
"strconv"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/internal/storage/memory"
"github.com/gofiber/fiber/v2/middleware/etag"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
)
func Test_Cache_CacheControl(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
CacheControl: true,
Expiration: 10 * time.Second,
}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "public, max-age=10", resp.Header.Get(fiber.HeaderCacheControl))
}
func Test_Cache_Expired(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{Expiration: 2 * time.Second}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(fmt.Sprintf("%d", time.Now().UnixNano()))
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
// Sleep until the cache is expired
time.Sleep(3 * time.Second)
respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
bodyCached, err := io.ReadAll(respCached.Body)
utils.AssertEqual(t, nil, err)
if bytes.Equal(body, bodyCached) {
t.Errorf("Cache should have expired: %s, %s", body, bodyCached)
}
// Next response should be also cached
respCachedNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
bodyCachedNextRound, err := io.ReadAll(respCachedNextRound.Body)
utils.AssertEqual(t, nil, err)
if !bytes.Equal(bodyCachedNextRound, bodyCached) {
t.Errorf("Cache should not have expired: %s, %s", bodyCached, bodyCachedNextRound)
}
}
func Test_Cache(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
now := fmt.Sprintf("%d", time.Now().UnixNano())
return c.SendString(now)
})
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
cachedReq := httptest.NewRequest(fiber.MethodGet, "/", nil)
cachedResp, err := app.Test(cachedReq)
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
cachedBody, err := io.ReadAll(cachedResp.Body)
utils.AssertEqual(t, nil, err)
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(fiber.MethodGet, "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
body, err := io.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(fiber.MethodGet, "/?id=2", nil)
cachedResp, err := app.Test(cachedReq)
utils.AssertEqual(t, nil, err)
cachedBody, err := io.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(fiber.MethodGet, "/?id=2", nil)
noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache)
noCacheResp, err := app.Test(noCacheReq)
utils.AssertEqual(t, nil, err)
noCacheBody, err := io.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(fiber.MethodGet, "/?id=2", nil)
noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache)
noCacheResp1, err := app.Test(noCacheReq1)
utils.AssertEqual(t, nil, err)
noCacheBody1, err := io.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(fiber.MethodGet, "/", nil)
cachedResp1, err := app.Test(cachedReq1)
utils.AssertEqual(t, nil, err)
cachedBody1, err := io.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(fiber.MethodGet, "/", 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(fiber.MethodGet, "/?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(fiber.MethodGet, "/?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(fiber.MethodGet, "/?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(fiber.MethodGet, "/", 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(fiber.MethodGet, "/?id=2", nil)
noStoreReq.Header.Set(fiber.HeaderCacheControl, noStore)
noStoreResp, err := app.Test(noStoreReq)
utils.AssertEqual(t, nil, err)
noStoreBody, err := io.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()
app := fiber.New()
app.Use(New(Config{
CacheControl: true,
Expiration: 10 * time.Second,
}))
app.Get("/:id", func(c *fiber.Ctx) error {
return c.SendString(c.Params("id"))
})
for runs := 0; runs < 10; runs++ {
for i := 0; i < 10; i++ {
func(id int) {
rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf("/%d", id), nil))
utils.AssertEqual(t, nil, err)
defer func(body io.ReadCloser) {
err := body.Close()
utils.AssertEqual(t, nil, err)
}(rsp.Body)
idFromServ, err := io.ReadAll(rsp.Body)
utils.AssertEqual(t, nil, err)
a, err := strconv.Atoi(string(idFromServ))
utils.AssertEqual(t, nil, err)
// SomeTimes,The id is not equal with a
utils.AssertEqual(t, id, a)
}(i)
}
}
}
func Test_Cache_Invalid_Expiration(t *testing.T) {
t.Parallel()
app := fiber.New()
cache := New(Config{Expiration: 0 * time.Second})
app.Use(cache)
app.Get("/", func(c *fiber.Ctx) error {
now := fmt.Sprintf("%d", time.Now().UnixNano())
return c.SendString(now)
})
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
cachedReq := httptest.NewRequest(fiber.MethodGet, "/", nil)
cachedResp, err := app.Test(cachedReq)
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
cachedBody, err := io.ReadAll(cachedResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cachedBody, body)
}
func Test_Cache_Get(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Post("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("cache"))
})
app.Get("/get", func(c *fiber.Ctx) error {
return c.SendString(c.Query("cache"))
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=123", nil))
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(body))
resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil))
utils.AssertEqual(t, nil, err)
body, err = io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "12345", string(body))
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=123", nil))
utils.AssertEqual(t, nil, err)
body, err = io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(body))
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=12345", nil))
utils.AssertEqual(t, nil, err)
body, err = io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(body))
}
func Test_Cache_Post(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
Methods: []string{fiber.MethodPost},
}))
app.Post("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("cache"))
})
app.Get("/get", func(c *fiber.Ctx) error {
return c.SendString(c.Query("cache"))
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=123", nil))
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(body))
resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil))
utils.AssertEqual(t, nil, err)
body, err = io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(body))
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=123", nil))
utils.AssertEqual(t, nil, err)
body, err = io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(body))
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=12345", nil))
utils.AssertEqual(t, nil, err)
body, err = io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "12345", string(body))
}
func Test_Cache_NothingToCache(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{Expiration: -(time.Second * 1)}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(time.Now().String())
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
time.Sleep(500 * time.Millisecond)
respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
bodyCached, err := io.ReadAll(respCached.Body)
utils.AssertEqual(t, nil, err)
if bytes.Equal(body, bodyCached) {
t.Errorf("Cache should have expired: %s, %s", body, bodyCached)
}
}
func Test_Cache_CustomNext(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
Next: func(c *fiber.Ctx) bool {
return c.Response().StatusCode() != fiber.StatusOK
},
CacheControl: true,
}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(time.Now().String())
})
app.Get("/error", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String())
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
bodyCached, err := io.ReadAll(respCached.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, true, bytes.Equal(body, bodyCached))
utils.AssertEqual(t, true, respCached.Header.Get(fiber.HeaderCacheControl) != "")
_, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil))
utils.AssertEqual(t, nil, err)
errRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, true, errRespCached.Header.Get(fiber.HeaderCacheControl) == "")
}
func Test_CustomKey(t *testing.T) {
t.Parallel()
app := fiber.New()
var called bool
app.Use(New(Config{KeyGenerator: func(c *fiber.Ctx) string {
called = true
return utils.CopyString(c.Path())
}}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("hi")
})
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
_, err := app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, true, called)
}
func Test_CustomExpiration(t *testing.T) {
t.Parallel()
app := fiber.New()
var called bool
var newCacheTime int
app.Use(New(Config{ExpirationGenerator: func(c *fiber.Ctx, cfg *Config) time.Duration {
called = true
var err error
newCacheTime, err = strconv.Atoi(c.GetRespHeader("Cache-Time", "600"))
utils.AssertEqual(t, nil, err)
return time.Second * time.Duration(newCacheTime)
}}))
app.Get("/", func(c *fiber.Ctx) error {
c.Response().Header.Add("Cache-Time", "1")
now := fmt.Sprintf("%d", time.Now().UnixNano())
return c.SendString(now)
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, true, called)
utils.AssertEqual(t, 1, newCacheTime)
// Sleep until the cache is expired
time.Sleep(1 * time.Second)
cachedResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
cachedBody, err := io.ReadAll(cachedResp.Body)
utils.AssertEqual(t, nil, err)
if bytes.Equal(body, cachedBody) {
t.Errorf("Cache should have expired: %s, %s", body, cachedBody)
}
// Next response should be cached
cachedRespNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
cachedBodyNextRound, err := io.ReadAll(cachedRespNextRound.Body)
utils.AssertEqual(t, nil, err)
if !bytes.Equal(cachedBodyNextRound, cachedBody) {
t.Errorf("Cache should not have expired: %s, %s", cachedBodyNextRound, cachedBody)
}
}
func Test_AdditionalE2EResponseHeaders(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
StoreResponseHeaders: true,
}))
app.Get("/", func(c *fiber.Ctx) error {
c.Response().Header.Add("X-Foobar", "foobar")
return c.SendString("hi")
})
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "foobar", resp.Header.Get("X-Foobar"))
req = httptest.NewRequest(fiber.MethodGet, "/", nil)
resp, err = app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "foobar", resp.Header.Get("X-Foobar"))
}
func Test_CacheHeader(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
Expiration: 10 * time.Second,
Next: func(c *fiber.Ctx) bool {
return c.Response().StatusCode() != fiber.StatusOK
},
}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Post("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("cache"))
})
app.Get("/error", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String())
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache"))
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheHit, resp.Header.Get("X-Cache"))
resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheUnreachable, resp.Header.Get("X-Cache"))
errRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheUnreachable, errRespCached.Header.Get("X-Cache"))
}
func Test_Cache_WithHead(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
now := fmt.Sprintf("%d", time.Now().UnixNano())
return c.SendString(now)
})
req := httptest.NewRequest(fiber.MethodHead, "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache"))
cachedReq := httptest.NewRequest(fiber.MethodHead, "/", nil)
cachedResp, err := app.Test(cachedReq)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheHit, cachedResp.Header.Get("X-Cache"))
body, err := io.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
cachedBody, err := io.ReadAll(cachedResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cachedBody, body)
}
func Test_Cache_WithHeadThenGet(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString(c.Query("cache"))
})
headResp, err := app.Test(httptest.NewRequest(fiber.MethodHead, "/?cache=123", nil))
utils.AssertEqual(t, nil, err)
headBody, err := io.ReadAll(headResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "", string(headBody))
utils.AssertEqual(t, cacheMiss, headResp.Header.Get("X-Cache"))
headResp, err = app.Test(httptest.NewRequest(fiber.MethodHead, "/?cache=123", nil))
utils.AssertEqual(t, nil, err)
headBody, err = io.ReadAll(headResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "", string(headBody))
utils.AssertEqual(t, cacheHit, headResp.Header.Get("X-Cache"))
getResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?cache=123", nil))
utils.AssertEqual(t, nil, err)
getBody, err := io.ReadAll(getResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(getBody))
utils.AssertEqual(t, cacheMiss, getResp.Header.Get("X-Cache"))
getResp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/?cache=123", nil))
utils.AssertEqual(t, nil, err)
getBody, err = io.ReadAll(getResp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "123", string(getBody))
utils.AssertEqual(t, cacheHit, getResp.Header.Get("X-Cache"))
}
func Test_CustomCacheHeader(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
CacheHeader: "Cache-Status",
}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, cacheMiss, resp.Header.Get("Cache-Status"))
}
// 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
// time intervals to maintain strong ascending order of expiration
func stableAscendingExpiration() func(c1 *fiber.Ctx, c2 *Config) time.Duration {
i := 0
return func(c1 *fiber.Ctx, c2 *Config) time.Duration {
i++
return time.Hour * time.Duration(i)
}
}
func Test_Cache_MaxBytesOrder(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
MaxBytes: 2,
ExpirationGenerator: stableAscendingExpiration(),
}))
app.Get("/*", func(c *fiber.Ctx) error {
return c.SendString("1")
})
cases := [][]string{
// Insert a, b into cache of size 2 bytes (responses are 1 byte)
{"/a", cacheMiss},
{"/b", cacheMiss},
{"/a", cacheHit},
{"/b", cacheHit},
// Add c -> a evicted
{"/c", cacheMiss},
{"/b", cacheHit},
// Add a again -> b evicted
{"/a", cacheMiss},
{"/c", cacheHit},
// Add b -> c evicted
{"/b", cacheMiss},
{"/c", cacheMiss},
}
for idx, tcase := range cases {
rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, tcase[1], rsp.Header.Get("X-Cache"), fmt.Sprintf("Case %v", idx))
}
}
func Test_Cache_MaxBytesSizes(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
MaxBytes: 7,
ExpirationGenerator: stableAscendingExpiration(),
}))
app.Get("/*", func(c *fiber.Ctx) error {
path := c.Context().URI().LastPathSegment()
size, err := strconv.Atoi(string(path))
utils.AssertEqual(t, nil, err)
return c.Send(make([]byte, size))
})
cases := [][]string{
{"/1", cacheMiss},
{"/2", cacheMiss},
{"/3", cacheMiss},
{"/4", cacheMiss}, // 1+2+3+4 > 7 => 1,2 are evicted now
{"/3", cacheHit},
{"/1", cacheMiss},
{"/2", cacheMiss},
{"/8", cacheUnreachable}, // too big to cache -> unreachable
}
for idx, tcase := range cases {
rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, tcase[1], rsp.Header.Get("X-Cache"), fmt.Sprintf("Case %v", idx))
}
}
// go test -v -run=^$ -bench=Benchmark_Cache -benchmem -count=4
func Benchmark_Cache(b *testing.B) {
app := fiber.New()
app.Use(New())
app.Get("/demo", func(c *fiber.Ctx) error {
data, _ := os.ReadFile("../../.github/README.md") //nolint:errcheck // We're inside a benchmark
return c.Status(fiber.StatusTeapot).Send(data)
})
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
fctx.Request.SetRequestURI("/demo")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
h(fctx)
}
utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())
utils.AssertEqual(b, true, len(fctx.Response.Body()) > 30000)
}
// go test -v -run=^$ -bench=Benchmark_Cache_Storage -benchmem -count=4
func Benchmark_Cache_Storage(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Storage: memory.New(),
}))
app.Get("/demo", func(c *fiber.Ctx) error {
data, _ := os.ReadFile("../../.github/README.md") //nolint:errcheck // We're inside a benchmark
return c.Status(fiber.StatusTeapot).Send(data)
})
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
fctx.Request.SetRequestURI("/demo")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
h(fctx)
}
utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())
utils.AssertEqual(b, true, len(fctx.Response.Body()) > 30000)
}
func Benchmark_Cache_AdditionalHeaders(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
StoreResponseHeaders: true,
}))
app.Get("/demo", func(c *fiber.Ctx) error {
c.Response().Header.Add("X-Foobar", "foobar")
return c.SendStatus(418)
})
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
fctx.Request.SetRequestURI("/demo")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
h(fctx)
}
utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())
utils.AssertEqual(b, []byte("foobar"), fctx.Response.Header.Peek("X-Foobar"))
}
func Benchmark_Cache_MaxSize(b *testing.B) {
// The benchmark is run with three different MaxSize parameters
// 1) 0: Tracking is disabled = no overhead
// 2) MaxInt32: Enough to store all entries = no removals
// 3) 100: Small size = constant insertions and removals
cases := []uint{0, math.MaxUint32, 100}
names := []string{"Disabled", "Unlim", "LowBounded"}
for i, size := range cases {
b.Run(names[i], func(b *testing.B) {
app := fiber.New()
app.Use(New(Config{MaxBytes: size}))
app.Get("/*", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTeapot).SendString("1")
})
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
fctx.Request.SetRequestURI(fmt.Sprintf("/%v", n))
h(fctx)
}
utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())
})
}
}

128
middleware/cache/config.go vendored Normal file
View file

@ -0,0 +1,128 @@
package cache
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/fiber/v2/utils"
)
// Config defines the config for middleware.
type Config struct {
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c *fiber.Ctx) bool
// Expiration is the time that an cached response will live
//
// Optional. Default: 1 * time.Minute
Expiration time.Duration
// CacheHeader header on response header, indicate cache status, with the following possible return value
//
// hit, miss, unreachable
//
// Optional. Default: X-Cache
CacheHeader string
// CacheControl enables client side caching if set to true
//
// Optional. Default: false
CacheControl bool
// Key allows you to generate custom keys, by default c.Path() is used
//
// Default: func(c *fiber.Ctx) string {
// return utils.CopyString(c.Path())
// }
KeyGenerator func(*fiber.Ctx) string
// allows you to generate custom Expiration Key By Key, default is Expiration (Optional)
//
// Default: nil
ExpirationGenerator func(*fiber.Ctx, *Config) time.Duration
// Store is used to store the state of the middleware
//
// Default: an in memory store for this process only
Storage fiber.Storage
// Deprecated: Use Storage instead
Store fiber.Storage
// Deprecated: Use KeyGenerator instead
Key func(*fiber.Ctx) string
// allows you to store additional headers generated by next middlewares & handler
//
// Default: false
StoreResponseHeaders bool
// Max number of bytes of response bodies simultaneously stored in cache. When limit is reached,
// entries with the nearest expiration are deleted to make room for new.
// 0 means no limit
//
// Default: 0
MaxBytes uint
// You can specify HTTP methods to cache.
// The middleware just caches the routes of its methods in this slice.
//
// Default: []string{fiber.MethodGet, fiber.MethodHead}
Methods []string
}
// ConfigDefault is the default config
var ConfigDefault = Config{
Next: nil,
Expiration: 1 * time.Minute,
CacheHeader: "X-Cache",
CacheControl: false,
KeyGenerator: func(c *fiber.Ctx) string {
return utils.CopyString(c.Path())
},
ExpirationGenerator: nil,
StoreResponseHeaders: false,
Storage: nil,
MaxBytes: 0,
Methods: []string{fiber.MethodGet, fiber.MethodHead},
}
// Helper function to set default values
func configDefault(config ...Config) Config {
// Return default config if nothing provided
if len(config) < 1 {
return ConfigDefault
}
// Override default config
cfg := config[0]
// Set default values
if cfg.Store != nil {
log.Warn("[CACHE] Store is deprecated, please use Storage")
cfg.Storage = cfg.Store
}
if cfg.Key != nil {
log.Warn("[CACHE] Key is deprecated, please use KeyGenerator")
cfg.KeyGenerator = cfg.Key
}
if cfg.Next == nil {
cfg.Next = ConfigDefault.Next
}
if int(cfg.Expiration.Seconds()) == 0 {
cfg.Expiration = ConfigDefault.Expiration
}
if cfg.CacheHeader == "" {
cfg.CacheHeader = ConfigDefault.CacheHeader
}
if cfg.KeyGenerator == nil {
cfg.KeyGenerator = ConfigDefault.KeyGenerator
}
if len(cfg.Methods) == 0 {
cfg.Methods = ConfigDefault.Methods
}
return cfg
}

92
middleware/cache/heap.go vendored Normal file
View file

@ -0,0 +1,92 @@
package cache
import (
"container/heap"
)
type heapEntry struct {
key string
exp uint64
bytes uint
idx int
}
// indexedHeap is a regular min-heap that allows finding
// elements in constant time. It does so by handing out special indices
// and tracking entry movement.
//
// indexdedHeap is used for quickly finding entries with the lowest
// expiration timestamp and deleting arbitrary entries.
type indexedHeap struct {
// Slice the heap is built on
entries []heapEntry
// Mapping "index" to position in heap slice
indices []int
// Max index handed out
maxidx int
}
func (h indexedHeap) Len() int {
return len(h.entries)
}
func (h indexedHeap) Less(i, j int) bool {
return h.entries[i].exp < h.entries[j].exp
}
func (h indexedHeap) Swap(i, j int) {
h.entries[i], h.entries[j] = h.entries[j], h.entries[i]
h.indices[h.entries[i].idx] = i
h.indices[h.entries[j].idx] = j
}
func (h *indexedHeap) Push(x interface{}) {
h.pushInternal(x.(heapEntry)) //nolint:forcetypeassert // Forced type assertion required to implement the heap.Interface interface
}
func (h *indexedHeap) Pop() interface{} {
n := len(h.entries)
h.entries = h.entries[0 : n-1]
return h.entries[0:n][n-1]
}
func (h *indexedHeap) pushInternal(entry heapEntry) {
h.indices[entry.idx] = len(h.entries)
h.entries = append(h.entries, entry)
}
// Returns index to track entry
func (h *indexedHeap) put(key string, exp uint64, bytes uint) int {
idx := 0
if len(h.entries) < h.maxidx {
// Steal index from previously removed entry
// capacity > size is guaranteed
n := len(h.entries)
idx = h.entries[:n+1][n].idx
} else {
idx = h.maxidx
h.maxidx++
h.indices = append(h.indices, idx)
}
// Push manually to avoid allocation
h.pushInternal(heapEntry{
key: key, exp: exp, idx: idx, bytes: bytes,
})
heap.Fix(h, h.Len()-1)
return idx
}
func (h *indexedHeap) removeInternal(realIdx int) (string, uint) {
x := heap.Remove(h, realIdx).(heapEntry) //nolint:forcetypeassert,errcheck // Forced type assertion required to implement the heap.Interface interface
return x.key, x.bytes
}
// Remove entry by index
func (h *indexedHeap) remove(idx int) (string, uint) {
return h.removeInternal(h.indices[idx])
}
// Remove entry with lowest expiration time
func (h *indexedHeap) removeFirst() (string, uint) {
return h.removeInternal(0)
}

132
middleware/cache/manager.go vendored Normal file
View file

@ -0,0 +1,132 @@
package cache
import (
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/internal/memory"
)
// go:generate msgp
// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported
type item struct {
body []byte
ctype []byte
cencoding []byte
status int
exp uint64
headers map[string][]byte
// used for finding the item in an indexed heap
heapidx int
}
//msgp:ignore manager
type manager struct {
pool sync.Pool
memory *memory.Storage
storage fiber.Storage
}
func newManager(storage fiber.Storage) *manager {
// Create new storage handler
manager := &manager{
pool: sync.Pool{
New: func() interface{} {
return new(item)
},
},
}
if storage != nil {
// Use provided storage if provided
manager.storage = storage
} else {
// Fallback to memory storage
manager.memory = memory.New()
}
return manager
}
// acquire returns an *entry from the sync.Pool
func (m *manager) acquire() *item {
return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool
}
// release and reset *entry to sync.Pool
func (m *manager) release(e *item) {
// don't release item if we using memory storage
if m.storage != nil {
return
}
e.body = nil
e.ctype = nil
e.status = 0
e.exp = 0
e.headers = nil
m.pool.Put(e)
}
// get data from storage or memory
func (m *manager) get(key string) *item {
var it *item
if m.storage != nil {
it = m.acquire()
raw, err := m.storage.Get(key)
if err != nil {
return it
}
if raw != nil {
if _, err := it.UnmarshalMsg(raw); err != nil {
return it
}
}
return it
}
if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool
it = m.acquire()
return it
}
return it
}
// get raw data from storage or memory
func (m *manager) getRaw(key string) []byte {
var raw []byte
if m.storage != nil {
raw, _ = m.storage.Get(key) //nolint:errcheck // TODO: Handle error here
} else {
raw, _ = m.memory.Get(key).([]byte) //nolint:errcheck // TODO: Handle error here
}
return raw
}
// set data to storage or memory
func (m *manager) set(key string, it *item, exp time.Duration) {
if m.storage != nil {
if raw, err := it.MarshalMsg(nil); err == nil {
_ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here
}
// we can release data because it's serialized to database
m.release(it)
} else {
m.memory.Set(key, it, exp)
}
}
// set data to storage or memory
func (m *manager) setRaw(key string, raw []byte, exp time.Duration) {
if m.storage != nil {
_ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here
} else {
m.memory.Set(key, raw, exp)
}
}
// delete data from storage or memory
func (m *manager) del(key string) {
if m.storage != nil {
_ = m.storage.Delete(key) //nolint:errcheck // TODO: Handle error here
} else {
m.memory.Delete(key)
}
}

300
middleware/cache/manager_msgp.go vendored Normal file
View file

@ -0,0 +1,300 @@
package cache
// NOTE: THIS FILE WAS PRODUCED BY THE
// MSGP CODE GENERATION TOOL (github.com/tinylib/msgp)
// DO NOT EDIT
import (
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *item) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zbai uint32
zbai, err = dc.ReadMapHeader()
if err != nil {
return
}
for zbai > 0 {
zbai--
field, err = dc.ReadMapKeyPtr()
if err != nil {
return
}
switch msgp.UnsafeString(field) {
case "body":
z.body, err = dc.ReadBytes(z.body)
if err != nil {
return
}
case "ctype":
z.ctype, err = dc.ReadBytes(z.ctype)
if err != nil {
return
}
case "cencoding":
z.cencoding, err = dc.ReadBytes(z.cencoding)
if err != nil {
return
}
case "status":
z.status, err = dc.ReadInt()
if err != nil {
return
}
case "exp":
z.exp, err = dc.ReadUint64()
if err != nil {
return
}
case "headers":
var zcmr uint32
zcmr, err = dc.ReadMapHeader()
if err != nil {
return
}
if z.headers == nil && zcmr > 0 {
z.headers = make(map[string][]byte, zcmr)
} else if len(z.headers) > 0 {
for key := range z.headers {
delete(z.headers, key)
}
}
for zcmr > 0 {
zcmr--
var zxvk string
var zbzg []byte
zxvk, err = dc.ReadString()
if err != nil {
return
}
zbzg, err = dc.ReadBytes(zbzg)
if err != nil {
return
}
z.headers[zxvk] = zbzg
}
case "heapidx":
z.heapidx, err = dc.ReadInt()
if err != nil {
return
}
default:
err = dc.Skip()
if err != nil {
return
}
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *item) EncodeMsg(en *msgp.Writer) (err error) {
// map header, size 7
// write "body"
err = en.Append(0x87, 0xa4, 0x62, 0x6f, 0x64, 0x79)
if err != nil {
return err
}
err = en.WriteBytes(z.body)
if err != nil {
return
}
// write "ctype"
err = en.Append(0xa5, 0x63, 0x74, 0x79, 0x70, 0x65)
if err != nil {
return err
}
err = en.WriteBytes(z.ctype)
if err != nil {
return
}
// write "cencoding"
err = en.Append(0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67)
if err != nil {
return err
}
err = en.WriteBytes(z.cencoding)
if err != nil {
return
}
// write "status"
err = en.Append(0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73)
if err != nil {
return err
}
err = en.WriteInt(z.status)
if err != nil {
return
}
// write "exp"
err = en.Append(0xa3, 0x65, 0x78, 0x70)
if err != nil {
return err
}
err = en.WriteUint64(z.exp)
if err != nil {
return
}
// write "headers"
err = en.Append(0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73)
if err != nil {
return err
}
err = en.WriteMapHeader(uint32(len(z.headers)))
if err != nil {
return
}
for zxvk, zbzg := range z.headers {
err = en.WriteString(zxvk)
if err != nil {
return
}
err = en.WriteBytes(zbzg)
if err != nil {
return
}
}
// write "heapidx"
err = en.Append(0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78)
if err != nil {
return err
}
err = en.WriteInt(z.heapidx)
if err != nil {
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *item) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// map header, size 7
// string "body"
o = append(o, 0x87, 0xa4, 0x62, 0x6f, 0x64, 0x79)
o = msgp.AppendBytes(o, z.body)
// string "ctype"
o = append(o, 0xa5, 0x63, 0x74, 0x79, 0x70, 0x65)
o = msgp.AppendBytes(o, z.ctype)
// string "cencoding"
o = append(o, 0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67)
o = msgp.AppendBytes(o, z.cencoding)
// string "status"
o = append(o, 0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73)
o = msgp.AppendInt(o, z.status)
// string "exp"
o = append(o, 0xa3, 0x65, 0x78, 0x70)
o = msgp.AppendUint64(o, z.exp)
// string "headers"
o = append(o, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73)
o = msgp.AppendMapHeader(o, uint32(len(z.headers)))
for zxvk, zbzg := range z.headers {
o = msgp.AppendString(o, zxvk)
o = msgp.AppendBytes(o, zbzg)
}
// string "heapidx"
o = append(o, 0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78)
o = msgp.AppendInt(o, z.heapidx)
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zajw uint32
zajw, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
return
}
for zajw > 0 {
zajw--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
return
}
switch msgp.UnsafeString(field) {
case "body":
z.body, bts, err = msgp.ReadBytesBytes(bts, z.body)
if err != nil {
return
}
case "ctype":
z.ctype, bts, err = msgp.ReadBytesBytes(bts, z.ctype)
if err != nil {
return
}
case "cencoding":
z.cencoding, bts, err = msgp.ReadBytesBytes(bts, z.cencoding)
if err != nil {
return
}
case "status":
z.status, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
return
}
case "exp":
z.exp, bts, err = msgp.ReadUint64Bytes(bts)
if err != nil {
return
}
case "headers":
var zwht uint32
zwht, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
return
}
if z.headers == nil && zwht > 0 {
z.headers = make(map[string][]byte, zwht)
} else if len(z.headers) > 0 {
for key := range z.headers {
delete(z.headers, key)
}
}
for zwht > 0 {
var zxvk string
var zbzg []byte
zwht--
zxvk, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
return
}
zbzg, bts, err = msgp.ReadBytesBytes(bts, zbzg)
if err != nil {
return
}
z.headers[zxvk] = zbzg
}
case "heapidx":
z.heapidx, bts, err = msgp.ReadIntBytes(bts)
if err != nil {
return
}
default:
bts, err = msgp.Skip(bts)
if err != nil {
return
}
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *item) Msgsize() (s int) {
s = 1 + 5 + msgp.BytesPrefixSize + len(z.body) + 6 + msgp.BytesPrefixSize + len(z.ctype) + 10 + msgp.BytesPrefixSize + len(z.cencoding) + 7 + msgp.IntSize + 4 + msgp.Uint64Size + 8 + msgp.MapHeaderSize
if z.headers != nil {
for zxvk, zbzg := range z.headers {
_ = zbzg
s += msgp.StringPrefixSize + len(zxvk) + msgp.BytesPrefixSize + len(zbzg)
}
}
s += 8 + msgp.IntSize
return
}