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(&timestamp, 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(&timestamp)
   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  }