Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ func main() {
os.Exit(1)
}

examplePathRegex := regexp.MustCompile(`^/api/v1/.*`)

cacheClient, err := cache.NewClient(
cache.ClientWithAdapter(memcached),
cache.ClientWithTTL(10 * time.Minute),
cache.ClientWithRefreshKey("opn"),
cache.ClientWithSkipCacheResponseHeader("x-skip-example"),
cache.ClientWithSkipCacheUriPathRegex(examplePathRegex)
)
if err != nil {
fmt.Println(err)
Expand Down Expand Up @@ -73,15 +77,44 @@ import (
"server": ":6379",
},
}

examplePathRegex := regexp.MustCompile(`^/api/v1/.*`)

cacheClient, err := cache.NewClient(
cache.ClientWithAdapter(redis.NewAdapter(ringOpt)),
cache.ClientWithTTL(10 * time.Minute),
cache.ClientWithRefreshKey("opn"),
cache.ClientWithSkipCacheResponseHeader("x-skip-example"),
cache.ClientWithSkipCacheUriPathRegex(examplePathRegex)
)

...
```

Example of handler func skipping cache using response header
```go
...
cacheClient, err := cache.NewClient(
cache.ClientWithAdapter(memcached),
cache.ClientWithTTL(10 * time.Minute),
cache.ClientWithSkipCacheResponseHeader("x-skip-example"),
)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

example := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Skip-Example", "1")
w.Write([]byte(fmt.Sprintf("response value at %s", time.Now().UTC().String())))
}

handler := http.HandlerFunc(example)
...
```

```

## Benchmarks
The benchmarks were based on [allegro/bigache](https://github.com/allegro/bigcache) tests and used to compare it with the http-cache memory adapter.<br>
The tests were run using an Intel i5-2410M with 8GB RAM on Arch Linux 64bits.<br>
Expand Down
2 changes: 1 addition & 1 deletion adapter/redis/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
"time"

"github.com/victorspringer/http-cache"
cache "github.com/victorspringer/http-cache"
)

var a cache.Adapter
Expand Down
86 changes: 67 additions & 19 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -62,11 +63,13 @@ type Response struct {

// Client data structure for HTTP cache middleware.
type Client struct {
adapter Adapter
ttl time.Duration
refreshKey string
methods []string
writeExpiresHeader bool
adapter Adapter
ttl time.Duration
refreshKey string
skipCacheResponseHeader string
skipCacheUriPathRegex *regexp.Regexp
methods []string
writeExpiresHeader bool
}

// ClientOption is used to set Client settings.
Expand All @@ -88,7 +91,8 @@ type Adapter interface {
// Middleware is the HTTP cache middleware handler.
func (c *Client) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c.cacheableMethod(r.Method) {

if c.cacheableUriPath(r.URL) && c.cacheableMethod(r.Method) {
sortURLParams(r.URL)
key := generateKey(r.URL.String())
if r.Method == http.MethodPost && r.Body != nil {
Expand Down Expand Up @@ -138,27 +142,36 @@ func (c *Client) Middleware(next http.Handler) http.Handler {
rec := httptest.NewRecorder()
next.ServeHTTP(rec, r)
result := rec.Result()
headers := result.Header

statusCode := result.StatusCode
value := rec.Body.Bytes()
now := time.Now()
expires := now.Add(c.ttl)
if statusCode < 400 {
response := Response{
Value: value,
Header: result.Header,
Expiration: expires,
LastAccess: now,
Frequency: 1,

skipCachingResponse := headers.Get(c.skipCacheResponseHeader) != ""

if !skipCachingResponse {

now := time.Now()
expires := now.Add(c.ttl)
if statusCode < 400 {
response := Response{
Value: value,
Header: result.Header,
Expiration: expires,
LastAccess: now,
Frequency: 1,
}
c.adapter.Set(key, response.Bytes(), response.Expiration)
}
if c.writeExpiresHeader {
w.Header().Set("Expires", expires.UTC().Format(http.TimeFormat))
}
c.adapter.Set(key, response.Bytes(), response.Expiration)

}

for k, v := range result.Header {
w.Header().Set(k, strings.Join(v, ","))
}
if c.writeExpiresHeader {
w.Header().Set("Expires", expires.UTC().Format(http.TimeFormat))
}
w.WriteHeader(statusCode)
w.Write(value)
return
Expand All @@ -176,6 +189,20 @@ func (c *Client) cacheableMethod(method string) bool {
return false
}

// cacheableUriPath takes the request url and see if it
// matches regex used for skipping cache based on request
// path
func (c *Client) cacheableUriPath(requestUrl *url.URL) bool {

if c.skipCacheUriPathRegex == nil {
return true
}

foundMatchingUriPath := c.skipCacheUriPathRegex.FindString(requestUrl.Path)

return foundMatchingUriPath == ""
}

// BytesToResponse converts bytes array into Response data structure.
func BytesToResponse(b []byte) Response {
var r Response
Expand Down Expand Up @@ -279,6 +306,27 @@ func ClientWithRefreshKey(refreshKey string) ClientOption {
}
}

// ClientWithSkipCacheResponseHeader sets the name of the response header
// that will be used to ensure a response does not get cached.
// Optional setting.
func ClientWithSkipCacheResponseHeader(headerName string) ClientOption {
return func(c *Client) error {
c.skipCacheResponseHeader = headerName
return nil
}
}

// ClientWithSkipCacheUriPathRegex sets the regex that will be
// used to ensure that both request/response of matching path
// is free of cache.
// Optional setting.
func ClientWithSkipCacheUriPathRegex(uriPathRegex *regexp.Regexp) ClientOption {
return func(c *Client) error {
c.skipCacheUriPathRegex = uriPathRegex
return nil
}
}

// ClientWithMethods sets the acceptable HTTP methods to be cached.
// Optional setting. If not set, default is "GET".
func ClientWithMethods(methods []string) ClientOption {
Expand Down
Loading