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