httpccache

package
v0.0.0-...-0b57a39 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package httpccache (http client cache) implements a caching net/http.RoundTripper that stores responses and serves them when fresh, with pluggable storage backends and HTTP cache semantics.

Index

Constants

View Source
const (
	// DefaultCacheStatusHeader is the default response header for cache status.
	DefaultCacheStatusHeader = "X-Cache-Status"
	// DefaultCacheReasonHeader is the default response header for cache reason.
	DefaultCacheReasonHeader = "X-Cache-Reason"
)

Variables

View Source
var DefaultIgnoredCacheKeyHeaders = []string{
	"X-Request-ID",
	"Request-ID",
	"X-Correlation-ID",
	"Correlation-ID",
	"X-Trade-ID",
	"Trade-ID",
	"Traceparent",
	"Tracestate",
	"B3",
	"X-B3-TraceID",
	"X-B3-SpanID",
	"X-B3-ParentSpanID",
	"X-B3-Sampled",
	"X-B3-Flags",
	"X-Amzn-Trace-Id",
}

DefaultIgnoredCacheKeyHeaders are per-request metadata headers ignored by the default canonical cache key to avoid unnecessary cache fragmentation.

View Source
var ErrNotFound = errors.New("httpccache: record not found")

ErrNotFound is returned by Storage.Get when the requested key does not exist in the storage backend.

View Source
var ErrReadLimitExceeded = errors.New("httpccache: read limit exceeded")

ErrReadLimitExceeded is returned by NewBodyLimitReader when a read would exceed the configured maximum body size (more bytes remain on the reader).

Functions

func CanonicalCacheKey

func CanonicalCacheKey(req *http.Request, ignoredHeaders []string) string

CanonicalCacheKey builds a stable cache key from a request's method, URL, URL userinfo, and headers while excluding ignored header keys.

func DefaultShouldCacheRequest

func DefaultShouldCacheRequest(req *http.Request) bool

DefaultShouldCacheRequest is the built-in request eligibility check used when Config.ShouldCacheRequest is nil. It returns true for GET and HEAD requests that do not carry conditional headers (If-None-Match, If-Modified-Since, etc.), Range headers, or a Cache-Control: no-store directive. Callers can wrap this function with additional logic and pass the wrapper via Config.ShouldCacheRequest:

cfg.ShouldCacheRequest = func(req *http.Request) bool {
    if req.URL.Host == "internal.api" {
        return false
    }
    return httpccache.DefaultShouldCacheRequest(req)
}

func IsCacheHit

func IsCacheHit(resp *http.Response) bool

IsCacheHit reports if the response was served from cache.

func IsCacheMiss

func IsCacheMiss(resp *http.Response) bool

IsCacheMiss reports if the response came from upstream.

func IsCacheRevalidated

func IsCacheRevalidated(resp *http.Response) bool

IsCacheRevalidated reports if cache revalidation succeeded via 304.

func IsFromCache

func IsFromCache(resp *http.Response) bool

IsFromCache reports if the response body came from cache material.

func NewBodyLimitReader

func NewBodyLimitReader(r io.Reader, limit int64) io.Reader

NewBodyLimitReader wraps r so that at most limit bytes are delivered. After limit bytes have been read, the next read attempts to take one more byte from r; if successful, it returns (0, ErrReadLimitExceeded) instead of EOF.

func NewClient

func NewClient(config *Config) *http.Client

NewClient creates a new http.Client using the cache transport.

func NewTransport

func NewTransport(config *Config) http.RoundTripper

NewTransport creates a new cache round tripper.

Types

type CacheEntry

type CacheEntry struct {
	Key    string `json:"key"`
	Method string `json:"method"`
	URL    string `json:"url"`

	StoredAt  time.Time `json:"stored_at"`
	ExpiresAt time.Time `json:"expires_at"`
	CreatedAt time.Time `json:"created_at"`

	ResponseStatus int         `json:"response_status"`
	ResponseHeader http.Header `json:"response_header"`
	BodySize       int64       `json:"-"` // Set by the storage implementation during Get.

	VaryValues map[string]string `json:"vary_values"`
}

CacheEntry is HTTP cache metadata for a stored response. Body bytes live in Storage beside JSON produced by [CacheEntry.marshalWire].

BodySize is set by the transport after Storage.Get (not part of persisted metadata) for Content-Length on synthesized responses.

ResponseHeader holds the full upstream response headers (including ETag, Last-Modified, Vary). VaryValues records the request header values that were sent when the response was cached; they are not derivable from ResponseHeader and are required for Vary matching.

