Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Cache middleware: Store e2e headers. #1807

Merged
7 changes: 7 additions & 0 deletions middleware/cache/README.md
Expand Up @@ -10,6 +10,7 @@ Cache middleware for [Fiber](https://github.com/gofiber/fiber) designed to inter
- [Examples](#examples)
- [Default Config](#default-config)
- [Custom Config](#custom-config)
- [Custom Cache Key Or Expiration](#custom-cache-key-or-expiration)
- [Config](#config)
- [Default Config](#default-config-1)

Expand Down Expand Up @@ -112,6 +113,11 @@ type Config struct {
//
// Default: an in memory store for this process only
Storage fiber.Storage

// allows you to store additional headers generated by next middlewares & handler
//
// Default: false
StoreResponseHeaders bool
}
```

Expand All @@ -128,6 +134,7 @@ var ConfigDefault = Config{
return utils.CopyString(c.Path())
},
ExpirationGenerator : nil,
StoreResponseHeaders: false,
Storage: nil,
}
```
30 changes: 30 additions & 0 deletions middleware/cache/cache.go
Expand Up @@ -27,6 +27,19 @@ const (
cacheMiss = "miss"
)

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 explicitely by the cache manager
"Content-Encoding": nil, // already stored explicitely by the cache manager
}

// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
// Set default config
Expand Down Expand Up @@ -96,6 +109,9 @@ func New(config ...Config) fiber.Handler {
if len(e.cencoding) > 0 {
c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding)
}
for k, v := range e.headers {
thylong marked this conversation as resolved.
Show resolved Hide resolved
c.Response().Header.AddBytesV(k, v)
}
// Set Cache-Control header if enabled
if cfg.CacheControl {
maxAge := strconv.FormatUint(e.exp-ts, 10)
Expand Down Expand Up @@ -133,6 +149,20 @@ func New(config ...Config) fiber.Handler {
e.status = c.Response().StatusCode()
e.ctype = utils.CopyBytes(c.Response().Header.ContentType())
e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding))
e.headers = make(map[string][]byte)
thylong marked this conversation as resolved.
Show resolved Hide resolved

// Store all response headers
// (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1)
if cfg.StoreResponseHeaders {
c.Response().Header.VisitAll(
func(key []byte, value []byte) {
keyS := string(key)
if _, isHopbyHop := ignoreHeaders[keyS]; !isHopbyHop {
e.headers[keyS] = value
}
},
)
}

// default cache expiration
expiration := uint64(cfg.Expiration.Seconds())
Expand Down
50 changes: 50 additions & 0 deletions middleware/cache/cache_test.go
Expand Up @@ -302,6 +302,28 @@ func Test_CustomExpiration(t *testing.T) {
utils.AssertEqual(t, 6000, newCacheTime)
}

func Test_AdditionalE2EResponseHeaders(t *testing.T) {
thylong marked this conversation as resolved.
Show resolved Hide resolved
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("GET", "/", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "foobar", resp.Header.Get("X-Foobar"))

req = httptest.NewRequest("GET", "/", 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) {
app := fiber.New()

Expand Down Expand Up @@ -475,3 +497,31 @@ func Benchmark_Cache_Storage(b *testing.B) {
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("GET")
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"))
}
10 changes: 8 additions & 2 deletions middleware/cache/config.go
Expand Up @@ -54,6 +54,11 @@ type Config struct {

// Deprecated, use KeyGenerator instead
Key func(*fiber.Ctx) string

// allows you to store additional headers generated by next middlewares & handler
//
// Default: false
StoreResponseHeaders bool
}

// ConfigDefault is the default config
Expand All @@ -65,8 +70,9 @@ var ConfigDefault = Config{
KeyGenerator: func(c *fiber.Ctx) string {
return utils.CopyString(c.Path())
},
ExpirationGenerator: nil,
Storage: nil,
ExpirationGenerator: nil,
StoreResponseHeaders: false,
Storage: nil,
}

// Helper function to set default values
Expand Down
2 changes: 2 additions & 0 deletions middleware/cache/manager.go
Expand Up @@ -18,6 +18,7 @@ type item struct {
cencoding []byte
status int
exp uint64
headers map[string][]byte
}

//msgp:ignore manager
Expand Down Expand Up @@ -61,6 +62,7 @@ func (m *manager) release(e *item) {
e.ctype = nil
e.status = 0
e.exp = 0
e.headers = nil
m.pool.Put(e)
}

Expand Down
105 changes: 100 additions & 5 deletions middleware/cache/manager_msgp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.