From befb1f0c08c3d290a5df2c8d2ef4a12ad870064b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 15:09:24 +0200 Subject: [PATCH 1/3] some major improvements to the (server-side) cache middleware and an example of a client-side responsibility cache Former-commit-id: 93d3a7a6f163c6d49f315f86d10e63f7b1b1d93a --- _examples/cache/client-side/main.go | 27 +++++++ cache/client/handler.go | 21 ++++-- cache/client/response_recorder.go | 109 ---------------------------- cache/client/utils.go | 20 ----- cache/entry/entry.go | 31 +++++++- cache/entry/response.go | 31 +++++--- core/router/fs.go | 2 +- 7 files changed, 90 insertions(+), 151 deletions(-) create mode 100644 _examples/cache/client-side/main.go delete mode 100644 cache/client/response_recorder.go delete mode 100644 cache/client/utils.go diff --git a/_examples/cache/client-side/main.go b/_examples/cache/client-side/main.go new file mode 100644 index 00000000..02b054f3 --- /dev/null +++ b/_examples/cache/client-side/main.go @@ -0,0 +1,27 @@ +// Package main shows how you can use the `WriteWithExpiration` +// based on the "modtime", if it's newer than the request header then +// it will refresh the contents, otherwise will let the client (99.9% the browser) +// to handle the cache mechanism, it's faster than iris.Cache because server-side +// has nothing to do and no need to store the responses in the memory. +package main + +import ( + "fmt" + "time" + + "github.com/kataras/iris" +) + +var modtime = time.Now() + +func greet(ctx iris.Context) { + ctx.Header("X-Custom", "my custom header") + response := fmt.Sprintf("Hello World! %s", time.Now()) + ctx.WriteWithExpiration([]byte(response), modtime) +} + +func main() { + app := iris.New() + app.Get("/", greet) + app.Run(iris.Addr(":8080")) +} diff --git a/cache/client/handler.go b/cache/client/handler.go index 389d0336..2210f49b 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -4,7 +4,6 @@ import ( "sync" "time" - "github.com/kataras/iris/cache/cfg" "github.com/kataras/iris/cache/client/rule" "github.com/kataras/iris/cache/entry" "github.com/kataras/iris/context" @@ -66,6 +65,12 @@ var emptyHandler = func(ctx context.Context) { ctx.StopExecution() } +func parseLifeChanger(ctx context.Context) entry.LifeChanger { + return func() time.Duration { + return time.Duration(ctx.MaxAge()) * time.Second + } +} + ///TODO: debug this and re-run the parallel tests on larger scale, // because I think we have a bug here when `core/router#StaticWeb` is used after this middleware. func (h *Handler) ServeHTTP(ctx context.Context) { @@ -135,14 +140,19 @@ func (h *Handler) ServeHTTP(ctx context.Context) { // no need to copy the body, its already done inside body := recorder.Body() if len(body) == 0 { - // if no body then just exit + // if no body then just exit. return } // check for an expiration time if the // given expiration was not valid then check for GetMaxAge & // update the response & release the recorder - e.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request())) + e.Reset( + recorder.StatusCode(), + recorder.Header(), + body, + parseLifeChanger(ctx), + ) // fmt.Printf("reset cache entry\n") // fmt.Printf("key: %s\n", key) @@ -152,12 +162,13 @@ func (h *Handler) ServeHTTP(ctx context.Context) { } // if it's valid then just write the cached results - ctx.ContentType(response.ContentType()) + entry.CopyHeaders(ctx.ResponseWriter().Header(), response.Headers()) + context.SetLastModified(ctx, e.LastModified) ctx.StatusCode(response.StatusCode()) ctx.Write(response.Body()) // fmt.Printf("key: %s\n", key) - // fmt.Printf("write content type: %s\n", response.ContentType()) + // fmt.Printf("write content type: %s\n", response.Headers()["ContentType"]) // fmt.Printf("write body len: %d\n", len(response.Body())) } diff --git a/cache/client/response_recorder.go b/cache/client/response_recorder.go deleted file mode 100644 index 04dd1a7d..00000000 --- a/cache/client/response_recorder.go +++ /dev/null @@ -1,109 +0,0 @@ -package client - -import ( - "net/http" - "sync" -) - -var rpool = sync.Pool{} - -// AcquireResponseRecorder returns a ResponseRecorder -func AcquireResponseRecorder(underline http.ResponseWriter) *ResponseRecorder { - v := rpool.Get() - var res *ResponseRecorder - if v != nil { - res = v.(*ResponseRecorder) - } else { - res = &ResponseRecorder{} - } - res.underline = underline - return res -} - -// ReleaseResponseRecorder releases a ResponseRecorder which has been previously received by AcquireResponseRecorder -func ReleaseResponseRecorder(res *ResponseRecorder) { - res.underline = nil - res.statusCode = 0 - res.chunks = res.chunks[0:0] - rpool.Put(res) -} - -// ResponseRecorder is used by httpcache to be able to get the Body and the StatusCode of a request handler -type ResponseRecorder struct { - underline http.ResponseWriter - chunks [][]byte // 2d because .Write can be called more than one time in the same handler and we want to cache all of them - statusCode int // the saved status code which will be used from the cache service -} - -// Body joins the chunks to one []byte slice, this is the full body -func (res *ResponseRecorder) Body() []byte { - var body []byte - for i := range res.chunks { - body = append(body, res.chunks[i]...) - } - return body -} - -// ContentType returns the header's value of "Content-Type" -func (res *ResponseRecorder) ContentType() string { - return res.Header().Get("Content-Type") -} - -// StatusCode returns the status code, if not given then returns 200 -// but doesn't changes the existing behavior -func (res *ResponseRecorder) StatusCode() int { - if res.statusCode == 0 { - return 200 - } - return res.statusCode -} - -// Header returns the header map that will be sent by -// WriteHeader. Changing the header after a call to -// WriteHeader (or Write) has no effect unless the modified -// headers were declared as trailers by setting the -// "Trailer" header before the call to WriteHeader (see example). -// To suppress implicit response headers, set their value to nil. -func (res *ResponseRecorder) Header() http.Header { - return res.underline.Header() -} - -// Write writes the data to the connection as part of an HTTP reply. -// -// If WriteHeader has not yet been called, Write calls -// WriteHeader(http.StatusOK) before writing the data. If the Header -// does not contain a Content-Type line, Write adds a Content-Type set -// to the result of passing the initial 512 bytes of written data to -// DetectContentType. -// -// Depending on the HTTP protocol version and the client, calling -// Write or WriteHeader may prevent future reads on the -// Request.Body. For HTTP/1.x requests, handlers should read any -// needed request body data before writing the response. Once the -// headers have been flushed (due to either an explicit Flusher.Flush -// call or writing enough data to trigger a flush), the request body -// may be unavailable. For HTTP/2 requests, the Go HTTP server permits -// handlers to continue to read the request body while concurrently -// writing the response. However, such behavior may not be supported -// by all HTTP/2 clients. Handlers should read before writing if -// possible to maximize compatibility. -func (res *ResponseRecorder) Write(contents []byte) (int, error) { - if res.statusCode == 0 { // if not setted set it here - res.WriteHeader(http.StatusOK) - } - res.chunks = append(res.chunks, contents) - return res.underline.Write(contents) -} - -// WriteHeader sends an HTTP response header with status code. -// If WriteHeader is not called explicitly, the first call to Write -// will trigger an implicit WriteHeader(http.StatusOK). -// Thus explicit calls to WriteHeader are mainly used to -// send error codes. -func (res *ResponseRecorder) WriteHeader(statusCode int) { - if res.statusCode == 0 { // set it only if not setted already, we don't want logs about multiple sends - res.statusCode = statusCode - res.underline.WriteHeader(statusCode) - } - -} diff --git a/cache/client/utils.go b/cache/client/utils.go deleted file mode 100644 index 33e4bd8c..00000000 --- a/cache/client/utils.go +++ /dev/null @@ -1,20 +0,0 @@ -package client - -import ( - "net/http" - "time" - - "github.com/kataras/iris/cache/entry" -) - -// GetMaxAge parses the "Cache-Control" header -// and returns a LifeChanger which can be passed -// to the response's Reset -func GetMaxAge(r *http.Request) entry.LifeChanger { - return func() time.Duration { - cacheControlHeader := r.Header.Get("Cache-Control") - // headerCacheDur returns the seconds - headerCacheDur := entry.ParseMaxAge(cacheControlHeader) - return time.Duration(headerCacheDur) * time.Second - } -} diff --git a/cache/entry/entry.go b/cache/entry/entry.go index 8e7cbd9b..141d1ede 100644 --- a/cache/entry/entry.go +++ b/cache/entry/entry.go @@ -13,6 +13,11 @@ type Entry struct { // ExpiresAt is the time which this cache will not be available expiresAt time.Time + // when `Reset` this value is reseting to time.Now(), + // it's used to send the "Last-Modified" header, + // some clients may need it. + LastModified time.Time + // Response the response should be served to the client response *Response // but we need the key to invalidate manually...xmm @@ -78,10 +83,23 @@ func (e *Entry) ChangeLifetime(fdur LifeChanger) { } } +// CopyHeaders clones headers "src" to "dst" . +func CopyHeaders(dst map[string][]string, src map[string][]string) { + if dst == nil || src == nil { + return + } + + for k, vv := range src { + v := make([]string, len(vv)) + copy(v, vv) + dst[k] = v + } +} + // Reset called each time the entry is expired // and the handler calls this after the original handler executed // to re-set the response with the new handler's content result -func (e *Entry) Reset(statusCode int, contentType string, +func (e *Entry) Reset(statusCode int, headers map[string][]string, body []byte, lifeChanger LifeChanger) { if e.response == nil { @@ -91,8 +109,10 @@ func (e *Entry) Reset(statusCode int, contentType string, e.response.statusCode = statusCode } - if contentType != "" { - e.response.contentType = contentType + if len(headers) > 0 { + newHeaders := make(map[string][]string, len(headers)) + CopyHeaders(newHeaders, headers) + e.response.headers = newHeaders } e.response.body = body @@ -101,5 +121,8 @@ func (e *Entry) Reset(statusCode int, contentType string, if lifeChanger != nil { e.ChangeLifetime(lifeChanger) } - e.expiresAt = time.Now().Add(e.life) + + now := time.Now() + e.expiresAt = now.Add(e.life) + e.LastModified = now } diff --git a/cache/entry/response.go b/cache/entry/response.go index 53569c59..7e24f44b 100644 --- a/cache/entry/response.go +++ b/cache/entry/response.go @@ -1,19 +1,21 @@ package entry +import "net/http" + // Response is the cached response will be send to the clients // its fields setted at runtime on each of the non-cached executions // non-cached executions = first execution, and each time after -// cache expiration datetime passed +// cache expiration datetime passed. type Response struct { - // statusCode for the response cache handler + // statusCode for the response cache handler. statusCode int - // contentType for the response cache handler - contentType string - // body is the contents will be served by the cache handler + // body is the contents will be served by the cache handler. body []byte + // the total headers of the response, including content type. + headers http.Header } -// StatusCode returns a valid status code +// StatusCode returns a valid status code. func (r *Response) StatusCode() int { if r.statusCode <= 0 { r.statusCode = 200 @@ -22,14 +24,19 @@ func (r *Response) StatusCode() int { } // ContentType returns a valid content type -func (r *Response) ContentType() string { - if r.contentType == "" { - r.contentType = "text/html; charset=utf-8" - } - return r.contentType +// func (r *Response) ContentType() string { +// if r.headers == "" { +// r.contentType = "text/html; charset=utf-8" +// } +// return r.contentType +// } + +// Headers returns the total headers of the cached response. +func (r *Response) Headers() http.Header { + return r.headers } -// Body returns contents will be served by the cache handler +// Body returns contents will be served by the cache handler. func (r *Response) Body() []byte { return r.body } diff --git a/core/router/fs.go b/core/router/fs.go index 822058cf..6c9ac7c8 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -823,7 +823,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo // and the binary data inside "f". detectOrWriteContentType(ctx, d.Name(), f) - return "", 200 + return "", http.StatusOK } // toHTTPError returns a non-specific HTTP error message and status code From 969c2e87d4c62fdb99d5586c47441e990f51f1fb Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 16:19:45 +0200 Subject: [PATCH 2/3] add `Cache304` as an alternative to the server-side kataras/iris/cache middleware - it can perform better with less server overheat but it comes with a cost of 304 instead of 200 so custom clients must make that check Former-commit-id: b0ba68c528c870fe060e2825c35689771a1d3680 --- _examples/README.md | 1 + _examples/cache/client-side/main.go | 28 +++++++--- _examples/cache/simple/main.go | 5 ++ cache/cache.go | 11 +++- cache/client/handler.go | 2 +- context/context.go | 84 +++++++++++++++++++++++++---- core/router/fs.go | 16 +++--- iris.go | 23 +++++++- 8 files changed, 140 insertions(+), 30 deletions(-) diff --git a/_examples/README.md b/_examples/README.md index 3076b4c5..2ba8c680 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -382,6 +382,7 @@ The `httptest` package is your way for end-to-end HTTP testing, it uses the http iris cache library lives on its own [package](https://github.com/kataras/iris/tree/master/cache). - [Simple](cache/simple/main.go) +- [Client-Side (304)](cache/client-side/main.go) - part of the iris context core > You're free to use your own favourite caching package if you'd like so. diff --git a/_examples/cache/client-side/main.go b/_examples/cache/client-side/main.go index 02b054f3..8b21438e 100644 --- a/_examples/cache/client-side/main.go +++ b/_examples/cache/client-side/main.go @@ -6,22 +6,34 @@ package main import ( - "fmt" "time" "github.com/kataras/iris" ) -var modtime = time.Now() - -func greet(ctx iris.Context) { - ctx.Header("X-Custom", "my custom header") - response := fmt.Sprintf("Hello World! %s", time.Now()) - ctx.WriteWithExpiration([]byte(response), modtime) -} +const refreshEvery = 10 * time.Second func main() { app := iris.New() + app.Use(iris.Cache304(refreshEvery)) + // same as: + // app.Use(func(ctx iris.Context) { + // now := time.Now() + // if modified, err := ctx.CheckIfModifiedSince(now.Add(-refresh)); !modified && err == nil { + // ctx.WriteNotModified() + // return + // } + + // ctx.SetLastModified(now) + + // ctx.Next() + // }) + app.Get("/", greet) app.Run(iris.Addr(":8080")) } + +func greet(ctx iris.Context) { + ctx.Header("X-Custom", "my custom header") + ctx.Writef("Hello World! %s", time.Now()) +} diff --git a/_examples/cache/simple/main.go b/_examples/cache/simple/main.go index 04aa0246..09f88881 100644 --- a/_examples/cache/simple/main.go +++ b/_examples/cache/simple/main.go @@ -73,3 +73,8 @@ func writeMarkdown(ctx iris.Context) { ctx.Markdown(markdownContents) } + +/* Note that `StaticWeb` does use the browser's disk caching by-default +therefore, register the cache handler AFTER any StaticWeb calls, +for a faster solution that server doesn't need to keep track of the response +navigate to https://github.com/kataras/iris/blob/master/_examples/cache/client-side/main.go */ diff --git a/cache/cache.go b/cache/cache.go index 09d5ab91..c6d100da 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -1,4 +1,7 @@ -/* Package cache provides cache capabilities with rich support of options and rules. +/* Package cache provides server-side caching capabilities with rich support of options and rules. + +Use it for server-side caching, see the `iris#Cache304` for an alternative approach that +may fit your needs most. Example code: @@ -37,6 +40,9 @@ import ( // // All types of response can be cached, templates, json, text, anything. // +// Use it for server-side caching, see the `iris#Cache304` for an alternative approach that +// may fit your needs most. +// // You can add validators with this function. func Cache(expiration time.Duration) *client.Handler { return client.NewHandler(expiration) @@ -49,6 +55,9 @@ func Cache(expiration time.Duration) *client.Handler { // // All types of response can be cached, templates, json, text, anything. // +// Use it for server-side caching, see the `iris#Cache304` for an alternative approach that +// may fit your needs most. +// // it returns a context.Handler which can be used as a middleware, for more options use the `Cache`. // // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching diff --git a/cache/client/handler.go b/cache/client/handler.go index 2210f49b..6cc35345 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -163,7 +163,7 @@ func (h *Handler) ServeHTTP(ctx context.Context) { // if it's valid then just write the cached results entry.CopyHeaders(ctx.ResponseWriter().Header(), response.Headers()) - context.SetLastModified(ctx, e.LastModified) + ctx.SetLastModified(e.LastModified) ctx.StatusCode(response.StatusCode()) ctx.Write(response.Body()) diff --git a/context/context.go b/context/context.go index 494b18e5..35a91896 100644 --- a/context/context.go +++ b/context/context.go @@ -639,6 +639,39 @@ type Context interface { // // Returns the number of bytes written and any write error encountered. WriteString(body string) (int, error) + + // SetLastModified sets the "Last-Modified" based on the "modtime" input. + // If "modtime" is zero then it does nothing. + // + // It's mostly internally on core/router and context packages. + // + // Note that modtime.UTC() is being used instead of just modtime, so + // you don't have to know the internals in order to make that works. + SetLastModified(modtime time.Time) + // CheckIfModifiedSince checks if the response is modified since the "modtime". + // Note that it has nothing to do with server-side caching. + // It does those checks by checking if the "If-Modified-Since" request header + // sent by client or a previous server response header + // (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) + // is a valid one and it's before the "modtime". + // + // A check for !modtime && err == nil is necessary to make sure that + // it's not modified since, because it may return false but without even + // had the chance to check the client-side (request) header due to some errors, + // like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero + // or if parsing time from the header failed. + // + // It's mostly used internally, e.g. `context#WriteWithExpiration`. + // + // Note that modtime.UTC() is being used instead of just modtime, so + // you don't have to know the internals in order to make that works. + CheckIfModifiedSince(modtime time.Time) (bool, error) + // WriteNotModified sends a 304 "Not Modified" status code to the client, + // it makes sure that the content type, the content length headers + // and any "ETag" are removed before the response sent. + // + // It's mostly used internally on core/router/fs.go and context methods. + WriteNotModified() // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. WriteWithExpiration(body []byte, modtime time.Time) (int, error) @@ -913,6 +946,35 @@ 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) { @@ -2092,9 +2154,9 @@ var FormatTime = func(ctx Context, t time.Time) string { // If "modtime" is zero then it does nothing. // // It's mostly internally on core/router and context packages. -func SetLastModified(ctx Context, modtime time.Time) { +func (ctx *context) SetLastModified(modtime time.Time) { if !IsZeroTime(modtime) { - ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime)) // or modtime.UTC()? + ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()? } } @@ -2112,7 +2174,7 @@ func SetLastModified(ctx Context, modtime time.Time) { // or if parsing time from the header failed. // // It's mostly used internally, e.g. `context#WriteWithExpiration`. -func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { +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") } @@ -2126,7 +2188,7 @@ func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { } // sub-second precision, so // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { + if modtime.UTC().Before(t.Add(1 * time.Second)) { return false, nil } return true, nil @@ -2137,7 +2199,7 @@ func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { // and any "ETag" are removed before the response sent. // // It's mostly used internally on core/router/fs.go and context methods. -func WriteNotModified(ctx Context) { +func (ctx *context) WriteNotModified() { // RFC 7232 section 4.1: // a sender SHOULD NOT generate representation metadata other than the // above listed fields unless said metadata exists for the purpose of @@ -2155,12 +2217,12 @@ func WriteNotModified(ctx Context) { // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) { - if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { - WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { + ctx.WriteNotModified() return 0, nil } - SetLastModified(ctx, modtime) + ctx.SetLastModified(modtime) return ctx.writer.Write(body) } @@ -2750,13 +2812,13 @@ const ( // You can define your own "Content-Type" header also, after this function call // Doesn't implements resuming (by range), use ctx.SendFile instead func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { - if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { - WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { + ctx.WriteNotModified() return nil } ctx.ContentType(filename) - SetLastModified(ctx, modtime) + ctx.SetLastModified(modtime) var out io.Writer if gzipCompression && ctx.ClientSupportsGzip() { ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) diff --git a/core/router/fs.go b/core/router/fs.go index 6c9ac7c8..ace1a494 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -411,7 +411,7 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS // content must be seeked to the beginning of the file. // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ { - context.SetLastModified(ctx, modtime) + ctx.SetLastModified(modtime) done, rangeReq := checkPreconditions(ctx, modtime) if done { return "", http.StatusNotModified @@ -651,15 +651,15 @@ func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rang switch checkIfNoneMatch(ctx) { case condFalse: if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead { - context.WriteNotModified(ctx) + ctx.WriteNotModified() return true, "" } ctx.StatusCode(http.StatusPreconditionFailed) return true, "" case condNone: - if modified, err := context.CheckIfModifiedSince(ctx, modtime); !modified && err == nil { - context.WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { + ctx.WriteNotModified() return true, "" } } @@ -785,11 +785,11 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo if !showList { return "", http.StatusForbidden } - if modified, err := context.CheckIfModifiedSince(ctx, d.ModTime()); !modified && err == nil { - context.WriteNotModified(ctx) + if modified, err := ctx.CheckIfModifiedSince(d.ModTime()); !modified && err == nil { + ctx.WriteNotModified() return "", http.StatusNotModified } - ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) + ctx.SetLastModified(d.ModTime()) return dirList(ctx, f) } @@ -801,7 +801,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo } // else, set the last modified as "serveContent" does. - context.SetLastModified(ctx, d.ModTime()) + ctx.SetLastModified(d.ModTime()) // write the file to the response writer. contents, err := ioutil.ReadAll(f) diff --git a/iris.go b/iris.go index 40431e3c..35316c6f 100644 --- a/iris.go +++ b/iris.go @@ -360,11 +360,32 @@ var ( // // A shortcut for the `handlerconv#FromStd`. FromStd = handlerconv.FromStd - // Cache is a middleware providing cache functionalities + // 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. // // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching Cache = cache.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, which is a shortcut of the, `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). + // + // A shortcut of the `context#Cache304`. + Cache304 = context.Cache304 ) // SPA accepts an "assetHandler" which can be the result of an From 687477f21009234200124d5f673056300f0622cc Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 28 Jan 2018 15:43:25 +0200 Subject: [PATCH 3/3] fix comment on csrf example Former-commit-id: c5c001cbc5381fb169429f132ea18c4fc7c76a42 --- _examples/experimental-handlers/csrf/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/experimental-handlers/csrf/main.go b/_examples/experimental-handlers/csrf/main.go index c9e6a7d5..e8a8bf00 100644 --- a/_examples/experimental-handlers/csrf/main.go +++ b/_examples/experimental-handlers/csrf/main.go @@ -39,7 +39,7 @@ func main() { } func getSignupForm(ctx iris.Context) { - // views/signup.html just needs a {{ .csrfField }} template tag for + // views/user/signup.html just needs a {{ .csrfField }} template tag for // csrf.TemplateField to inject the CSRF token into. Easy! ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx)) ctx.View("user/signup.html")