44package cache
55
66import (
7+ "path"
78 "strconv"
89 "sync"
910 "sync/atomic"
@@ -18,6 +19,11 @@ const (
1819 // maxLRUItems is the maximum number of entries the underlying LRU may hold.
1920 // Byte-level eviction happens before this is reached in practice.
2021 maxLRUItems = 65536
22+
23+ // HTTPTimeFormat is the time format used for HTTP Date, Last-Modified,
24+ // and Expires headers. It is identical to the value of net/http.TimeFormat
25+ // but defined here to avoid importing net/http in the cache package.
26+ HTTPTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
2127)
2228
2329// CachedFile holds the content and metadata for a single cached file.
@@ -44,20 +50,102 @@ type CachedFile struct {
4450
4551 // Pre-formatted header values avoid per-request string formatting.
4652 // These are populated by InitHeaders() or by the preload path.
47- CTHeader []string // e.g. {"text/html; charset=utf-8"}
48- CLHeader []string // e.g. {"2943"} — raw data Content-Length
53+ // With fasthttp, headers are set via Set(key, value) taking plain strings;
54+ // an empty string means "not initialised".
55+ CTHeader string // e.g. "text/html; charset=utf-8"
56+ CLHeader string // e.g. "2943" — raw data Content-Length
57+
58+ // Pre-formatted cache/conditional headers (PERF-003).
59+ // Populated by InitHeaders(); the serving hot path assigns these
60+ // directly to the response header, skipping all formatting.
61+ ETagHeader string // e.g. `W/"abc123"`
62+ LastModHeader string // e.g. "Mon, 15 Jan 2024 10:00:00 GMT"
63+ VaryHeader string // e.g. "Accept-Encoding"
64+ CacheControlHeader string // e.g. "public, max-age=3600"
4965}
5066
51- // InitHeaders pre-formats the Content-Type and Content-Length header slices
52- // so that the serving hot path can assign them directly to the header map
53- // without allocating. This is idempotent.
67+ // InitHeaders pre-formats the Content-Type, Content-Length, ETag,
68+ // Last-Modified, and Vary header strings so that the serving hot path can
69+ // assign them directly without allocating (PERF-003).
70+ // This is idempotent.
5471func (f * CachedFile ) InitHeaders () {
55- if f .CTHeader == nil {
56- f .CTHeader = []string {f .ContentType }
72+ if f .CTHeader == "" {
73+ f .CTHeader = f .ContentType
74+ }
75+ if f .CLHeader == "" {
76+ f .CLHeader = strconv .FormatInt (f .Size , 10 )
77+ }
78+ if f .ETagHeader == "" {
79+ etag := f .ETagFull
80+ if etag == "" {
81+ etag = `W/"` + f .ETag + `"`
82+ }
83+ f .ETagHeader = etag
84+ }
85+ if f .LastModHeader == "" {
86+ f .LastModHeader = f .LastModified .UTC ().Format (HTTPTimeFormat )
87+ }
88+ if f .VaryHeader == "" {
89+ f .VaryHeader = "Accept-Encoding"
90+ }
91+ }
92+
93+ // InitCacheControl pre-formats the Cache-Control header for a specific URL
94+ // path and header configuration. This must be called after InitHeaders().
95+ // urlPath is used to determine HTML vs static max-age; isHTML reports whether
96+ // the file is an HTML document; immutablePattern is the glob for immutable files.
97+ func (f * CachedFile ) InitCacheControl (urlPath string , htmlMaxAge , staticMaxAge int , immutablePattern string ) {
98+ if f .CacheControlHeader != "" {
99+ return
57100 }
58- if f .CLHeader == nil {
59- f .CLHeader = []string {strconv .FormatInt (f .Size , 10 )}
101+ maxAge := staticMaxAge
102+ if isHTMLContent (urlPath , f .ContentType ) {
103+ maxAge = htmlMaxAge
104+ }
105+ if maxAge == 0 {
106+ f .CacheControlHeader = "no-cache"
107+ } else {
108+ cc := "public, max-age=" + strconv .Itoa (maxAge )
109+ if immutablePattern != "" && matchesImmutable (urlPath , immutablePattern ) {
110+ cc += ", immutable"
111+ }
112+ f .CacheControlHeader = cc
113+ }
114+ }
115+
116+ // isHTMLContent reports whether the given URL path + content type indicates HTML.
117+ func isHTMLContent (urlPath , contentType string ) bool {
118+ if len (contentType ) >= 9 {
119+ // Fast prefix check before calling strings functions.
120+ if contentType [0 ] == 't' && contentType [4 ] == '/' && contentType [5 ] == 'h' {
121+ return true // "text/html..."
122+ }
123+ }
124+ // Fallback to extension check.
125+ for i := len (urlPath ) - 1 ; i >= 0 ; i -- {
126+ if urlPath [i ] == '.' {
127+ ext := urlPath [i :]
128+ return ext == ".html" || ext == ".htm" ||
129+ ext == ".HTML" || ext == ".HTM"
130+ }
131+ }
132+ return false
133+ }
134+
135+ // matchesImmutable checks if the base filename matches the immutable glob.
136+ func matchesImmutable (urlPath , pattern string ) bool {
137+ // Extract base name without filepath.Base allocation.
138+ base := urlPath
139+ if i := len (urlPath ) - 1 ; i >= 0 {
140+ for i >= 0 && urlPath [i ] != '/' {
141+ i --
142+ }
143+ base = urlPath [i + 1 :]
60144 }
145+ // filepath.Match is unavoidable for glob support but is only called
146+ // at cache-population time, never on the hot path.
147+ matched , _ := path .Match (pattern , base )
148+ return matched
61149}
62150
63151// totalSize returns the approximate byte footprint of the entry.
0 commit comments