1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-07-07 16:48:36 +00:00

feat: use Cache-Control max-age and Expires headers to calculate next check

This commit is contained in:
Frédéric Guillot 2025-04-06 16:18:41 -07:00
parent 0af1a6e121
commit c45b51d1f8
3 changed files with 110 additions and 2 deletions

View file

@ -9,6 +9,7 @@ import (
"fmt"
"io"
"log/slog"
"math"
"net"
"net/http"
"net/url"
@ -53,6 +54,33 @@ func (r *ResponseHandler) ETag() string {
return r.httpResponse.Header.Get("ETag")
}
func (r *ResponseHandler) ExpiresInMinutes() int {
expiresHeaderValue := r.httpResponse.Header.Get("Expires")
if expiresHeaderValue != "" {
t, err := time.Parse(time.RFC1123, expiresHeaderValue)
if err == nil {
return int(math.Ceil(time.Until(t).Minutes()))
}
}
return 0
}
func (r *ResponseHandler) CacheControlMaxAgeInMinutes() int {
cacheControlHeaderValue := r.httpResponse.Header.Get("Cache-Control")
if cacheControlHeaderValue != "" {
for _, directive := range strings.Split(cacheControlHeaderValue, ",") {
directive = strings.TrimSpace(directive)
if strings.HasPrefix(directive, "max-age=") {
maxAge, err := strconv.Atoi(strings.TrimPrefix(directive, "max-age="))
if err == nil {
return int(math.Ceil(float64(maxAge) / 60))
}
}
}
}
return 0
}
func (r *ResponseHandler) ParseRetryDelay() int {
retryAfterHeaderValue := r.httpResponse.Header.Get("Retry-After")
if retryAfterHeaderValue != "" {

View file

@ -102,3 +102,75 @@ func TestRetryDelay(t *testing.T) {
})
}
}
func TestExpiresInMinutes(t *testing.T) {
var testCases = map[string]struct {
ExpiresHeader string
ExpectedMinutes int
}{
"Empty header": {
ExpiresHeader: "",
ExpectedMinutes: 0,
},
"Valid Expires header": {
ExpiresHeader: time.Now().Add(10 * time.Minute).Format(time.RFC1123),
ExpectedMinutes: 10,
},
"Invalid Expires header": {
ExpiresHeader: "invalid-date",
ExpectedMinutes: 0,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
header := http.Header{}
header.Add("Expires", tc.ExpiresHeader)
rh := ResponseHandler{
httpResponse: &http.Response{
Header: header,
},
}
if tc.ExpectedMinutes != rh.ExpiresInMinutes() {
t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.ExpiresInMinutes(), name)
}
})
}
}
func TestCacheControlMaxAgeInMinutes(t *testing.T) {
var testCases = map[string]struct {
CacheControlHeader string
ExpectedMinutes int
}{
"Empty header": {
CacheControlHeader: "",
ExpectedMinutes: 0,
},
"Valid max-age": {
CacheControlHeader: "max-age=600",
ExpectedMinutes: 10,
},
"Invalid max-age": {
CacheControlHeader: "max-age=invalid",
ExpectedMinutes: 0,
},
"Multiple directives": {
CacheControlHeader: "no-cache, max-age=300",
ExpectedMinutes: 5,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
header := http.Header{}
header.Add("Cache-Control", tc.CacheControlHeader)
rh := ResponseHandler{
httpResponse: &http.Response{
Header: header,
},
}
if tc.ExpectedMinutes != rh.CacheControlMaxAgeInMinutes() {
t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.CacheControlMaxAgeInMinutes(), name)
}
})
}
}