Skip to content

Commit

Permalink
✨ Cache middleware: Store e2e headers. (#1807)
Browse files Browse the repository at this point in the history
* ✨ Cache middleware: Store e2e headers.

As defined in RFC2616 - section-13.5.1, shared caches MUST
store end-to-end headers from backend response and MUST be
transmitted in any response formed from a cache entry.

This commit ensures a stronger consistency between responses
served from the handlers & from the cache middleware.

* ✨ Cache middleware: Add flag for e2e headers.

Set flag to prevent e2e headers caching to
be the default behavior of the cache middleware.
This would otherwise change quite a lot the
experience for cache middleware current users.

* ✨ Cache middleware: Add Benchmark for additionalHeaders feature.

* ✨ Cache middleware: Rename E2Eheaders into StoreResponseHeaders.

E2E is an acronym commonly associated with test.
While in the present case it refers to end-to-end
HTTP headers (by opposition to hop-by-hop), this
still remains confusing. This commits renames it
to a more generic name.

* ✨ Cache middleware: Update README

* ✨ Cache middleware: Move map instanciation.

This will prevent an extra memory allocation for users
not interested in this feature.

* ✨ Cache middleware: Prevent memory allocation when StoreResponseHeaders is disabled.

* ✨ Cache middleware: Store e2e headers. #1807
- use set instead of add for the headers
- copy value from the headers -> prevent problems with mutable values

Co-authored-by: wernerr <rene@gofiber.io>
  • Loading branch information
thylong and ReneWerner87 committed Mar 8, 2022
1 parent 0120531 commit 1cddc56
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 7 deletions.
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,
}
```
33 changes: 33 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,11 @@ func New(config ...Config) fiber.Handler {
if len(e.cencoding) > 0 {
c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding)
}
if e.headers != nil {
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)
Expand Down Expand Up @@ -134,6 +152,21 @@ func New(config ...Config) fiber.Handler {
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 []byte, value []byte) {
// create real copy
keyS := string(key)
if _, ok := ignoreHeaders[keyS]; !ok {
e.headers[keyS] = utils.CopyBytes(value)
}
},
)
}

// default cache expiration
expiration := uint64(cfg.Expiration.Seconds())
// Calculate expiration by response header or other setting
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) {
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.

0 comments on commit 1cddc56

Please sign in to comment.