From 50164f082c51f4a8d0428334ded47bdd019de646 Mon Sep 17 00:00:00 2001 From: speedwheel Date: Sat, 17 Mar 2018 06:15:13 +0200 Subject: [PATCH] new handlers for client (i.e browser) catching: cache.NoCache and cache.StaticCache including tests Former-commit-id: 18975297c8b96c7f9d5ff757f92051f6b10933c1 --- cache/browser.go | 98 +++++++++++++++++++++++++++++++++++++++++++ cache/browser_test.go | 76 +++++++++++++++++++++++++++++++++ cache/cache.go | 6 --- cache/cache_test.go | 5 ++- context/context.go | 29 ------------- iris.go | 34 ++++++++++++--- 6 files changed, 206 insertions(+), 42 deletions(-) create mode 100644 cache/browser.go create mode 100644 cache/browser_test.go diff --git a/cache/browser.go b/cache/browser.go new file mode 100644 index 00000000..89797a9b --- /dev/null +++ b/cache/browser.go @@ -0,0 +1,98 @@ +package cache + +import ( + "strconv" + "time" + + "github.com/kataras/iris/cache/client" + "github.com/kataras/iris/context" +) + +// CacheControlHeaderValue is the header value of the +// "Cache-Control": "private, no-cache, max-age=0, must-revalidate, no-store, proxy-revalidate, s-maxage=0". +// +// It can be overriden. +var CacheControlHeaderValue = "private, no-cache, max-age=0, must-revalidate, no-store, proxy-revalidate, s-maxage=0" + +const ( + // PragmaHeaderKey is the header key of "Pragma". + PragmaHeaderKey = "Pragma" + // PragmaNoCacheHeaderValue is the header value of "Pragma": "no-cache". + PragmaNoCacheHeaderValue = "no-cache" + // ExpiresHeaderKey is the header key of "Expires". + ExpiresHeaderKey = "Expires" + // ExpiresNeverHeaderValue is the header value of "ExpiresHeaderKey": "0". + ExpiresNeverHeaderValue = "0" +) + +// NoCache is a middleware which overrides the Cache-Control, Pragma and Expires headers +// in order to disable the cache during the browser's back and forward feature. +// +// A good use of this middleware is on HTML routes; to refresh the page even on "back" and "forward" browser's arrow buttons. +// +// See `cache#StaticCache` for the opposite behavior. +var NoCache = func(ctx context.Context) { + ctx.Header(context.CacheControlHeaderKey, CacheControlHeaderValue) + ctx.Header(PragmaHeaderKey, PragmaNoCacheHeaderValue) + ctx.Header(ExpiresHeaderKey, ExpiresNeverHeaderValue) + // Add the X-No-Cache header as well, for any customized case, i.e `cache#Handler` or `cache#Cache`. + client.NoCache(ctx) + + ctx.Next() +} + +// StaticCache middleware for caching static files by sending the "Cache-Control" and "Expires" headers to the client. +// It accepts a single input parameter, the "cacheDur", a time.Duration that it's used to calculate the expiration. +// +// If "cacheDur" <=0 then it returns the `NoCache` middleware instaed to disable the caching between browser's "back" and "forward" actions. +// +// Usage: `app.Use(cache.StaticCache(24 * time.Hour))` or `app.Use(cache.Staticcache(-1))`. +// A middleware, which is a simple Handler can be called inside another handler as well, example: +// cacheMiddleware := cache.StaticCache(...) +// func(ctx iris.Context){ +// cacheMiddleware(ctx) +// [...] +// } +var StaticCache = func(cacheDur time.Duration) context.Handler { + if int64(cacheDur) <= 0 { + return NoCache + } + + cacheControlHeaderValue := "public, max-age=" + strconv.Itoa(int(cacheDur.Seconds())) + return func(ctx context.Context) { + cacheUntil := time.Now().Add(cacheDur).Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) + ctx.Header(ExpiresHeaderKey, cacheUntil) + ctx.Header(context.CacheControlHeaderKey, cacheControlHeaderValue) + + ctx.Next() + } +} + +// Cache304 sends a `StatusNotModified` (304) whenever +// the "If-Modified-Since" request header (time) is before the +// time.Now() + expiresEvery (always compared to their UTC values). +// Use this `cache#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache +// for better performance. +// Clients that are compatible with the http RCF (all browsers are and tools like postman) +// will handle the caching. +// The only disadvantage of using that instead of server-side caching +// is that this method will send a 304 status code instead of 200, +// So, if you use it side by side with other micro services +// you have to check for that status code as well for a valid response. +// +// Developers are free to extend this method's behavior +// by watching system directories changes manually and use of the `ctx.WriteWithExpiration` +// with a "modtime" based on the file modified date, +// simillary to the `Party#StaticWeb` (which sends status OK(200) and browser disk caching instead of 304). +var Cache304 = func(expiresEvery time.Duration) context.Handler { + return func(ctx context.Context) { + now := time.Now() + if modified, err := ctx.CheckIfModifiedSince(now.Add(-expiresEvery)); !modified && err == nil { + ctx.WriteNotModified() + return + } + + ctx.SetLastModified(now) + ctx.Next() + } +} diff --git a/cache/browser_test.go b/cache/browser_test.go new file mode 100644 index 00000000..a3312575 --- /dev/null +++ b/cache/browser_test.go @@ -0,0 +1,76 @@ +package cache_test + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/cache" + "github.com/kataras/iris/context" + "github.com/kataras/iris/httptest" + "strconv" + "testing" + "time" +) + +func TestNoCache(t *testing.T) { + t.Parallel() + app := iris.New() + app.Get("/", cache.NoCache, func(ctx iris.Context) { + ctx.WriteString("no_cache") + }) + + // tests + e := httptest.New(t, app) + + r := e.GET("/").Expect().Status(httptest.StatusOK) + r.Body().Equal("no_cache") + r.Header(context.CacheControlHeaderKey).Equal(cache.CacheControlHeaderValue) + r.Header(cache.PragmaHeaderKey).Equal(cache.PragmaNoCacheHeaderValue) + r.Header(cache.ExpiresHeaderKey).Equal(cache.ExpiresNeverHeaderValue) +} + +func TestStaticCache(t *testing.T) { + t.Parallel() + // test change the time format, which is not reccomended but can be done. + app := iris.New().Configure(iris.WithTimeFormat("02 Jan 2006 15:04:05 GMT")) + + cacheDur := 30 * (24 * time.Hour) + var expectedTime time.Time + app.Get("/", cache.StaticCache(cacheDur), func(ctx iris.Context) { + expectedTime = time.Now() + ctx.WriteString("static_cache") + }) + + // tests + e := httptest.New(t, app) + r := e.GET("/").Expect().Status(httptest.StatusOK) + r.Body().Equal("static_cache") + + r.Header(cache.ExpiresHeaderKey).Equal(expectedTime.Add(cacheDur).Format(app.ConfigurationReadOnly().GetTimeFormat())) + cacheControlHeaderValue := "public, max-age=" + strconv.Itoa(int(cacheDur.Seconds())) + r.Header(context.CacheControlHeaderKey).Equal(cacheControlHeaderValue) +} + +func TestCache304(t *testing.T) { + t.Parallel() + app := iris.New() + + expiresEvery := 4 * time.Second + app.Get("/", cache.Cache304(expiresEvery), func(ctx iris.Context) { + ctx.WriteString("send") + }) + // handlers + e := httptest.New(t, app) + + // when 304, content type, content length and if ETagg is there are removed from the headers. + insideCacheTimef := time.Now().Add(-expiresEvery).UTC().Format(app.ConfigurationReadOnly().GetTimeFormat()) + r := e.GET("/").WithHeader(context.IfModifiedSinceHeaderKey, insideCacheTimef).Expect().Status(httptest.StatusNotModified) + r.Headers().NotContainsKey(context.ContentTypeHeaderKey).NotContainsKey(context.ContentLengthHeaderKey).NotContainsKey("ETag") + r.Body().Equal("") + + // continue to the handler itself. + cacheInvalidatedTimef := time.Now().Add(expiresEvery).UTC().Format(app.ConfigurationReadOnly().GetTimeFormat()) // after ~5seconds. + r = e.GET("/").WithHeader(context.LastModifiedHeaderKey, cacheInvalidatedTimef).Expect().Status(httptest.StatusOK) + r.Body().Equal("send") + // now without header, it should continue to the handler itself as well. + r = e.GET("/").Expect().Status(httptest.StatusOK) + r.Body().Equal("send") +} diff --git a/cache/cache.go b/cache/cache.go index c6d100da..645d6313 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -65,9 +65,3 @@ func Handler(expiration time.Duration) context.Handler { h := Cache(expiration).ServeHTTP return h } - -var ( - // NoCache disables the cache for a particular request, - // can be used as a middleware or called manually from the handler. - NoCache = client.NoCache -) diff --git a/cache/cache_test.go b/cache/cache_test.go index 8c92b08f..07f30311 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/kataras/iris/cache" + "github.com/kataras/iris/cache/client" "github.com/kataras/iris/cache/client/rule" "github.com/kataras/iris" @@ -84,7 +85,7 @@ func runTest(e *httpexpect.Expect, path string, counterPtr *uint32, expectedBody return nil } -func TestNoCache(t *testing.T) { +func TestClientNoCache(t *testing.T) { app := iris.New() var n uint32 @@ -94,7 +95,7 @@ func TestNoCache(t *testing.T) { }) app.Get("/nocache", cache.Handler(cacheDuration), func(ctx context.Context) { - cache.NoCache(ctx) // <---- + client.NoCache(ctx) // <---- atomic.AddUint32(&n, 1) ctx.Write([]byte(expectedBodyStr)) }) diff --git a/context/context.go b/context/context.go index fefbd9db..b2c846fe 100644 --- a/context/context.go +++ b/context/context.go @@ -980,35 +980,6 @@ var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) Handler { } } -// Cache304 sends a `StatusNotModified` (304) whenever -// the "If-Modified-Since" request header (time) is before the -// time.Now() + expiresEvery (always compared to their UTC values). -// Use this `context#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache -// for better performance. -// Clients that are compatible with the http RCF (all browsers are and tools like postman) -// will handle the caching. -// The only disadvantage of using that instead of server-side caching -// is that this method will send a 304 status code instead of 200, -// So, if you use it side by side with other micro services -// you have to check for that status code as well for a valid response. -// -// Developers are free to extend this method's behavior -// by watching system directories changes manually and use of the `ctx.WriteWithExpiration` -// with a "modtime" based on the file modified date, -// simillary to the `StaticWeb`(StaticWeb sends an OK(200) and browser disk caching instead of 304). -var Cache304 = func(expiresEvery time.Duration) Handler { - return func(ctx Context) { - now := time.Now() - if modified, err := ctx.CheckIfModifiedSince(now.Add(-expiresEvery)); !modified && err == nil { - ctx.WriteNotModified() - return - } - - ctx.SetLastModified(now) - ctx.Next() - } -} - // Gzip is a middleware which enables writing // using gzip compression, if client supports. var Gzip = func(ctx Context) { diff --git a/iris.go b/iris.go index 592f53ce..c2efb75a 100644 --- a/iris.go +++ b/iris.go @@ -375,14 +375,38 @@ var ( // Cache is a middleware providing server-side cache functionalities // to the next handlers, can be used as: `app.Get("/", iris.Cache, aboutHandler)`. // It should be used after Static methods. - // See `context#Cache304` for an alternative, faster way. + // See `iris#Cache304` for an alternative, faster way. // // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching Cache = cache.Handler + // NoCache is a middleware which overrides the Cache-Control, Pragma and Expires headers + // in order to disable the cache during the browser's back and forward feature. + // + // A good use of this middleware is on HTML routes; to refresh the page even on "back" and "forward" browser's arrow buttons. + // + // See `iris#StaticCache` for the opposite behavior. + // + // A shortcut of the `cache#NoCache` + NoCache = cache.NoCache + // StaticCache middleware for caching static files by sending the "Cache-Control" and "Expires" headers to the client. + // It accepts a single input parameter, the "cacheDur", a time.Duration that it's used to calculate the expiration. + // + // If "cacheDur" <=0 then it returns the `NoCache` middleware instaed to disable the caching between browser's "back" and "forward" actions. + // + // Usage: `app.Use(iris.StaticCache(24 * time.Hour))` or `app.Use(iris.Staticcache(-1))`. + // A middleware, which is a simple Handler can be called inside another handler as well, example: + // cacheMiddleware := iris.StaticCache(...) + // func(ctx iris.Context){ + // cacheMiddleware(ctx) + // [...] + // } + // + // A shortcut of the `cache#StaticCache` + StaticCache = cache.StaticCache // Cache304 sends a `StatusNotModified` (304) whenever // the "If-Modified-Since" request header (time) is before the // time.Now() + expiresEvery (always compared to their UTC values). - // Use this, which is a shortcut of the, `context#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache + // Use this, which is a shortcut of the, `chache#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache // for better performance. // Clients that are compatible with the http RCF (all browsers are and tools like postman) // will handle the caching. @@ -394,10 +418,10 @@ var ( // Developers are free to extend this method's behavior // by watching system directories changes manually and use of the `ctx.WriteWithExpiration` // with a "modtime" based on the file modified date, - // simillary to the `StaticWeb`(StaticWeb sends an OK(200) and browser disk caching instead of 304). + // simillary to the `StaticWeb`(which sends status OK(200) and browser disk caching instead of 304). // - // A shortcut of the `context#Cache304`. - Cache304 = context.Cache304 + // A shortcut of the `cache#Cache304`. + Cache304 = cache.Cache304 ) // SPA accepts an "assetHandler" which can be the result of an