httpcache

package module
v2.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

README

httpcache-kit

Go Reference Go Report Card License

中文文档

An RFC 7234-compliant HTTP cache handler for Go. Wraps any http.Handler — typically a httputil.ReverseProxy — with memory or disk storage, Cache-Control parsing, conditional revalidation, RFC 7234 invalidation and optional Prometheus metrics.

Evolved from lox/httpcache (MIT).

Features

  • RFC 7234 caching: freshness, heuristic expiration, revalidation, Vary
  • Invalidation: an unsafe method invalidates the request URI and the URIs named by the response's Location / Content-Location
  • Private and shared profiles: a shared cache refuses private responses and strips private headers
  • Memory, disk and VFS backends: disk storage goes through vfs-kit
  • Bounded: configurable TTL, max size, cleanup interval, LRU eviction
  • Observable: optional Prometheus metrics via metrics-kit, debug logging via logger-kit
  • Graceful shutdown: background cache writes are tracked and can be awaited

Requirements

  • Go 1.27+ (go.mod declares go 1.27.0)
  • github.com/prometheus/client_golang for metrics

The v2 module line uses the Fiber v3-compatible logger-kit/v2 and metrics-kit/v2 types exposed by the cache API. Applications still on the v1 kit ecosystem should remain on github.com/soulteary/httpcache-kit v1.

Installation

go get github.com/soulteary/httpcache-kit/v2

Quick Start

Shared cache (reverse proxy)
package main

import (
    "log"
    "net/http"
    "net/http/httputil"

    httpcache "github.com/soulteary/httpcache-kit/v2"
)