type CacheReason

type CacheReason string

CacheReason is the machine-readable reason attached to cache decisions.

const (
	ReasonMethodNotCacheable         CacheReason = "method_not_cacheable"
	ReasonRequestNotCacheable        CacheReason = "request_not_cacheable"
	ReasonRequestNoStore             CacheReason = "request_no_store"
	ReasonResponseNoStore            CacheReason = "response_no_store"
	ReasonStatusNotCacheable         CacheReason = "status_not_cacheable"
	ReasonNoExplicitFreshness        CacheReason = "no_explicit_freshness"
	ReasonInvalidFreshness           CacheReason = "invalid_freshness"
	ReasonVaryMismatch               CacheReason = "vary_mismatch"
	ReasonVaryWildcard               CacheReason = "vary_wildcard"
	ReasonStaleRequiresRevalidation  CacheReason = "stale_requires_revalidation"
	ReasonAuthorizationNotAllowed    CacheReason = "authorization_not_allowed"
	ReasonConditionalPassthrough     CacheReason = "conditional_request_passthrough"
	ReasonRangePassthrough           CacheReason = "range_request_passthrough"
	ReasonStorageError               CacheReason = "storage_error"
	ReasonObjectTooLarge             CacheReason = "object_too_large"
	ReasonStored                     CacheReason = "stored"
	ReasonCacheHit                   CacheReason = "cache_hit"
	ReasonCacheMiss                  CacheReason = "cache_miss"
	ReasonOnlyIfCachedUnsatisfied    CacheReason = "only_if_cached_unsatisfied"
	ReasonDecodeEntryFailed          CacheReason = "decode_entry_failed"
	ReasonUpstreamRevalidationFailed CacheReason = "upstream_revalidation_failed"
)

func CacheReasonFromResponse

func CacheReasonFromResponse(resp *http.Response) CacheReason

CacheReasonFromResponse returns the typed cache reason from response headers.

func (CacheReason) IsValid

func (r CacheReason) IsValid() bool

IsValid reports whether the reason is one of the known CacheReason values.

type CacheStatus

type CacheStatus string

CacheStatus is the machine-readable cache status value stored in response headers and decision logs.

const (
	// StatusHit indicates the returned response came directly from cache.
	StatusHit CacheStatus = "hit"
	// StatusMiss indicates an upstream request was required.
	StatusMiss CacheStatus = "miss"
	// StatusRevalidated indicates cache revalidation produced a 304 and the cached
	// object was returned.
	StatusRevalidated CacheStatus = "revalidated"
	// StatusBypass indicates cache logic was skipped for this request.
	StatusBypass CacheStatus = "bypass"
)

func CacheStatusFromResponse

func CacheStatusFromResponse(resp *http.Response) CacheStatus

CacheStatusFromResponse returns the typed cache status from response headers.

func (CacheStatus) IsValid

func (s CacheStatus) IsValid() bool

IsValid reports whether the status is one of the known CacheStatus values.

type Config

