github.com/gofiber/fiber/v2@v2.47.0/middleware/cache/cache.go (about) 1 // Special thanks to @codemicro for moving this to fiber core 2 // Original middleware: github.com/codemicro/fiber-cache 3 package cache 4 5 import ( 6 "strconv" 7 "strings" 8 "sync" 9 "sync/atomic" 10 "time" 11 12 "github.com/gofiber/fiber/v2" 13 "github.com/gofiber/fiber/v2/utils" 14 ) 15 16 // timestampUpdatePeriod is the period which is used to check the cache expiration. 17 // It should not be too long to provide more or less acceptable expiration error, and in the same 18 // time it should not be too short to avoid overwhelming of the system 19 const timestampUpdatePeriod = 300 * time.Millisecond 20 21 // cache status 22 // unreachable: when cache is bypass, or invalid 23 // hit: cache is served 24 // miss: do not have cache record 25 const ( 26 cacheUnreachable = "unreachable" 27 cacheHit = "hit" 28 cacheMiss = "miss" 29 ) 30 31 // directives 32 const ( 33 noCache = "no-cache" 34 noStore = "no-store" 35 ) 36 37 var ignoreHeaders = map[string]interface{}{ 38 "Connection": nil, 39 "Keep-Alive": nil, 40 "Proxy-Authenticate": nil, 41 "Proxy-Authorization": nil, 42 "TE": nil, 43 "Trailers": nil, 44 "Transfer-Encoding": nil, 45 "Upgrade": nil, 46 "Content-Type": nil, // already stored explicitly by the cache manager 47 "Content-Encoding": nil, // already stored explicitly by the cache manager 48 } 49 50 // New creates a new middleware handler 51 func New(config ...Config) fiber.Handler { 52 // Set default config 53 cfg := configDefault(config...) 54 55 // Nothing to cache 56 if int(cfg.Expiration.Seconds()) < 0 { 57 return func(c *fiber.Ctx) error { 58 return c.Next() 59 } 60 } 61 62 var ( 63 // Cache settings 64 mux = &sync.RWMutex{} 65 timestamp = uint64(time.Now().Unix()) 66 ) 67 // Create manager to simplify storage operations ( see manager.go ) 68 manager := newManager(cfg.Storage) 69 // Create indexed heap for tracking expirations ( see heap.go ) 70 heap := &indexedHeap{} 71 // count stored bytes (sizes of response bodies) 72 var storedBytes uint 73 74 // Update timestamp in the configured interval 75 go func() { 76 for { 77 atomic.StoreUint64(×tamp, uint64(time.Now().Unix())) 78 time.Sleep(timestampUpdatePeriod) 79 } 80 }() 81 82 // Delete key from both manager and storage 83 deleteKey := func(dkey string) { 84 manager.del(dkey) 85 // External storage saves body data with different key 86 if cfg.Storage != nil { 87 manager.del(dkey + "_body") 88 } 89 } 90 91 // Return new handler 92 return func(c *fiber.Ctx) error { 93 // Refrain from caching 94 if hasRequestDirective(c, noStore) { 95 return c.Next() 96 } 97 98 // Only cache selected methods 99 var isExists bool 100 for _, method := range cfg.Methods { 101 if c.Method() == method { 102 isExists = true 103 } 104 } 105 106 if !isExists { 107 c.Set(cfg.CacheHeader, cacheUnreachable) 108 return c.Next() 109 } 110 111 // Get key from request 112 // TODO(allocation optimization): try to minimize the allocation from 2 to 1 113 key := cfg.KeyGenerator(c) + "_" + c.Method() 114 115 // Get entry from pool 116 e := manager.get(key) 117 118 // Lock entry 119 mux.Lock() 120 121 // Get timestamp 122 ts := atomic.LoadUint64(×tamp) 123 124 // Check if entry is expired 125 if e.exp != 0 && ts >= e.exp { 126 deleteKey(key) 127 if cfg.MaxBytes > 0 { 128 _, size := heap.remove(e.heapidx) 129 storedBytes -= size 130 } 131 } else if e.exp != 0 && !hasRequestDirective(c, noCache) { 132 // Separate body value to avoid msgp serialization 133 // We can store raw bytes with Storage 👍 134 if cfg.Storage != nil { 135 e.body = manager.getRaw(key + "_body") 136 } 137 // Set response headers from cache 138 c.Response().SetBodyRaw(e.body) 139 c.Response().SetStatusCode(e.status) 140 c.Response().Header.SetContentTypeBytes(e.ctype) 141 if len(e.cencoding) > 0 { 142 c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding) 143 } 144 if e.headers != nil { 145 for k, v := range e.headers { 146 c.Response().Header.SetBytesV(k, v) 147 } 148 } 149 // Set Cache-Control header if enabled 150 if cfg.CacheControl { 151 maxAge := strconv.FormatUint(e.exp-ts, 10) 152 c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge) 153 } 154 155 c.Set(cfg.CacheHeader, cacheHit) 156 157 mux.Unlock() 158 159 // Return response 160 return nil 161 } 162 163 // make sure we're not blocking concurrent requests - do unlock 164 mux.Unlock() 165 166 // Continue stack, return err to Fiber if exist 167 if err := c.Next(); err != nil { 168 return err 169 } 170 171 // lock entry back and unlock on finish 172 mux.Lock() 173 defer mux.Unlock() 174 175 // Don't cache response if Next returns true 176 if cfg.Next != nil && cfg.Next(c) { 177 c.Set(cfg.CacheHeader, cacheUnreachable) 178 return nil 179 } 180 181 // Don't try to cache if body won't fit into cache 182 bodySize := uint(len(c.Response().Body())) 183 if cfg.MaxBytes > 0 && bodySize > cfg.MaxBytes { 184 c.Set(cfg.CacheHeader, cacheUnreachable) 185 return nil 186 } 187 188 // Remove oldest to make room for new 189 if cfg.MaxBytes > 0 { 190 for storedBytes+bodySize > cfg.MaxBytes { 191 key, size := heap.removeFirst() 192 deleteKey(key) 193 storedBytes -= size 194 } 195 } 196 197 // Cache response 198 e.body = utils.CopyBytes(c.Response().Body()) 199 e.status = c.Response().StatusCode() 200 e.ctype = utils.CopyBytes(c.Response().Header.ContentType()) 201 e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding)) 202 203 // Store all response headers 204 // (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1) 205 if cfg.StoreResponseHeaders { 206 e.headers = make(map[string][]byte) 207 c.Response().Header.VisitAll( 208 func(key, value []byte) { 209 // create real copy 210 keyS := string(key) 211 if _, ok := ignoreHeaders[keyS]; !ok { 212 e.headers[keyS] = utils.CopyBytes(value) 213 } 214 }, 215 ) 216 } 217 218 // default cache expiration 219 expiration := cfg.Expiration 220 // Calculate expiration by response header or other setting 221 if cfg.ExpirationGenerator != nil { 222 expiration = cfg.ExpirationGenerator(c, &cfg) 223 } 224 e.exp = ts + uint64(expiration.Seconds()) 225 226 // Store entry in heap 227 if cfg.MaxBytes > 0 { 228 e.heapidx = heap.put(key, e.exp, bodySize) 229 storedBytes += bodySize 230 } 231 232 // For external Storage we store raw body separated 233 if cfg.Storage != nil { 234 manager.setRaw(key+"_body", e.body, expiration) 235 // avoid body msgp encoding 236 e.body = nil 237 manager.set(key, e, expiration) 238 manager.release(e) 239 } else { 240 // Store entry in memory 241 manager.set(key, e, expiration) 242 } 243 244 c.Set(cfg.CacheHeader, cacheMiss) 245 246 // Finish response 247 return nil 248 } 249 } 250 251 // Check if request has directive 252 func hasRequestDirective(c *fiber.Ctx, directive string) bool { 253 return strings.Contains(c.Get(fiber.HeaderCacheControl), directive) 254 }