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
This commit is contained in:
Gerasimos Maropoulos 2018-03-18 11:55:05 +02:00
parent fae3906587
commit 8e9deec4ab
4 changed files with 72 additions and 4 deletions

43
cache/browser.go vendored
View File

@ -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()

25
cache/browser_test.go vendored
View File

@ -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("__")
}

View File

@ -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)

View File

@ -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()
}