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}