diff --git a/plugin/http.go b/plugin/http.go index 30c844be5..4e9b4f02e 100644 --- a/plugin/http.go +++ b/plugin/http.go @@ -115,6 +115,11 @@ func NewHTTP(log *util.Logger, method, uri string, insecure bool, cache time.Dur Modifier: func(resp *http.Response) error { dropCacheBusting(resp, "Cache-Control") dropCacheBusting(resp, "Pragma") + // httpcache derives freshness from the response Date; stamp one + // for devices that omit it, else every read is treated as stale + if resp.Header.Get("Date") == "" { + resp.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) + } return nil }, Base: base, diff --git a/plugin/http_test.go b/plugin/http_test.go index ddb7f6c5d..06e78986e 100644 --- a/plugin/http_test.go +++ b/plugin/http_test.go @@ -1,6 +1,7 @@ package plugin import ( + "fmt" "net/http" "net/http/httptest" "testing" @@ -16,11 +17,26 @@ type httpHandler struct { req *http.Request cnt int cacheBusting bool + noDate bool } func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { h.req = req h.val = lo.RandomString(16, lo.LettersCharset) + + // emulate a device that omits the Date header (e.g. Zendure Solarflow) + if h.noDate { + conn, buf, err := w.(http.Hijacker).Hijack() + if err != nil { + panic(err) + } + defer conn.Close() + fmt.Fprintf(buf, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s", len(h.val), h.val) + buf.Flush() + h.cnt++ + return + } + if h.cacheBusting { w.Header().Set("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate") w.Header().Set("Pragma", "no-cache") @@ -101,6 +117,30 @@ func (suite *httpTestSuite) TestCacheGetNoStore() { } } +func (suite *httpTestSuite) TestCacheGetNoDate() { + // upstream omits the Date header, cache must still take effect via injected Date + suite.h.noDate = true + defer func() { suite.h.noDate = false }() + + uri := suite.srv.URL + "/foo/bar?baz=3" + p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, time.Minute) + + g, err := p.StringGetter() + suite.Require().NoError(err) + + suite.h.cnt = 0 + res, err := g() + suite.Require().NoError(err) + suite.Require().Equal(1, suite.h.cnt) + + for range 3 { + val, err := g() + suite.Require().NoError(err) + suite.Require().Equal(res, val) + suite.Require().Equal(1, suite.h.cnt) + } +} + func (suite *httpTestSuite) TestSetQuery() { uri := suite.srv.URL + "/foo/bar?baz={{.baz}}" p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 0)