From b0898cd1f3cd29d3c4fb8ddcc4df370bbb7278d7 Mon Sep 17 00:00:00 2001 From: Umputun Date: Wed, 15 Jan 2020 15:07:45 -0600 Subject: [PATCH] add cache control for frontend assets --- backend/app/rest/api/rest.go | 51 ++++++++++++++++++++++++------- backend/app/rest/api/rest_test.go | 33 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 65eb353c..5dbec418 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -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 diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 42710de5..c7092800 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -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)