type Config struct {
	// BaseTransport is the wrapped upstream transport. Defaults to
	// [net/http.DefaultTransport].
	BaseTransport http.RoundTripper
	// Storage is the cache storage backend. Defaults to [NewMemoryStorage] with
	// 1024 max entries and a 7-day max age.
	Storage Storage

	// Logger is used for cache decision. Defaults to [slog.DiscardHandler].
	Logger *slog.Logger
	// LogLevel is the [slog.Level] used by the built-in decision log. Defaults
	// to [slog.LevelDebug].
	LogLevel *slog.Level

	// LogDecisionFunc is called whenever the cache makes a hit/miss/bypass
	// decision. Defaults to a structured log call at LogLevel.
	LogDecisionFunc func(ctx context.Context, logger *slog.Logger, req *http.Request, decision CacheStatus, reason CacheReason, attrs ...slog.Attr)

	// ShouldCacheRequest decides if a request is eligible for caching. Defaults
	// to [DefaultShouldCacheRequest]. If you override this, it's still recommended
	// to call [DefaultShouldCacheRequest] after your custom logic:
	//
	//	cfg.ShouldCacheRequest = func(req *http.Request) bool {
	//	    if req.URL.Host == "internal.api" {
	//	        return false
	//	    }
	//	    return httpccache.DefaultShouldCacheRequest(req)
	//	}
	ShouldCacheRequest func(*http.Request) bool
	// CacheKeyFunc generates the storage key for cache lookups and writes.
	CacheKeyFunc func(r *http.Request, ignoredHeaders []string) string
	// IgnoredCacheKeyHeaders are headers that should be ignored by the default
	// cache key function because they are typically per-request metadata
	// (request ID, tracing IDs, etc). Defaults to [DefaultIgnoredCacheKeyHeaders].
	IgnoredCacheKeyHeaders []string
	// AllowAuthorizationCaching, when true, relaxes conservative authorization
	// caching behavior. Defaults to false.
	AllowAuthorizationCaching bool
	// AllowHeuristicFreshness, when true, enables Last-Modified heuristic
	// freshness when explicit freshness directives are missing. Defaults to
	// false.
	AllowHeuristicFreshness bool
	// MaxObjectSize is the maximum response body size (bytes) that will be
	// stored. Zero means unlimited. Defaults to 0 (unlimited).
	MaxObjectSize int64

	// DisableResponseAnnotation, when true, disables adding cache metadata
	// headers (Via, CacheStatusHeader, CacheReasonHeader) to responses.
	DisableResponseAnnotation bool
	// ViaProduct is the token added to the Via header when annotation is
	// enabled. Defaults to "httpccache".
	ViaProduct string
	// CacheStatusHeader is the response header name used for cache status
	// metadata. Defaults to [DefaultCacheStatusHeader] ("X-Cache-Status").
	CacheStatusHeader string
	// CacheReasonHeader is the response header name used for cache reason
	// metadata. Defaults to [DefaultCacheReasonHeader] ("X-Cache-Reason").
	CacheReasonHeader string
}

Config is the configuration for the cache transport.

func (*Config) Validate

func (c *Config) Validate() error

Validate validates and sets defaults for the config.

type FileStorage

type FileStorage struct {
	Dir        string
	MaxEntries int
	MaxAge     time.Duration
	// contains filtered or unexported fields
}

FileStorage stores cache entries on disk as:

<metadata-json>\n<body-bytes>

Filenames are opaque and versioned.

func NewFileStorage

func NewFileStorage(dir string, maxEntries int, maxAge time.Duration) (*FileStorage, error)

NewFileStorage creates a new filesystem storage backend.

func (*FileStorage) Delete

func (f *FileStorage) Delete(_ context.Context, key string) error

Delete removes a cached file for a key.

func (*FileStorage) Get

Get retrieves a record by key from filesystem storage.

func (*FileStorage) Prune

func (f *FileStorage) Prune(_ context.Context) error

Prune removes stale entries, evicts by MaxEntries, and purges mismatched storage versions.

func (*FileStorage) Set

func (f *FileStorage) Set(_ context.Context, key string, entry *CacheEntry, body io.Reader) error

Set writes a record by key to filesystem storage.

type MemoryStorage

type MemoryStorage struct {
	MaxEntries int
	MaxAge     time.Duration
	// contains filtered or unexported fields
}

MemoryStorage is an in-memory implementation of Storage.

func NewMemoryStorage

func NewMemoryStorage(maxEntries int, maxAge time.Duration) *MemoryStorage

NewMemoryStorage creates a new in-memory storage backend.

func (*MemoryStorage) Delete

func (m *MemoryStorage) Delete(_ context.Context, key string) error

Delete removes a record by key.

func (*MemoryStorage) Get

Get retrieves a record by key from in-memory storage.

func (*MemoryStorage) Prune

func (m *MemoryStorage) Prune(_ context.Context) error

Prune deletes expired entries and evicts old entries beyond max capacity.

func (*MemoryStorage) Set

func (m *MemoryStorage) Set(_ context.Context, key string, entry *CacheEntry, body io.Reader) error

Set stores a record in memory and prunes old entries.

type Storage

type Storage interface {
	// Get retrieves a cached entry by key. The caller must close the body.
	Get(ctx context.Context, key string) (entry *CacheEntry, body io.ReadCloser, err error)
	// Set stores metadata and streams the body. When body is nil, only metadata
	// and timestamps are updated and existing body bytes are preserved.
	Set(ctx context.Context, key string, entry *CacheEntry, body io.Reader) error
	Delete(ctx context.Context, key string) error
	// Prune removes entries past the storage max-age (if configured) and enforces
	// any max-capacity policy (e.g. evicting oldest). Backends that do not
	// implement pruning return nil.
	Prune(ctx context.Context) error
}

Storage is the backend contract used by the cache transport. Implementations store opaque metadata bytes and timestamps separately from the response body. Marshaling CacheEntry to metadata is the caller's responsibility.

Jump to

Keyboard shortcuts

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