func main() {
    proxy := &httputil.ReverseProxy{
        Director: func(r *http.Request) {},
    }

    // NewSharedHandler, not NewHandler: this cache serves more than one user.
    handler := httpcache.NewSharedHandler(httpcache.NewMemoryCache(), proxy)

    log.Print("proxy listening on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}
Private cache (single user)
handler := httpcache.NewHandler(httpcache.NewMemoryCache(), upstream)
Disk-backed, with options
cache, err := httpcache.NewDiskCacheWithConfig("/var/cache/myproxy",
    httpcache.DefaultCacheConfig().
        WithMaxSize(2 * 1024 * 1024 * 1024).
        WithTTL(24 * time.Hour).
        WithCleanupInterval(30 * time.Minute))
if err != nil {
    log.Fatal(err)
}
defer cache.Close()

handler := httpcache.NewHandlerWithOptions(cache, proxy, &httpcache.HandlerOptions{
    Logger: myLogger,
})
handler.Shared = true

Shared vs Private

This is the one decision to get right, because the unsafe default is the one a field can be forgotten in.

NewHandler NewSharedHandler
Handler.Shared false true
Correct for a per-user cache a reverse proxy, any cache serving more than one user
Cache-Control: private stored not stored
Responses to authorized requests stored not stored unless marked public or carrying s-maxage
Headers listed in private kept stripped before storing
s-maxage ignored honoured over max-age

A shared cache running with Shared: false will serve one user's private response to another. Prefer the constructor over setting the field — a constructor cannot be forgotten.

Backends

httpcache.NewMemoryCache()                              // Cache
httpcache.NewMemoryCacheWithConfig(cfg)                 // ExtendedCache
httpcache.NewDiskCache("/var/cache/x")                  // Cache
httpcache.NewDiskCacheWithConfig("/var/cache/x", cfg)   // ExtendedCache
httpcache.NewVFSCache(fs)                               // Cache
httpcache.NewVFSCacheWithConfig(fs, cfg)                // ExtendedCache

Cache is the minimum the handler needs:

type Cache interface {
    Header(key string) (Header, error)
    Retrieve(key string) (*Resource, error)
    Store(res *Resource, keys ...string) error
    Freshen(res *Resource, keys ...string) error
    Invalidate(keys ...string)
}

ExtendedCache adds management, and is what the *WithConfig constructors return:

type ExtendedCache interface {
    Cache
    Stats() CacheStats
    Cleanup() CleanupResult
    Purge() error
    Close() error
}
stats := cache.Stats()
log.Printf("items=%d bytes=%d hits=%d misses=%d stale=%d",
    stats.ItemCount, stats.TotalSize, stats.HitCount, stats.MissCount, stats.StaleCount)

result := cache.Cleanup()
log.Printf("removed %d items (%d bytes, %d stale markers) in %s",
    result.RemovedItems, result.RemovedBytes, result.RemovedStaleEntries, result.Duration)

Retrieve returns ErrNotFoundInCache for a miss — match it with errors.Is. A *Resource it returns owns a file handle on the disk backend, so close it.

On-disk layout

The disk backend keeps four things under its directory:

body/v1/<hashed-key>      response body
header/v1/<hashed-key>    status line, headers, and the store timestamp
staging/v1/               entries being written, empty when idle
stale-markers.json        invalidation state

An entry is its body plus its header, and the two are published together by rename, so a reader sees the whole previous entry or the whole new one. That holds between goroutines sharing one live cache, and no further:

  • The two renames are sequential. A process that exits between them leaves the new body beside the old header, and startup removes the leftover staging file rather than repairing the pair.
  • The lock is per-instance. Two caches opened on one directory do not coordinate, and their publications can interleave.

Nothing under staging/v1 is a cache entry: it is never scanned, and whatever an interrupted process left there is removed at startup.

Only body/v1 and header/v1 count toward MaxSize and Stats().TotalSize. The other Stats() fields are independent of these directories — StaleCount tracks stale-markers.json, and HitCount/MissCount are counters.

Configuration

cfg := httpcache.DefaultCacheConfig().
    WithMaxSize(10 * 1024 * 1024 * 1024).
    WithTTL(7 * 24 * time.Hour).
    WithCleanupInterval(1 * time.Hour).
    WithStaleMapTTL(24 * time.Hour).
    Validate()
Option Default Notes
MaxSize DefaultMaxCacheSize (10 GiB) 0 means unbounded; LRU eviction above it
TTL DefaultCacheTTL (7 days) 0 means no TTL
CleanupInterval DefaultCleanupInterval (1 hour) 0 disables the background cycle
StaleMapTTL DefaultStaleMapTTL (24 hours) how long the backend remembers a stale marker

Validate() clamps negatives to zero and restores StaleMapTTL to its default if it is non-positive. It mutates and returns the same config, so it chains.

Handler options
handler := httpcache.NewHandlerWithOptions(cache, upstream, &httpcache.HandlerOptions{
    Logger:         myLogger,          // *logger.Logger; nil uses the package logger
    StaleMarkerTTL: 7 * 24 * time.Hour,
})

StaleMarkerTTL is how long the handler remembers an invalidation for a Cache that does not track staleness itself. It must be at least as long as that cache retains entries: the marker is the only record that older entries are pre-mutation, so expiring it first republishes them as fresh. Zero means DefaultCacheTTL. A cache that keeps its own record ignores this.

Cache Keys and Vary

A key is derived from the effective request URI and the method:

key := httpcache.NewRequestKey(r)           // from a request
key = httpcache.NewKey("GET", u, r.Header)  // explicitly
key = key.ForMethod("HEAD")                 // the sibling key for another method
key = key.Vary(resp.Header.Get("Vary"), r)  // the variant key
keyString := key.String()

The request's own Content-Location does not affect the key. RFC 7234 uses Content-Location, but the response's, and only for invalidation — letting a request choose its key allows a client to park its response under another URL's key, or read another URL's entry.

Key.String() is injective: the Vary section uses a control-byte separator with each value quoted, and since url.URL.String percent-encodes control bytes while strconv.Quote escapes them, neither side of the boundary can contain the delimiter. Keys without Vary are plain and unchanged.

Invalidation

An unsafe method (POST, PUT, DELETE, PATCH) invalidates, per RFC 7234 section 4.4:

  • the effective request URI,
  • the URI in the response's Location header,
  • the URI in the response's Content-Location header,

for both the GET and HEAD keys. Cross-origin targets are ignored, so a response cannot evict another origin's entries.

Metrics

import metrics "github.com/soulteary/metrics-kit/v2"

registry := metrics.NewRegistry("myproxy")
m := httpcache.NewCacheMetrics(registry)
handler.SetMetrics(m)

// Or register a process-wide default
httpcache.SetDefaultMetrics(m)
m = httpcache.GetDefaultMetrics()

// Feed gauges from a cache's own view
m.UpdateCacheStats(cache.Stats())

CacheMetrics exposes hits, misses, skips, evictions, store and retrieve operations, item count, size in bytes, stale count, cleanup duration, and upstream duration and errors.

Logging

import logger "github.com/soulteary/logger-kit/v2"

httpcache.SetLogger(myLogger)     // package-level logger
httpcache.SetDebugLogging(true)   // verbose cache decisions
on := httpcache.IsDebugLogging()

Response Headers

Header Values Meaning
X-Cache HIT served from cache
MISS fetched from upstream and stored
SKIP not cacheable, or cache bypassed
Proxy-Date HTTP-date when this cache received the response

Cache-Control

cc, err := httpcache.ParseCacheControl("max-age=3600, s-maxage=60, private")
cc, err = httpcache.ParseCacheControlHeaders(resp.Header)

cc.Has("no-store")
value, ok := cc.Get("max-age")
d, err := cc.Duration("max-age")
cc.Add("stale-while-revalidate", "30")
header := cc.String()

Resources

res := httpcache.NewResourceBytes(200, body, header)
res = httpcache.NewResource(200, readSeekCloser, header)

res.Status()
res.Header()
res.Age()
res.Expires()
res.MaxAge(shared)
res.HasExplicitExpiration()
res.HeuristicFreshness()
res.HasValidators()
res.MustValidate(shared)
res.IsStale()
res.MarkStale()
res.LastModified()
res.RemovePrivateHeaders()
res.Via()

Graceful Shutdown

The handler stores responses in the background. Await them before closing the backend, or an in-flight write hits a closed cache:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := handler.Shutdown(ctx); err != nil {
    log.Printf("cache shutdown: %v", err)
}
cache.Close()

httpcache.Writes is a package-level sync.WaitGroup covering every handler's background writes, for tests that need to wait on all of them.

Caveats

  • Conditional requests carrying Range are not cached.
  • Clock is a package-level variable, swappable in tests.

Upgrade Notes (v2.5.0)

No API was added, removed or changed. The disk backend writes entries differently, and there is a new directory inside the cache directory.

  • Entries are published by rename on the disk backend. The previous write opened the target with O_CREATE|O_TRUNC, so new contents were never visible as a unit: on a first store an empty file appeared before any bytes reached it, and on a re-store a perfectly readable entry was emptied for as long as the copy took. Replacing a 128 KiB entry under eight concurrent readers produced 2 misses and 258 reads that saw neither the old nor the new body in full; a watcher caught the header file empty 4543 times across 200 re-stores. A reader sharing the cache now sees the whole previous entry or the whole new one — see the on-disk layout section for the two limits on that, a crash between the two renames and two instances on one directory.
  • A body and its header change together. Retrieve opens the body first and reads the header second, so a publish landing between those two steps used to hand back one version's payload with the other's status and headers — a Content-Length or Content-Encoding describing a body that was no longer there. Under load, 148 of 600 retrievals mismatched. Both files are now renamed under one lock that Retrieve holds across both lookups.
  • staging/v1 is new inside the cache directory. Entries are written there before being renamed into place. It is outside everything that is scanned, so nothing in it is ever mistaken for an entry, and it is swept at startup — a process killed mid-write leaves a file behind that nothing else would reclaim. If you size, back up or rsync the cache directory, include it; it is empty when the cache is idle.
  • Storing costs more, mostly on small entries. Measured per store against v2.4.0: 4 KiB went 193µs → 967µs, 2 MiB went 1.66ms → 2.03ms. Large entries are close to free; the small-entry cost is the extra syscalls against a very short write. Numbers are from a container filesystem and will differ elsewhere.
  • Entry files are not fsynced. Rename is what makes publication atomic; durability across a crash is not what a cache needs, and an entry lost that way is a miss. stale-markers.json is still fsynced, because losing invalidation state would republish superseded content.
  • Memory and other VFS backends are unchanged. The VFS interface has no rename, so they keep the in-place write. The header-completeness check added in v2.4.0 still turns that window into a miss for them.

Upgrade Notes (v2.2.0)

This release changes which entries are served and how they are keyed. One constructor and one option were added; nothing was removed.

  • Invalidation now happens. invalidateResource()'s entire body was a debug log call, so a resource that had been POSTed to, PUT or DELETEd kept being served from cache until its own freshness lifetime expired. It now invalidates the request URI and the same-origin URIs named by the response's Location and Content-Location, for both the GET and HEAD keys. Expect more upstream traffic after unsafe methods — that is the bug being fixed.
  • A request's Content-Location no longer selects the cache key. It did, while the upstream request still used the original URL, so a client could park its own response under a different URL's key (shared cache poisoning) or read another URL's entry. If you deliberately relied on that to alias entries, there is no replacement — it was not a feature.
  • Key.String() changed for keys with Vary. It joined a raw URL and raw header values with ":" and "::", so a crafted URL could collide with a different URL carrying Vary values. The Vary section now uses a quoted, control-byte-separated encoding. Existing cached entries with Vary will miss once and be re-fetched; keys without Vary are unchanged.
  • NewSharedHandler is the constructor for a reverse proxy. Shared defaults to false, which is the correct private-cache profile and the wrong one for a shared cache — and a field can be forgotten in a way a constructor cannot. If you set handler.Shared = true by hand, nothing breaks; new code should use the constructor.
  • The disk backend no longer leaks a file handle per Vary lookup. The primary Resource was overwritten without being closed.
  • CacheControl.String() no longer emits empty entries. It allocated its key slice with make([]string, len(cc)) and then appended, so the output began with len(cc) empty fields.
  • HandlerOptions.StaleMarkerTTL is new. Set it to at least your cache's retention when the cache does not track staleness itself; otherwise an expired marker republishes pre-mutation entries as fresh.

Testing

go test ./...

# With coverage
go test ./... -coverprofile=coverage.out -covermode=atomic
go tool cover -func=coverage.out

References

License

Apache License 2.0 — see LICENSE. Portions derive from lox/httpcache, MIT licensed.

Documentation

Index

Constants

View Source
const (
	CacheHeader     = "X-Cache"
	ProxyDateHeader = "Proxy-Date"
)
View Source
const (
	CacheControlHeader = "Cache-Control"
)
View Source
const DefaultCacheTTL = 7 * 24 * time.Hour

DefaultCacheTTL is the default TTL for cached items (7 days)

View Source
const DefaultCleanupInterval = 1 * time.Hour

DefaultCleanupInterval is the default interval for cache cleanup (1 hour)

View Source
const DefaultMaxCacheSize int64 = 10 * 1024 * 1024 * 1024

DefaultMaxCacheSize is the default maximum cache size (10 GB)

View Source
const DefaultStaleMapTTL = 24 * time.Hour

DefaultStaleMapTTL is the default TTL for stale map entries (24 hours)

Variables

View Source
var Clock = func() time.Time {
	return time.Now().UTC()
}
View Source
var ErrNotFoundInCache = errors.New("not found in cache")

Returned when a resource doesn't exist

Functions

func IsDebugLogging

func IsDebugLogging() bool

IsDebugLogging returns whether debug messages are logged. Safe for concurrent use.

func SetDebugLogging

func SetDebugLogging(b bool)

SetDebugLogging sets whether debug messages are logged. Safe for concurrent use.

func SetDefaultMetrics

func SetDefaultMetrics(m *CacheMetrics)

SetDefaultMetrics sets the default metrics instance (e.g. for tests).

func SetLogger

func SetLogger(log *logger.Logger)

SetLogger sets the package-level logger used when Handler has no Logger injected. Prefer passing Logger via NewHandlerWithOptions; SetLogger is retained for backward compatibility.

Types

type Cache

type Cache interface {
	Header(key string) (Header, error)
	Store(res *Resource, keys ...string) error
	Retrieve(key string) (*Resource, error)
	Invalidate(keys ...string)
	Freshen(res *Resource, keys ...string) error
}

func NewDiskCache

func NewDiskCache(dir string) (Cache, error)

NewDiskCache returns a disk-backed cache

func NewMemoryCache

func NewMemoryCache() Cache

NewMemoryCache returns an ephemeral cache in memory

func NewVFSCache

func NewVFSCache(fs vfs.VFS) Cache

NewVFSCache returns a cache backend off the provided VFS

type CacheConfig

type CacheConfig struct {
	// MaxSize is the maximum size of the cache in bytes.
	// When exceeded, the oldest items will be evicted (LRU).
	// Set to 0 to disable size-based eviction.
	MaxSize int64

	// TTL is the time-to-live for cached items.
	// Items older than this will be evicted during cleanup.
	// Set to 0 to disable TTL-based eviction.
	TTL time.Duration

	// CleanupInterval is the interval between automatic cleanup runs.
	// Set to 0 to disable automatic cleanup.
	CleanupInterval time.Duration

	// StaleMapTTL is the TTL for stale map entries.
	// Stale entries older than this will be removed.
	StaleMapTTL time.Duration
}

CacheConfig holds configuration for cache behavior

func DefaultCacheConfig

func DefaultCacheConfig() *CacheConfig

DefaultCacheConfig returns a CacheConfig with sensible defaults

func (*CacheConfig) Validate

func (c *CacheConfig) Validate() *CacheConfig

Validate validates the configuration and applies defaults where needed

func (*CacheConfig) WithCleanupInterval

func (c *CacheConfig) WithCleanupInterval(interval time.Duration) *CacheConfig

WithCleanupInterval sets the cleanup interval

func (*CacheConfig) WithMaxSize

func (c *CacheConfig) WithMaxSize(size int64) *CacheConfig

WithMaxSize sets the maximum cache size

func (*CacheConfig) WithStaleMapTTL

func (c *CacheConfig) WithStaleMapTTL(ttl time.Duration) *CacheConfig

WithStaleMapTTL sets the stale map TTL

func (*CacheConfig) WithTTL

func (c *CacheConfig) WithTTL(ttl time.Duration) *CacheConfig

WithTTL sets the TTL for cached items

type CacheControl

type CacheControl map[string][]string

func ParseCacheControl

func ParseCacheControl(input string) (CacheControl, error)

func ParseCacheControlHeaders

func ParseCacheControlHeaders(h http.Header) (CacheControl, error)

func (CacheControl) Add

func (cc CacheControl) Add(key, val string)

func (CacheControl) Duration

func (cc CacheControl) Duration(key string) (time.Duration, error)

func (CacheControl) Get

func (cc CacheControl) Get(key string) (string, bool)

func (CacheControl) Has

func (cc CacheControl) Has(key string) bool

func (CacheControl) String

func (cc CacheControl) String() string

type CacheMetrics

type CacheMetrics struct {
	// CacheHits tracks the number of cache hits
	CacheHits *prometheus.CounterVec

	// CacheMisses tracks the number of cache misses
	CacheMisses *prometheus.CounterVec

	// CacheSkips tracks the number of cache skips (non-cacheable requests)
	CacheSkips prometheus.Counter

	// UpstreamDuration tracks the duration of upstream requests
	UpstreamDuration *prometheus.HistogramVec

	// CacheSizeBytes tracks the current cache size in bytes (gauge)
	CacheSizeBytes prometheus.Gauge

	// CacheItemCount tracks the current number of cached items (gauge)
	CacheItemCount prometheus.Gauge

	// CacheStaleCount tracks the current number of stale map entries (gauge)
	CacheStaleCount prometheus.Gauge

	// UpstreamErrors tracks the number of upstream errors
	UpstreamErrors *prometheus.CounterVec

	// CacheStoreOperations tracks cache store operations
	CacheStoreOperations *prometheus.CounterVec

	// CacheRetrieveOperations tracks cache retrieve operations
	CacheRetrieveOperations *prometheus.CounterVec

	// CacheEvictions tracks the number of cache evictions
	CacheEvictions *prometheus.CounterVec

	// CacheCleanupDuration tracks the duration of cleanup operations
	CacheCleanupDuration prometheus.Histogram
}

CacheMetrics holds Prometheus metrics for cache operations

func GetDefaultMetrics

func GetDefaultMetrics() *CacheMetrics

GetDefaultMetrics returns the current default metrics instance (nil until initialized).

func NewCacheMetrics

func NewCacheMetrics(registry *metrics.Registry) *CacheMetrics

NewCacheMetrics creates and registers cache metrics with the given registry

func (*CacheMetrics) RecordCacheEviction

func (m *CacheMetrics) RecordCacheEviction(reason string)

RecordCacheEviction records a cache eviction

func (*CacheMetrics) RecordCacheHit

func (m *CacheMetrics) RecordCacheHit(method string)

RecordCacheHit records a cache hit

func (*CacheMetrics) RecordCacheMiss

func (m *CacheMetrics) RecordCacheMiss(method string)

RecordCacheMiss records a cache miss

func (*CacheMetrics) RecordCacheSkip

func (m *CacheMetrics) RecordCacheSkip()

RecordCacheSkip records a cache skip

func (*CacheMetrics) RecordCleanupDuration

func (m *CacheMetrics) RecordCleanupDuration(durationSeconds float64)

RecordCleanupDuration records the duration of a cleanup operation

func (*CacheMetrics) RecordRetrieveOperation

func (m *CacheMetrics) RecordRetrieveOperation(found bool)

RecordRetrieveOperation records a cache retrieve operation

func (*CacheMetrics) RecordStoreOperation

func (m *CacheMetrics) RecordStoreOperation(success bool)

RecordStoreOperation records a cache store operation

func (*CacheMetrics) RecordUpstreamDuration

func (m *CacheMetrics) RecordUpstreamDuration(method string, status int, durationSeconds float64)

RecordUpstreamDuration records the duration of an upstream request

func (*CacheMetrics) RecordUpstreamError

func (m *CacheMetrics) RecordUpstreamError(errorType string)

RecordUpstreamError records an upstream error

func (*CacheMetrics) SetCacheItemCount

func (m *CacheMetrics) SetCacheItemCount(count int)

SetCacheItemCount sets the current number of cached items

func (*CacheMetrics) SetCacheSize

func (m *CacheMetrics) SetCacheSize(sizeBytes int64)

SetCacheSize sets the current cache size in bytes

func (*CacheMetrics) SetCacheStaleCount

func (m *CacheMetrics) SetCacheStaleCount(count int)

SetCacheStaleCount sets the current number of stale map entries

func (*CacheMetrics) UpdateCacheStats

func (m *CacheMetrics) UpdateCacheStats(stats CacheStats)

UpdateCacheStats updates all cache gauge metrics from stats

type CacheStats

type CacheStats struct {
	// TotalSize is the total size of cached items in bytes
	TotalSize int64
	// ItemCount is the number of cached items
	ItemCount int
	// StaleCount is the number of stale map entries
	StaleCount int
	// HitCount is the number of cache hits
	HitCount int64
	// MissCount is the number of cache misses
	MissCount int64
}

CacheStats holds cache statistics

type CleanupResult

type CleanupResult struct {
	// RemovedItems is the number of items removed
	RemovedItems int
	// RemovedBytes is the number of bytes freed
	RemovedBytes int64
	// RemovedStaleEntries is the number of stale map entries removed
	RemovedStaleEntries int
	// Duration is how long the cleanup took
	Duration time.Duration
}

CleanupResult holds the result of a cleanup operation

type ExtendedCache

type ExtendedCache interface {
	Cache
	// Stats returns current cache statistics
	Stats() CacheStats
	// Cleanup runs a manual cleanup cycle
	Cleanup() CleanupResult
	// Purge removes all cached items
	Purge() error
	// Close stops the cache and cleanup goroutines
	Close() error
}

ExtendedCache extends Cache with management capabilities

func NewDiskCacheWithConfig

func NewDiskCacheWithConfig(dir string, config *CacheConfig) (ExtendedCache, error)

NewDiskCacheWithConfig returns a disk-backed cache with custom configuration

func NewMemoryCacheWithConfig

func NewMemoryCacheWithConfig(config *CacheConfig) ExtendedCache

NewMemoryCacheWithConfig returns an ephemeral cache with custom configuration

func NewVFSCacheWithConfig

func NewVFSCacheWithConfig(fs vfs.VFS, config *CacheConfig) ExtendedCache

NewVFSCacheWithConfig returns a cache backend with custom configuration

type Handler

type Handler struct {
	// Shared reports whether this cache is shared between users (a reverse
	// proxy) rather than private to one (a browser-style cache).
	//
	// It defaults to false, which is the *private* cache profile: responses
	// marked "Cache-Control: private" and responses to requests carrying an
	// Authorization header are storable. Deploying a shared cache without
	// setting this serves one user's response to another. Prefer
	// NewSharedHandler, which cannot be forgotten.
	Shared bool
	// contains filtered or unexported fields
}

func NewHandler

func NewHandler(cache Cache, upstream http.Handler) *Handler

NewHandler returns a private cache handler with default options (package-level logger). Use NewSharedHandler for a cache that is shared between users, such as a reverse proxy.

func NewHandlerWithOptions

func NewHandlerWithOptions(cache Cache, upstream http.Handler, opts *HandlerOptions) *Handler

NewHandlerWithOptions returns a cache handler with the given options. If opts.Logger is set, it is used for handler logging; otherwise the package-level logger is used.

func NewSharedHandler added in v2.2.0

func NewSharedHandler(cache Cache, upstream http.Handler) *Handler

NewSharedHandler returns a cache handler configured as a shared cache: it refuses to store "Cache-Control: private" responses and responses to authorized requests unless they are explicitly marked public or carry s-maxage, and it strips headers listed in "private" before storing.

This is the constructor to use for a reverse proxy. NewHandler leaves Shared false, which is correct for a per-user cache and unsafe for a shared one.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request)

func (*Handler) SetMetrics

func (h *Handler) SetMetrics(m *CacheMetrics)

SetMetrics sets the metrics instance for the handler

func (*Handler) Shutdown added in v2.0.1

func (h *Handler) Shutdown(ctx context.Context) error

Shutdown prevents new background cache writes and waits for writes already in progress. The caller should invoke it before closing the cache backend.

type HandlerOptions

type HandlerOptions struct {
	Logger *logger.Logger

	// StaleMarkerTTL is how long the handler remembers an invalidation for a
	// Cache that does not implement StaleAt itself.
	//
	// It must be at least as long as that Cache retains entries: the marker is
	// the only record that entries older than it are pre-mutation, so expiring
	// it first republishes them as fresh. Only the implementation knows its
	// own retention, which is why this is a knob rather than a constant.
	//
	// Zero means DefaultCacheTTL. A Cache that implements StaleAt keeps its
	// own record and ignores this.
	StaleMarkerTTL time.Duration
}

HandlerOptions holds optional configuration for NewHandlerWithOptions. Logger injected here is used by the handler for all debug/error logging; if nil, the package-level logger (see SetLogger) is used.

type Header struct {
	http.Header
	StatusCode int
}

type Key

type Key struct {
	// contains filtered or unexported fields
}

Key represents a unique identifier for a resource in the cache

func NewKey

func NewKey(method string, u *url.URL, h http.Header) Key

NewKey returns a new Key instance

func NewRequestKey

func NewRequestKey(r *http.Request) Key

NewRequestKey generates a Key for a request.

The key is derived from the effective request URI only. A request header must never be able to choose which cache entry a response is stored under: letting the *request's* Content-Location pick the key allowed a client to park its own response under another URL's key ("GET /attacker-page" with "Content-Location: /admin"), poisoning a shared cache for every other user.

RFC 7234 does use Content-Location, but the *response's* — and only to invalidate entries, never to select a storage key. See invalidationKeys.

func (Key) ForMethod

func (k Key) ForMethod(method string) Key

ForMethod returns a new Key with a given method

func (Key) String

func (k Key) String() string

func (Key) Vary

func (k Key) Vary(varyHeader string, r *http.Request) Key

Vary returns a Key that is varied on particular headers in a http.Request

type ReadSeekCloser

type ReadSeekCloser interface {
	io.Reader
	io.Seeker
	io.Closer
}

type Resource

type Resource struct {
	ReadSeekCloser
	RequestTime, ResponseTime time.Time
	// contains filtered or unexported fields
}

func NewResource

func NewResource(statusCode int, body ReadSeekCloser, hdrs http.Header) *Resource

func NewResourceBytes

func NewResourceBytes(statusCode int, b []byte, hdrs http.Header) *Resource

func (*Resource) Age

func (r *Resource) Age() (time.Duration, error)

Calculate the age of the resource

func (*Resource) DateAfter

func (r *Resource) DateAfter(d time.Time) bool

func (*Resource) Expires

func (r *Resource) Expires() (time.Time, error)

func (*Resource) HasExplicitExpiration

func (r *Resource) HasExplicitExpiration() bool

func (*Resource) HasValidators

func (r *Resource) HasValidators() bool

func (*Resource) Header

func (r *Resource) Header() http.Header

func (*Resource) HeuristicFreshness

func (r *Resource) HeuristicFreshness() time.Duration

func (*Resource) IsNonErrorStatus

func (r *Resource) IsNonErrorStatus() bool

func (*Resource) IsStale

func (r *Resource) IsStale() bool

func (*Resource) LastModified

func (r *Resource) LastModified() time.Time

func (*Resource) MarkStale

func (r *Resource) MarkStale()

func (*Resource) MaxAge

func (r *Resource) MaxAge(shared bool) (time.Duration, error)

func (*Resource) MustValidate

func (r *Resource) MustValidate(shared bool) bool

func (*Resource) ReceivedAfter added in v2.2.0

func (r *Resource) ReceivedAfter(d time.Time) bool

func (*Resource) RemovePrivateHeaders

func (r *Resource) RemovePrivateHeaders()

func (*Resource) SetStoredAt added in v2.2.0

func (r *Resource) SetStoredAt(t time.Time)

ReceivedAfter reports whether this response reached the cache after d.

It reads Proxy-Date, which the handler stamps from the LOCAL clock the moment the upstream response arrives, and only falls back to the origin's Date for a response stored without one.

Deciding this from Date alone was wrong in both directions. A response that carries no Date at all -- which a direct http.Handler upstream may well omit -- never counted as superseding anything, so an invalidated key stayed stale on every retrieval and was refetched until the marker was swept. An origin whose clock trails the cache's did the same. Proxy-Date is always present and always this machine's clock.

Receive time, not store time: an older store still in flight when a mutation lands carries a Proxy-Date from BEFORE the invalidation, so the marker still wins and the pre-mutation body is not republished as fresh.

Both stamps are HTTP dates, so this is second-granular: a replacement that arrives in the same second as the invalidation does not count as superseding it and is refetched once more. That is the safe direction to round in. SetStoredAt records when this cache wrote the entry.

func (*Resource) Status

func (r *Resource) Status() int

func (*Resource) StoredAfter added in v2.2.0

func (r *Resource) StoredAfter(d time.Time) bool

StoredAfter reports whether this cache's copy is newer than d.

It prefers the cache's OWN store time to the response's dates, because Date and Proxy-Date are HTTP dates: one-second granularity, formatted with http.TimeFormat. An invalidation marker is a full-precision time.Time, so a response stored or revalidated in the same second as the invalidation is never "after" it -- and being judged against a marker it can never clear, the entry was re-marked stale and revalidated upstream on EVERY request until the marker was swept. The store time is also ours rather than the origin's, so a trailing origin clock cannot produce the same deadlock.

Falls back to the header dates for a resource that did not come from this cache, which is the only thing available for one.

func (*Resource) Via

func (r *Resource) Via() string

type Validator

type Validator struct {
	Handler http.Handler
}

func (*Validator) Validate

func (v *Validator) Validate(req *http.Request, res *Resource) bool

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL