From bc45e6444c2743788419bc2ece29ec02dfd637a4 Mon Sep 17 00:00:00 2001 From: Gerasimos Maropoulos Date: Sat, 17 Mar 2018 02:27:25 +0200 Subject: [PATCH 1/4] Cleanup of some old code Remove the StaticCacheDuration unused package-level variable from context/context.go and /core/router/api_builder.go it should be removed a year ago, now all the cache funcs and middlewares accept their own options | Remove duplicated header keys that are used on both context/context.go and core/router/fs.go by exporting context's one. Former-commit-id: 2a5c840d26b32144d2b5b3173a1e4bb4324cecba --- .travis.yml | 5 +++ context/context.go | 66 ++++++++++++++++----------------- context/gzip_response_writer.go | 12 +++--- context/transaction.go | 2 +- core/router/api_builder.go | 17 --------- core/router/fs.go | 4 +- 6 files changed, 47 insertions(+), 59 deletions(-) diff --git a/.travis.yml b/.travis.yml index b928da0d..427787be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,9 @@ after_script: # typescript examples - cd ./typescript/_examples - go get ./... + - go test -v -cover ./... + - cd ../../ + # make sure that the _benchmarks code is working + - cd ./_benchmarks + - go get ./... - go test -v -cover ./... \ No newline at end of file diff --git a/context/context.go b/context/context.go index ef1f4651..fefbd9db 100644 --- a/context/context.go +++ b/context/context.go @@ -1602,8 +1602,6 @@ func (ctx *context) Header(name string, value string) { ctx.writer.Header().Add(name, value) } -const contentTypeHeaderKey = "Content-Type" - // ContentType sets the response writer's header key "Content-Type" to the 'cType'. func (ctx *context) ContentType(cType string) { if cType == "" { @@ -1623,13 +1621,13 @@ func (ctx *context) ContentType(cType string) { } } - ctx.writer.Header().Set(contentTypeHeaderKey, cType) + ctx.writer.Header().Set(ContentTypeHeaderKey, cType) } // GetContentType returns the response writer's header value of "Content-Type" // which may, setted before with the 'ContentType'. func (ctx *context) GetContentType() string { - return ctx.writer.Header().Get(contentTypeHeaderKey) + return ctx.writer.Header().Get(ContentTypeHeaderKey) } // StatusCode sets the status code header to the response. @@ -2198,19 +2196,29 @@ func (ctx *context) WriteString(body string) (n int, err error) { return ctx.writer.WriteString(body) } -var ( - // StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration - // which can be changed. - StaticCacheDuration = 20 * time.Second +const ( + // ContentTypeHeaderKey is the header key of "Content-Type". + ContentTypeHeaderKey = "Content-Type" - lastModifiedHeaderKey = "Last-Modified" - ifModifiedSinceHeaderKey = "If-Modified-Since" - contentDispositionHeaderKey = "Content-Disposition" - cacheControlHeaderKey = "Cache-Control" - contentEncodingHeaderKey = "Content-Encoding" - gzipHeaderValue = "gzip" - acceptEncodingHeaderKey = "Accept-Encoding" - varyHeaderKey = "Vary" + // LastModifiedHeaderKey is the header key of "Last-Modified". + LastModifiedHeaderKey = "Last-Modified" + // IfModifiedSinceHeaderKey is the header key of "If-Modified-Since". + IfModifiedSinceHeaderKey = "If-Modified-Since" + // CacheControlHeaderKey is the header key of "Cache-Control". + CacheControlHeaderKey = "Cache-Control" + + // ContentDispositionHeaderKey is the header key of "Content-Disposition". + ContentDispositionHeaderKey = "Content-Disposition" + // ContentLengthHeaderKey is the header key of "Content-Length" + ContentLengthHeaderKey = "Content-Length" + // ContentEncodingHeaderKey is the header key of "Content-Encoding". + ContentEncodingHeaderKey = "Content-Encoding" + // GzipHeaderValue is the header value of "gzip". + GzipHeaderValue = "gzip" + // AcceptEncodingHeaderKey is the header key of "Accept-Encoding". + AcceptEncodingHeaderKey = "Accept-Encoding" + // VaryHeaderKey is the header key of "Vary". + VaryHeaderKey = "Vary" ) var unixEpochTime = time.Unix(0, 0) @@ -2251,7 +2259,7 @@ var FormatTime = func(ctx Context, t time.Time) string { // It's mostly internally on core/router and context packages. func (ctx *context) SetLastModified(modtime time.Time) { if !IsZeroTime(modtime) { - ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()? + ctx.Header(LastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()? } } @@ -2273,7 +2281,7 @@ func (ctx *context) CheckIfModifiedSince(modtime time.Time) (bool, error) { if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead { return false, errors.New("skip: method") } - ims := ctx.GetHeader(ifModifiedSinceHeaderKey) + ims := ctx.GetHeader(IfModifiedSinceHeaderKey) if ims == "" || IsZeroTime(modtime) { return false, errors.New("skip: zero time") } @@ -2301,10 +2309,10 @@ func (ctx *context) WriteNotModified() { // guiding cache updates (e.g.," Last-Modified" might be useful if the // response does not have an ETag field). h := ctx.ResponseWriter().Header() - delete(h, contentTypeHeaderKey) - delete(h, contentLengthHeaderKey) + delete(h, ContentTypeHeaderKey) + delete(h, ContentLengthHeaderKey) if h.Get("Etag") != "" { - delete(h, lastModifiedHeaderKey) + delete(h, LastModifiedHeaderKey) } ctx.StatusCode(http.StatusNotModified) } @@ -2359,9 +2367,9 @@ func (ctx *context) StreamWriter(writer func(w io.Writer) bool) { // ClientSupportsGzip retruns true if the client supports gzip compression. func (ctx *context) ClientSupportsGzip() bool { - if h := ctx.GetHeader(acceptEncodingHeaderKey); h != "" { + if h := ctx.GetHeader(AcceptEncodingHeaderKey); h != "" { for _, v := range strings.Split(h, ";") { - if strings.Contains(v, gzipHeaderValue) { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){ + if strings.Contains(v, GzipHeaderValue) { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){ return true } } @@ -2896,11 +2904,6 @@ var ( errServeContent = errors.New("while trying to serve content to the client. Trace %s") ) -const ( - // contentLengthHeaderKey represents the header["Content-Length"] - contentLengthHeaderKey = "Content-Length" -) - // ServeContent serves content, headers are autoset // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) // @@ -2918,9 +2921,6 @@ func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime if gzipCompression && ctx.ClientSupportsGzip() { AddGzipHeaders(ctx.writer) - // ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) - // ctx.Header(contentEncodingHeaderKey,gzipHeaderValue) - gzipWriter := acquireGzipWriter(ctx.writer) defer releaseGzipWriter(gzipWriter) out = gzipWriter @@ -2958,7 +2958,7 @@ func (ctx *context) ServeFile(filename string, gzipCompression bool) error { // // Use this instead of ServeFile to 'force-download' bigger files to the client. func (ctx *context) SendFile(filename string, destinationName string) error { - ctx.writer.Header().Set(contentDispositionHeaderKey, "attachment;filename="+destinationName) + ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destinationName) return ctx.ServeFile(filename, false) } @@ -3032,7 +3032,7 @@ var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`) // seconds as int64 // if header not found or parse failed then it returns -1. func (ctx *context) MaxAge() int64 { - header := ctx.GetHeader(cacheControlHeaderKey) + header := ctx.GetHeader(CacheControlHeaderKey) if header == "" { return -1 } diff --git a/context/gzip_response_writer.go b/context/gzip_response_writer.go index c4f79586..a4379d9d 100644 --- a/context/gzip_response_writer.go +++ b/context/gzip_response_writer.go @@ -117,8 +117,8 @@ func (w *GzipResponseWriter) Write(contents []byte) (int, error) { func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err error) { n, err = fmt.Fprintf(w, format, a...) if err == nil { - if w.ResponseWriter.Header()[contentTypeHeaderKey] == nil { - w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue) + if w.ResponseWriter.Header()[ContentTypeHeaderKey] == nil { + w.ResponseWriter.Header().Set(ContentTypeHeaderKey, ContentTextHeaderValue) } } @@ -130,8 +130,8 @@ func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err func (w *GzipResponseWriter) WriteString(s string) (n int, err error) { n, err = w.Write([]byte(s)) if err == nil { - if w.ResponseWriter.Header()[contentTypeHeaderKey] == nil { - w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue) + if w.ResponseWriter.Header()[ContentTypeHeaderKey] == nil { + w.ResponseWriter.Header().Set(ContentTypeHeaderKey, ContentTextHeaderValue) } } @@ -180,8 +180,8 @@ func (w *GzipResponseWriter) WriteNow(contents []byte) (int, error) { // AddGzipHeaders just adds the headers "Vary" to "Accept-Encoding" // and "Content-Encoding" to "gzip". func AddGzipHeaders(w ResponseWriter) { - w.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) - w.Header().Add(contentEncodingHeaderKey, gzipHeaderValue) + w.Header().Add(VaryHeaderKey, AcceptEncodingHeaderKey) + w.Header().Add(ContentEncodingHeaderKey, GzipHeaderValue) } // FlushResponse validates the response headers in order to be compatible with the gzip written data diff --git a/context/transaction.go b/context/transaction.go index e0f580ac..fb0916b7 100644 --- a/context/transaction.go +++ b/context/transaction.go @@ -114,7 +114,7 @@ func (t *Transaction) Complete(err error) { reason = errWstatus.Reason } // get the content type used on this transaction - if cTypeH := t.context.ResponseWriter().Header().Get(contentTypeHeaderKey); cTypeH != "" { + if cTypeH := t.context.ResponseWriter().Header().Get(ContentTypeHeaderKey); cTypeH != "" { cType = cTypeH } diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 1b176ca5..1a21656f 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -549,23 +549,6 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro return } -// StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration -// which can be changed. -var StaticCacheDuration = 20 * time.Second - -const ( - lastModifiedHeaderKey = "Last-Modified" - ifModifiedSinceHeaderKey = "If-Modified-Since" - contentDispositionHeaderKey = "Content-Disposition" - cacheControlHeaderKey = "Cache-Control" - contentEncodingHeaderKey = "Content-Encoding" - acceptEncodingHeaderKey = "Accept-Encoding" - // contentLengthHeaderKey represents the header["Content-Length"] - contentLengthHeaderKey = "Content-Length" - contentTypeHeaderKey = "Content-Type" - varyHeaderKey = "Vary" -) - func (api *APIBuilder) registerResourceRoute(reqPath string, h context.Handler) *Route { api.Head(reqPath, h) return api.Get(reqPath, h) diff --git a/core/router/fs.go b/core/router/fs.go index 65a41332..690ab6a8 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -517,8 +517,8 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc }() } ctx.Header("Accept-Ranges", "bytes") - if ctx.ResponseWriter().Header().Get(contentEncodingHeaderKey) == "" { - ctx.Header(contentLengthHeaderKey, strconv.FormatInt(sendSize, 10)) + if ctx.ResponseWriter().Header().Get(context.ContentEncodingHeaderKey) == "" { + ctx.Header(context.ContentLengthHeaderKey, strconv.FormatInt(sendSize, 10)) } } From 50164f082c51f4a8d0428334ded47bdd019de646 Mon Sep 17 00:00:00 2001 From: speedwheel Date: Sat, 17 Mar 2018 06:15:13 +0200 Subject: [PATCH 2/4] 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 From 46453ff522ad109df4a984b39e2d43f763310fc7 Mon Sep 17 00:00:00 2001 From: speedwheel Date: Sat, 17 Mar 2018 06:19:49 +0200 Subject: [PATCH 3/4] fix import path order, guidelines of iris Former-commit-id: 4c938dd13691b8f753a7d1b00a6fa2da56ad4e99 --- cache/browser_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cache/browser_test.go b/cache/browser_test.go index a3312575..41993331 100644 --- a/cache/browser_test.go +++ b/cache/browser_test.go @@ -1,13 +1,15 @@ 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" + + "github.com/kataras/iris/cache" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/httptest" ) func TestNoCache(t *testing.T) { From 8e9deec4abdcacf2f390d2e6d52665cc92f94f97 Mon Sep 17 00:00:00 2001 From: Gerasimos Maropoulos Date: Sun, 18 Mar 2018 11:55:05 +0200 Subject: [PATCH 4/4] Add one more browser (and 304 server) cache method using ETag and If-None-Match headers And replace the 'ctx.WriteWithExpiration' with simple 'ctx.Write' at 'StaticEmbeddedHandler' of core/router/fs.go, now that we have plenty of options for client cache give the end-dev the oportunity to use them or not on static embedded handlers Former-commit-id: 9c9e2f3de3c5ad8c9e14e453b67e6b649b02bde8 --- cache/browser.go | 43 ++++++++++++++++++++++++++++++++++++++++++- cache/browser_test.go | 25 +++++++++++++++++++++++++ context/context.go | 4 +++- core/router/fs.go | 4 ++-- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/cache/browser.go b/cache/browser.go index 89797a9b..6058dc83 100644 --- a/cache/browser.go +++ b/cache/browser.go @@ -68,6 +68,46 @@ var StaticCache = func(cacheDur time.Duration) context.Handler { } } +const ifNoneMatchHeaderKey = "If-None-Match" + +// ETag is another browser & server cache request-response feature. +// It can be used side by side with the `StaticCache`, usually `StaticCache` middleware should go first. +// This should be used on routes that serves static files only. +// The key of the `ETag` is the `ctx.Request().URL.Path`, invalidation of the not modified cache method +// can be made by other request handler as well. +// +// In typical usage, when a URL is retrieved, the web server will return the resource's current +// representation along with its corresponding ETag value, +// which is placed in an HTTP response header "ETag" field: +// +// ETag: "/mypath" +// +// The client may then decide to cache the representation, along with its ETag. +// Later, if the client wants to retrieve the same URL resource again, +// it will first determine whether the local cached version of the URL has expired +// (through the Cache-Control (`StaticCache` method) and the Expire headers). +// If the URL has not expired, it will retrieve the local cached resource. +// If it determined that the URL has expired (is stale), then the client will contact the server +// and send its previously saved copy of the ETag along with the request in a "If-None-Match" field. +// +// Usage with combination of `StaticCache`: +// assets := app.Party("/assets", cache.StaticCache(24 * time.Hour), ETag) +// assets.StaticWeb("/", "./assets") or StaticEmbedded("/", "./assets") or StaticEmbeddedGzip("/", "./assets"). +// +// Similar to `Cache304` but it doesn't depends on any "modified date", it uses just the ETag and If-None-Match headers. +// +// Read more at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching and +// https://en.wikipedia.org/wiki/HTTP_ETag +var ETag = func(ctx context.Context) { + key := ctx.Request().URL.Path + ctx.Header(context.ETagHeaderKey, key) + if match := ctx.GetHeader(ifNoneMatchHeaderKey); match == key { + ctx.WriteNotModified() + return + } + 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). @@ -83,7 +123,8 @@ var StaticCache = func(cacheDur time.Duration) context.Handler { // 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). +// can be used on Party's that contains a static handler, +// i.e `StaticWeb`, `StaticEmbedded` or even `StaticEmbeddedGzip`. var Cache304 = func(expiresEvery time.Duration) context.Handler { return func(ctx context.Context) { now := time.Now() diff --git a/cache/browser_test.go b/cache/browser_test.go index 41993331..9c2867ba 100644 --- a/cache/browser_test.go +++ b/cache/browser_test.go @@ -76,3 +76,28 @@ func TestCache304(t *testing.T) { r = e.GET("/").Expect().Status(httptest.StatusOK) r.Body().Equal("send") } +func TestETag(t *testing.T) { + t.Parallel() + + app := iris.New() + n := "_" + app.Get("/", cache.ETag, func(ctx iris.Context) { + ctx.WriteString(n) + n += "_" + }) + + // the first and last test writes the content with status OK without cache, + // the rest tests the cache headers and status 304 and return, so body should be "". + e := httptest.New(t, app) + + r := e.GET("/").Expect().Status(httptest.StatusOK) + r.Header("ETag").Equal("/") // test if header setted. + r.Body().Equal("_") + + e.GET("/").WithHeader("ETag", "/").WithHeader("If-None-Match", "/").Expect(). + Status(httptest.StatusNotModified).Body().Equal("") // browser is responsible, no the test engine. + + r = e.GET("/").Expect().Status(httptest.StatusOK) + r.Header("ETag").Equal("/") // test if header setted. + r.Body().Equal("__") +} diff --git a/context/context.go b/context/context.go index b2c846fe..d4b47f9c 100644 --- a/context/context.go +++ b/context/context.go @@ -2177,6 +2177,8 @@ const ( IfModifiedSinceHeaderKey = "If-Modified-Since" // CacheControlHeaderKey is the header key of "Cache-Control". CacheControlHeaderKey = "Cache-Control" + // ETagHeaderKey is the header key of "ETag". + ETagHeaderKey = "ETag" // ContentDispositionHeaderKey is the header key of "Content-Disposition". ContentDispositionHeaderKey = "Content-Disposition" @@ -2282,7 +2284,7 @@ func (ctx *context) WriteNotModified() { h := ctx.ResponseWriter().Header() delete(h, ContentTypeHeaderKey) delete(h, ContentLengthHeaderKey) - if h.Get("Etag") != "" { + if h.Get(ETagHeaderKey) != "" { delete(h, LastModifiedHeaderKey) } ctx.StatusCode(http.StatusNotModified) diff --git a/core/router/fs.go b/core/router/fs.go index 690ab6a8..8aa5728e 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -68,7 +68,7 @@ func StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error names = append(names, path) } - modtime := time.Now() + // modtime := time.Now() h := func(ctx context.Context) { reqPath := strings.TrimPrefix(ctx.Request().URL.Path, "/"+vdir) @@ -100,7 +100,7 @@ func StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error } ctx.ContentType(cType) - if _, err := ctx.WriteWithExpiration(buf, modtime); err != nil { + if _, err := ctx.Write(buf); err != nil { ctx.StatusCode(http.StatusInternalServerError) ctx.StopExecution() }