add cache control for frontend assets

This commit is contained in:
Umputun
2020-01-15 15:07:45 -06:00
parent b082c2a82f
commit b0898cd1f3
2 changed files with 73 additions and 11 deletions
+40 -11
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -338,7 +339,7 @@ func (s *Rest) routes() chi.Router {
})
// file server for static content from /web
addFileServer(router, "/web", http.Dir(s.WebRoot))
addFileServer(router, "/web", http.Dir(s.WebRoot), s.Version)
return router
}
@@ -444,7 +445,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
}
// serves static files from /web or embedded by statik
func addFileServer(r chi.Router, path string, root http.FileSystem) {
func addFileServer(r chi.Router, path string, root http.FileSystem, version string) {
var webFS http.Handler
@@ -466,15 +467,17 @@ func addFileServer(r chi.Router, path string, root http.FileSystem) {
}
path += "*"
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil)), middleware.Timeout(10*time.Second)).
Get(path, func(w http.ResponseWriter, r *http.Request) {
// don't show dirs, just serve files
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != (origPath+"/") {
http.NotFound(w, r)
return
}
webFS.ServeHTTP(w, r)
})
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil)),
middleware.Timeout(10*time.Second),
cacheControl(time.Hour*24, version),
).Get(path, func(w http.ResponseWriter, r *http.Request) {
// don't show dirs, just serve files
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != (origPath+"/") {
http.NotFound(w, r)
return
}
webFS.ServeHTTP(w, r)
})
}
func encodeJSONWithHTML(v interface{}) ([]byte, error) {
@@ -566,6 +569,32 @@ func matchSiteID(next http.Handler) http.Handler {
return http.HandlerFunc(fn)
}
// cacheControl is a middleware setting cache expiration. Using url+version as etag
func cacheControl(expiration time.Duration, version string) func(http.Handler) http.Handler {
etag := func(r *http.Request, version string) string {
s := version + ":" + r.URL.String()
return store.EncodeID(s)
}
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
e := `"` + etag(r, version) + `"`
w.Header().Set("Etag", e)
w.Header().Set("Cache-Control", "max-age="+strconv.Itoa(int(expiration.Seconds())))
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, e) {
w.WriteHeader(http.StatusNotModified)
return
}
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func parseError(err error, defaultCode int) (code int) {
code = defaultCode
+33
View File
@@ -293,6 +293,39 @@ func TestRest_parseError(t *testing.T) {
}
}
func TestRest_cacheControl(t *testing.T) {
tbl := []struct {
url string
version string
exp time.Duration
etag string
maxAge int
}{
{"http://example.com/foo", "v1", time.Hour, "b433be1ea19edaee9dc92ca4b895b6bdf3c058cb", 3600},
{"http://example.com/foo2", "v1", 10 * time.Hour, "6d8466aef3246c1057452561acddf7ad9d0d99e0", 36000},
{"http://example.com/foo", "v2", time.Hour, "481700c52aab0dfbca99f3ffc2a4fbb27884c114", 3600},
{"https://example.com/foo", "v2", time.Hour, "bebd4f1b87f474792c4e75e5affe31fbf67f5778", 3600},
}
for i, tt := range tbl {
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest("GET", tt.url, nil)
w := httptest.NewRecorder()
h := cacheControl(tt.exp, tt.version)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
h.ServeHTTP(w, req)
resp := w.Result()
assert.Equal(t, http.StatusOK, resp.StatusCode)
t.Logf("%+v", resp.Header)
assert.Equal(t, `"`+tt.etag+`"`, resp.Header.Get("Etag"))
assert.Equal(t, `max-age=`+strconv.Itoa(int(tt.exp.Seconds())), resp.Header.Get("Cache-Control"))
})
}
}
func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
log.Setup(log.CallerFile, log.CallerFunc, log.Msec, log.LevelBraces)