Skip to content

Commit

Permalink
✨ Cache middleware: Store e2e headers.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
thylong committed Mar 5, 2022
1 parent df85b62 commit aaa2238
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 10 deletions.
28 changes: 28 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.e2eHeaders {
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,18 @@ 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.e2eHeaders = make(map[string][]byte)

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

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

func Test_AdditionalE2EResponseHeaders(t *testing.T) {
app := fiber.New()
app.Use(New())

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
12 changes: 7 additions & 5 deletions middleware/cache/manager.go
Expand Up @@ -13,11 +13,12 @@ import (
// don't forget to replace the msgp import path to:
// "github.com/gofiber/fiber/v2/internal/msgp"
type item struct {
body []byte
ctype []byte
cencoding []byte
status int
exp uint64
body []byte
ctype []byte
cencoding []byte
status int
exp uint64
e2eHeaders 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.e2eHeaders = 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 aaa2238

Please sign in to comment.