cache-control tests

This commit is contained in:
perennial
2024-12-04 10:40:24 +11:00
parent affb32a7af
commit 780ea8c339
15 changed files with 149 additions and 27 deletions
+5 -1
View File
@@ -25,12 +25,16 @@ PIXIVFE_HOST='127.0.0.1'
# PIXIVFE_TOKEN_BASE_TIMEOUT=
# PIXIVFE_TOKEN_MAX_BACKOFF_TIME=
### Caching configuration
### API response caching configuration
# PIXIVFE_CACHE_ENABLED=
# PIXIVFE_CACHE_SIZE=
# PIXIVFE_CACHE_TTL=
# PIXIVFE_CACHE_SHORT_TTL=
### HTTP caching configuration
# PIXIVFE_CACHE_CONTROL_MAX_AGE=
# PIXIVFE_CACHE_CONTROL_STALE_WHILE_REVALIDATE=
### Feature configuration
# PIXIVFE_POPULAR_SEARCH_ENABLED=
+29 -19
View File
@@ -31,24 +31,26 @@ const (
defaultPort = "8282"
defaultRepoUrl = "https://codeberg.org/PixivFE/PixivFE"
defaultAcceptLanguage = "en-US,en;q=0.5"
defaultProxyServerStaging = BuiltinProxyUrl
defaultProxyCheckEnabled = true
defaultProxyCheckInterval = 8 * time.Hour
defaultProxyCheckTimeout = 4 * time.Second
defaultTokenLoadBalancing = "round-robin"
defaultTokenMaxRetries = 5
defaultTokenBaseTimeout = 1000 * time.Millisecond
defaultTokenMaxBackoffTime = 32000 * time.Millisecond
defaultCacheEnabled = false
defaultCacheSize = 100
defaultCacheTTL = 60 * time.Minute
defaultCacheShortTTL = 10 * time.Second
defaultPopularSearchEnabled = false
defaultResponseSaveLocation = "/tmp/pixivfe/responses"
defaultLogLevel = "info"
defaultLogFormat = "console"
defaultRepoUrl = "https://codeberg.org/PixivFE/PixivFE"
defaultAcceptLanguage = "en-US,en;q=0.5"
defaultProxyServerStaging = BuiltinProxyUrl
defaultProxyCheckEnabled = true
defaultProxyCheckInterval = 8 * time.Hour
defaultProxyCheckTimeout = 4 * time.Second
defaultTokenLoadBalancing = "round-robin"
defaultTokenMaxRetries = 5
defaultTokenBaseTimeout = 1000 * time.Millisecond
defaultTokenMaxBackoffTime = 32000 * time.Millisecond
defaultCacheEnabled = false
defaultCacheSize = 100
defaultCacheTTL = 60 * time.Minute
defaultCacheShortTTL = 10 * time.Second
defaultCacheControlMaxAge = 30 * time.Second
defaultCacheControlStaleWhileRevalidate = 60 * time.Second
defaultPopularSearchEnabled = false
defaultResponseSaveLocation = "/tmp/pixivfe/responses"
defaultLogLevel = "info"
defaultLogFormat = "console"
)
var defaultLogOutputs = []string{"stdout"}
@@ -86,12 +88,17 @@ type ServerConfig struct {
ProxyCheckInterval time.Duration `env:"PIXIVFE_PROXY_CHECK_INTERVAL,overwrite"`
ProxyCheckTimeout time.Duration `env:"PIXIVFE_PROXY_CHECK_TIMEOUT,overwrite"`
// Caching configuration
// API response caching configuration
CacheEnabled bool `env:"PIXIVFE_CACHE_ENABLED,overwrite"`
CacheSize int `env:"PIXIVFE_CACHE_SIZE,overwrite"`
CacheTTL time.Duration `env:"PIXIVFE_CACHE_TTL,overwrite"`
CacheShortTTL time.Duration `env:"PIXIVFE_CACHE_SHORT_TTL,overwrite"`
// Cache-Control header configuration
// TODO: fine-tune the default durations to actual usage patterns
CacheControlMaxAge time.Duration `env:"PIXIVFE_CACHE_CONTROL_MAX_AGE,overwrite"`
CacheControlStaleWhileRevalidate time.Duration `env:"PIXIVFE_CACHE_CONTROL_STALE_WHILE_REVALIDATE,overwrite"`
// Feature configuration
PopularSearchEnabled bool `env:"PIXIVFE_POPULAR_SEARCH_ENABLED,overwrite"`
@@ -165,6 +172,9 @@ func (s *ServerConfig) LoadConfig() error {
s.CacheTTL = defaultCacheTTL
s.CacheShortTTL = defaultCacheShortTTL
s.CacheControlMaxAge = defaultCacheControlMaxAge
s.CacheControlStaleWhileRevalidate = defaultCacheControlStaleWhileRevalidate
s.PopularSearchEnabled = defaultPopularSearchEnabled
s.ResponseSaveLocation = defaultResponseSaveLocation
+21 -1
View File
@@ -167,7 +167,7 @@ Base timeout duration for token management.
Maximum backoff time for token management.
## Caching configuration
## API response caching configuration
PixivFE implements a caching system for API responses to improve performance. The cache uses a [Least Recently Used (LRU) eviction policy](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_(LRU)).
@@ -215,6 +215,26 @@ Specifies a shorter Time To Live (TTL) for specific API endpoints that change fr
These paths are considered more dynamic, and the shorter TTL ensures more up-to-date content.
## HTTP caching configuration
These cache control settings affect browser-level caching behavior and are separate from PixivFE's internal API response cache. They help reduce server load by allowing browsers to cache artwork pages while ensuring content stays reasonably fresh.
### `PIXIVFE_CACHE_CONTROL_MAX_AGE`
**Required**: No
**Default:** `30s`
Controls the `max-age` directive in the Cache-Control response header for artwork pages. This determines how long browsers should cache the page before revalidating. The value should be specified in Go's `time.Duration` notation (e.g., `60s`, `5m`).
### `PIXIVFE_CACHE_CONTROL_STALE_WHILE_REVALIDATE`
**Required**: No
**Default:** `60s`
Controls the `stale-while-revalidate` directive in the Cache-Control response header for artwork pages. This allows browsers to show stale content while fetching a fresh version in the background. The value should be specified in Go's `time.Duration` notation (e.g., `120s`, `2m`).
## Feature configuration
### `PIXIVFE_POPULAR_SEARCH_ENABLED`
+6
View File
@@ -6,6 +6,7 @@ import (
"strconv"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/i18n"
)
@@ -31,6 +32,11 @@ func ArtworkPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request)
PreloadImage(w, img.Large)
}
// TODO: need to handle bookmarked/liked status
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_artwork{
Illust: *illust,
Title: illust.Title,
+6
View File
@@ -8,6 +8,7 @@ import (
"sync"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/i18n"
)
@@ -66,6 +67,11 @@ func ArtworkMultiPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Req
}
}
// TODO: need to handle bookmarked/liked status
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_artworkMulti{
Artworks: artworks,
Title: fmt.Sprintf("(%d images)", len(artworks)),
+7
View File
@@ -1,9 +1,11 @@
package routes
import (
"fmt"
"net/http"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/server/session"
"codeberg.org/vnpower/pixivfe/v2/server/template"
@@ -24,6 +26,11 @@ func IndexPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request) e
Query: map[string]string{"mode": mode},
}
// Login status handled by RenderWithContentType, so set public directive unconditionally here
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_index{
Title: "Landing",
Data: *works,
+5
View File
@@ -7,6 +7,7 @@ import (
"strconv"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/i18n"
)
@@ -62,6 +63,10 @@ func MangaSeriesPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Requ
title := fmt.Sprintf("%s / %s Series", seriesContent.Brief.Title, user.Name)
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_mangaSeries{
MangaSeriesContent: seriesContent,
Title: title,
+6
View File
@@ -7,6 +7,7 @@ import (
"strings"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/i18n"
"codeberg.org/vnpower/pixivfe/v2/server/session"
@@ -75,6 +76,11 @@ func NovelPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request) e
novelSeriesTitles[i] = fmt.Sprintf("#%d %s", i+1, ct.Title)
}
// User preferences handled by Vary: Cookie
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_novel{
Novel: novel,
NovelRelated: related,
+5
View File
@@ -7,6 +7,7 @@ import (
"strconv"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/i18n"
)
@@ -45,6 +46,10 @@ func NovelSeriesPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Requ
title := fmt.Sprintf("%s | %s", series.Title, series.UserName)
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_novelSeries{
NovelSeries: series,
NovelSeriesContents: seriesContents,
+19
View File
@@ -1,10 +1,13 @@
package routes
import (
"fmt"
"net/http"
"strconv"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/server/session"
)
@@ -27,6 +30,10 @@ func PixivisionHomePage(w http.ResponseWriter, r *http.Request) error {
return err
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_pixivisionIndex{
Data: data,
Page: pageint,
@@ -57,6 +64,10 @@ func PixivisionArticlePage(w http.ResponseWriter, r *http.Request) error {
}
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_pixivisionArticle{
Article: data,
})
@@ -82,6 +93,10 @@ func PixivisionCategoryPage(w http.ResponseWriter, r *http.Request) error {
return err
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_pixivisionCategory{
Category: data,
Page: pageint,
@@ -111,6 +126,10 @@ func PixivisionTagPage(w http.ResponseWriter, r *http.Request) error {
return err
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_pixivisionTag{
Tag: data,
Page: pageint,
+6
View File
@@ -1,10 +1,12 @@
package routes
import (
"fmt"
"net/http"
"strconv"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
)
@@ -24,6 +26,10 @@ func RankingPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request)
return err
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_rank{
Title: "Ranking",
Page: pageInt,
+5
View File
@@ -7,6 +7,7 @@ import (
"time"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/server/utils"
)
@@ -91,6 +92,10 @@ func RankingCalendarPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.
return err
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
// Prepare and render the template with the calendar data
return RenderHTML(w, r, Data_rankingCalendar{
Title: "Ranking calendar",
+5
View File
@@ -1,6 +1,7 @@
package routes
import (
"fmt"
"net/http"
"net/url"
"strconv"
@@ -60,6 +61,10 @@ func TagPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request) err
PreloadImage(w, tag.Metadata.Image)
PreloadImage(w, tag.Metadata.ImageMaster)
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_tag{
Title: "Results for " + name,
Tag: tag,
+12 -6
View File
@@ -6,6 +6,7 @@ import (
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/server/request_context"
"codeberg.org/vnpower/pixivfe/v2/server/session"
"codeberg.org/vnpower/pixivfe/v2/server/template"
)
@@ -26,15 +27,20 @@ func RenderWithContentType[T any](w http.ResponseWriter, r *http.Request, conten
// NOTE: the ngx_brotli nginx module appears to weaken strong ETags (even though gzipping is fine)
w.Header().Set("ETag", `"`+etag+`"`)
// As a default, set Cache-Control to no-cache, which allows revalidation via ETag
w.Header().Set("Cache-Control", "no-cache")
// Set Vary: Cookie to prevent personalized content from being shared between users in a shared cache
// Set Cache-Control to private as a safe default while allowing revalidation via ETag
//
// TODO: a better approach is to set the "private" Cache-Control directive on personalized content directly,
// but this works as a stopgap solution
// The private directive is always set if the user is logged in to prevent personalized
// content from being shared between users in a shared cache [1]
if w.Header().Get("Cache-Control") == "" || session.GetUserToken(r) != "" {
w.Header().Set("Cache-Control", "private, max-age=5, stale-while-revalidate=10")
}
// Set Vary: Cookie to prevent user preferences from affecting the shared cache [2]
w.Header().Set("Vary", "Cookie")
// [1], [2]: these options negatively affect HTTP cache hit rate, but we don't have the option of
// client-side hydration via JS nor ESI so these will have to do
w.WriteHeader(request_context.Get(r).RenderStatusCode)
return nil
+12
View File
@@ -1,11 +1,13 @@
package routes
import (
"fmt"
"net/http"
"strconv"
"time"
"codeberg.org/vnpower/pixivfe/v2/audit"
"codeberg.org/vnpower/pixivfe/v2/config"
"codeberg.org/vnpower/pixivfe/v2/core"
"codeberg.org/vnpower/pixivfe/v2/server/template"
"go.uber.org/zap"
@@ -81,6 +83,11 @@ func UserPage(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request) er
zap.Int("pageLimit", data.category.PageLimit),
)
// TODO: need to handle following status
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderHTML(w, r, Data_user{
Title: data.user.Name,
User: data.user,
@@ -96,6 +103,11 @@ func UserAtomFeed(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request
return err
}
// TODO: need to handle following status
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d",
int(config.GlobalConfig.CacheControlMaxAge.Seconds()),
int(config.GlobalConfig.CacheControlStaleWhileRevalidate.Seconds())))
return RenderWithContentType(w, r, "application/atom+xml", Data_userAtom{
URL: r.RequestURI,
Title: data.user.Name,