cache.go

  1package middleware
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"strings"
  7	"time"
  8)
  9
 10// MaxCacheAge is the maximum cache age for responses, set to 1 year.
 11const MaxCacheAge = 365 * 24 * time.Hour // 1 year in seconds
 12
 13type cacheOptions struct {
 14	vary      []string
 15	immutable bool
 16	private   bool
 17	noStore   bool
 18}
 19
 20// CacheOption configures the behavior of the Cache middleware.
 21type CacheOption func(*cacheOptions)
 22
 23// WithVary adds the specified header names to the Vary response header.
 24// This instructs caches to store separate versions of the response based
 25// on the value of these request headers. Common values include
 26// "Accept-Encoding" and "Accept-Language".
 27func WithVary(vary ...string) CacheOption {
 28	return func(opts *cacheOptions) {
 29		opts.vary = vary
 30	}
 31}
 32
 33// WithImmutable adds the immutable directive to Cache-Control.
 34// Use this for content-addressed resources (e.g., files with a hash in the URL)
 35// that will never change. This allows browsers to avoid conditional requests
 36// (If-None-Match, If-Modified-Since) for the resource.
 37func WithImmutable() CacheOption {
 38	return func(opts *cacheOptions) {
 39		opts.immutable = true
 40	}
 41}
 42
 43// WithPrivate marks the response as private, preventing shared caches (e.g., CDNs)
 44// from storing the response. Use this for user-specific or account pages.
 45func WithPrivate() CacheOption {
 46	return func(opts *cacheOptions) {
 47		opts.private = true
 48	}
 49}
 50
 51// WithNoStore prevents the response from being stored in any cache.
 52// Use this for dynamic API routes, WebSocket setup endpoints, or any
 53// response that should never be cached by browsers or CDNs.
 54// When used, this directive takes precedence over all other options.
 55func WithNoStore() CacheOption {
 56	return func(opts *cacheOptions) {
 57		opts.noStore = true
 58	}
 59}
 60
 61// Cache returns a middleware that sets Cache-Control and optional Vary headers.
 62//
 63// The max-age directive is derived from the provided duration and capped at
 64// 1 year (31,536,000 seconds), which is the practical maximum supported by
 65// browsers and CDNs like Cloudflare.
 66//
 67// By default, responses are marked as public. Use WithPrivate for responses
 68// that should only be cached by the end user's browser. Use WithNoStore for
 69// responses that must never be cached anywhere.
 70//
 71// Example usage:
 72//
 73//	r.Use(middleware.Cache(1*time.Hour))
 74//	r.Use(middleware.Cache(365*24*time.Hour, middleware.WithImmutable()))
 75//	r.Use(middleware.Cache(5*time.Minute, middleware.WithPrivate()))
 76//	r.Use(middleware.Cache(0, middleware.WithNoStore()))
 77func Cache(d time.Duration, opts ...CacheOption) Middleware {
 78	options := &cacheOptions{}
 79	for _, opt := range opts {
 80		opt(options)
 81	}
 82
 83	// no-store takes precedence over all other directives
 84	if options.noStore {
 85		return func(next http.Handler) http.Handler {
 86			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 87				w.Header().Set("Cache-Control", "no-store")
 88				next.ServeHTTP(w, r)
 89			})
 90		}
 91	}
 92
 93	// Cap max-age at 1 year
 94	if d > MaxCacheAge {
 95		d = MaxCacheAge
 96	}
 97	maxAge := int(d.Seconds())
 98
 99	var directives []string
100	if options.private {
101		directives = append(directives, "private")
102	} else {
103		directives = append(directives, "public")
104	}
105	directives = append(directives, fmt.Sprintf("max-age=%d", maxAge))
106	if options.immutable {
107		directives = append(directives, "immutable")
108	}
109	cacheControl := strings.Join(directives, ", ")
110
111	return func(next http.Handler) http.Handler {
112		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
113			w.Header().Set("Cache-Control", cacheControl)
114			for _, v := range options.vary {
115				w.Header().Add("Vary", v)
116			}
117			next.ServeHTTP(w, r)
118		})
119	}
120}