From 5a29107f3bb90fb9d1e1c41c9cb122e5c6cf0492 Mon Sep 17 00:00:00 2001 From: makpoc Date: Wed, 28 Oct 2015 14:38:58 +0200 Subject: [PATCH] Add Last-Modified header when serving markdown and templates --- middleware/header.go | 33 +++++++++++++++ middleware/header_test.go | 70 +++++++++++++++++++++++++++++++ middleware/markdown/markdown.go | 2 + middleware/templates/templates.go | 9 +++- 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 middleware/header.go create mode 100644 middleware/header_test.go diff --git a/middleware/header.go b/middleware/header.go new file mode 100644 index 00000000..56749b55 --- /dev/null +++ b/middleware/header.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "net/http" + "time" +) + +// currentTime returns time.Now() everytime it's called. It's used for mocking in tests. +var currentTime = func() time.Time { + return time.Now() +} + +// SetLastModifiedHeader checks if the provided modTime is valid and if it is sets it +// as a Last-Modified header to the ResponseWriter. If the modTime is in the future +// the current time is used instead. +func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) { + if modTime.IsZero() || modTime.Equal(time.Unix(0, 0)) { + // the time does not appear to be valid. Don't put it in the response + return + } + + // RFC 2616 - Section 14.29 - Last-Modified: + // An origin server MUST NOT send a Last-Modified date which is later than the + // server's time of message origination. In such cases, where the resource's last + // modification would indicate some time in the future, the server MUST replace + // that date with the message origination date. + now := currentTime() + if modTime.After(now) { + modTime = now + } + + w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat)) +} diff --git a/middleware/header_test.go b/middleware/header_test.go new file mode 100644 index 00000000..c76d8e63 --- /dev/null +++ b/middleware/header_test.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestSetLastModified(t *testing.T) { + nowTime := time.Now() + + // ovewrite the function to return reliable time + originalGetCurrentTimeFunc := currentTime + currentTime = func() time.Time { + return nowTime + } + defer func() { + currentTime = originalGetCurrentTimeFunc + }() + + pastTime := nowTime.Truncate(1 * time.Hour) + futureTime := nowTime.Add(1 * time.Hour) + + tests := []struct { + inputModTime time.Time + expectedIsHeaderSet bool + expectedLastModified string + }{ + { + inputModTime: pastTime, + expectedIsHeaderSet: true, + expectedLastModified: pastTime.UTC().Format(http.TimeFormat), + }, + { + inputModTime: nowTime, + expectedIsHeaderSet: true, + expectedLastModified: nowTime.UTC().Format(http.TimeFormat), + }, + { + inputModTime: futureTime, + expectedIsHeaderSet: true, + expectedLastModified: nowTime.UTC().Format(http.TimeFormat), + }, + { + inputModTime: time.Time{}, + expectedIsHeaderSet: false, + }, + } + + for i, test := range tests { + responseRecorder := httptest.NewRecorder() + errorPrefix := fmt.Sprintf("Test [%d]: ", i) + SetLastModifiedHeader(responseRecorder, test.inputModTime) + actualLastModifiedHeader := responseRecorder.Header().Get("Last-Modified") + + if test.expectedIsHeaderSet && actualLastModifiedHeader == "" { + t.Fatalf(errorPrefix + "Expected to find Last-Modified header, but found nothing") + } + + if !test.expectedIsHeaderSet && actualLastModifiedHeader != "" { + t.Fatalf(errorPrefix+"Did not expect to find Last-Modified header, but found one [%s].", actualLastModifiedHeader) + } + + if test.expectedLastModified != actualLastModifiedHeader { + t.Errorf(errorPrefix+"Expected Last-Modified content [%s], found [%s}", test.expectedLastModified, actualLastModifiedHeader) + } + } +} diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index bdf142cf..3b3bc96e 100644 --- a/middleware/markdown/markdown.go +++ b/middleware/markdown/markdown.go @@ -136,6 +136,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // generation, serve the static page if fs.ModTime().Before(fs1.ModTime()) { if html, err := ioutil.ReadFile(filepath); err == nil { + middleware.SetLastModifiedHeader(w, fs1.ModTime()) w.Write(html) return http.StatusOK, nil } @@ -162,6 +163,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error return http.StatusInternalServerError, err } + middleware.SetLastModifiedHeader(w, fs.ModTime()) w.Write(html) return http.StatusOK, nil } diff --git a/middleware/templates/templates.go b/middleware/templates/templates.go index a699d002..76447479 100644 --- a/middleware/templates/templates.go +++ b/middleware/templates/templates.go @@ -34,7 +34,8 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ctx := middleware.Context{Root: t.FileSys, Req: r, URL: r.URL} // Build the template - tpl, err := template.ParseFiles(filepath.Join(t.Root, fpath)) + templatePath := filepath.Join(t.Root, fpath) + tpl, err := template.ParseFiles(templatePath) if err != nil { if os.IsNotExist(err) { return http.StatusNotFound, nil @@ -50,6 +51,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error if err != nil { return http.StatusInternalServerError, err } + + templateInfo, err := os.Stat(templatePath) + if err == nil { + // add the Last-Modified header if we were able to optain the information + middleware.SetLastModifiedHeader(w, templateInfo.ModTime()) + } buf.WriteTo(w) return http.StatusOK, nil