titip

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 22 Imported by: 0

README

Titip

Go Reference Go Version

Titip is a high-throughput, low-allocation HTTP caching middleware for Go applications and API gateways.

Designed for high-concurrency services, Titip reduces backend load by serving cached responses with minimal memory allocation, atomic Redis Hash multi-variant negotiation, RFC-compliant freshness calculations, and fail-open resilience.

Key Features

  • Low-Allocation Design: Reuses internal memory buffers and decompression streams to minimize heap allocations under high concurrency.
  • Fail-Open Resilience: Storage outages, decompression errors, or upstream panics safely bypass to the origin handler (fwd=bypass) or serve stale cache without terminating the process.
  • Session & Privacy Protection: Cold URL misses execute independently without singleflight coalescing, preventing accidental sharing of Set-Cookie or private session headers across concurrent callers.
  • RFC-7234, RFC-9111 & RFC-9213 Compliant: Implements the official Age & Freshness calculation standard (apparent age, corrected initial age, resident time, clock-skew correction, and multi-variant Vary header negotiation).
  • Tiered Cache-Control (RFC 9213): Supports targeted header resolution (Titip-Cache-ControlCDN-Cache-ControlCache-Control), allowing backends to configure edge caching independently from browser caching.
  • RFC-9211 Cache-Status Observability: Structured diagnostics (Cache-Status: titip; hit; ttl=295, fwd=stale, fwd=bypass) with multi-tier cache chaining.
  • Edge Side Includes (ESI 1.0): Concurrent fragment assembly, in-process routing, recursive loop protection, and SSRF prevention.
  • Granular Cache Purge API: Invalidation via programmatic Go API (exact URL, wildcard prefixes, surrogate Cache-Tag, soft-purge, or total cache wipeout).
  • Pluggable Architecture: Standard net/http middleware with modular framework adapters and decoupled storage engines.

Architecture

Titip separates metadata from variant payloads to enable atomic multi-variant negotiation (Vary) and short-circuiting with zero redundant body I/O:

[ Incoming Request ] ──► (Primary Key: Scheme + Host + Path + Filtered Query)
                               │
                               ▼
        ┌──────────────────────────────────────────────┐
        │ Stage 1: Metadata Lookup (Redis Hash)        │
        │ Redis Key: titip:meta:<primaryKey>           │
        │ Fields: _index (pb.CacheMetadata), <variant> │
        └──────────────────────┬───────────────────────┘
                               │
        ┌──────────────────────┴───────────────────────┐
        │ Match Vary Variant & Evaluate Freshness      │
        └──────┬───────────────────────────────┬───────┘
               │                               │
      (Downstream 304 / HEAD)             (Cache Hit)
               │                               │
               ▼                               ▼
    ┌─────────────────────┐       ┌──────────────────────────────┐
    │ Serve 304 / Headers │       │ Stage 2: Fetch Variant Body  │
    │ (0 Body Payload I/O)│       │ Redis Key: titip:body:...    │
    └─────────────────────┘       └──────────────┬───────────────┘
                                                 │
                                                 ▼
                                  ┌──────────────────────────────┐
                                  │ LZ4 Decompress & Stream Body │
                                  └──────────────────────────────┘

Modules & Ecosystem

Titip is organized as a multi-module workspace. Each module is versioned independently:

Module Description Documentation
github.com/indragunawan/titip Core caching middleware, state machine, and programmatic Purge API Core Quickstart
github.com/indragunawan/titip/adapter/caddy Native Caddy HTTP middleware directive (titip) & Admin Purge API Caddy Adapter Guide
github.com/indragunawan/titip/storage/redis High-performance Redis 7.4+/8 distributed storage driver (rueidis) Redis Storage Guide
github.com/indragunawan/titip/storage/redis/caddy Guest storage module for Caddy (titip.storage.redis) Caddy Redis Guide

Quickstart

Install the core package and Redis storage driver:

go get github.com/indragunawan/titip
go get github.com/indragunawan/titip/storage/redis

Wrap any standard net/http handler:

package main

import (
    "context"
    "net/http"
    "time"

    "github.com/redis/rueidis"

    "github.com/indragunawan/titip"
    storageRedis "github.com/indragunawan/titip/storage/redis"
)

func main() {
    // 1. Initialize Redis Client
    client, err := rueidis.NewClient(rueidis.ClientOption{
        InitAddress: []string{"127.0.0.1:6379"},
    })
    if err != nil {
        panic(err)
    }
    defer client.Close()

    // 2. Create Titip Redis Storage
    store, err := storageRedis.New(client, storageRedis.WithKeyPrefix("titip:"))
    if err != nil {
        panic(err)
    }
    defer store.Close()

    // 3. Configure Titip Engine
    cache, err := titip.New(
        titip.WithStorage(store),
        titip.WithCacheStatusMode(titip.CacheStatusRFC9211),
        titip.WithBackgroundFetchTimeout(125*time.Second),
    )
    if err != nil {
        panic(err)
    }
    defer cache.Close(context.Background())

    // 4. Wrap Standard HTTP Handler
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/data", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        // Cache publicly for 60 seconds, allow serving stale for 5 minutes during revalidation
        w.Header().Set("Cache-Control", "public, max-age=60, stale-while-revalidate=300")
        w.Header().Set("Cache-Tag", "catalog items")
        w.Write([]byte(`{"message": "hello from origin", "timestamp": "` + time.Now().String() + `"}`))
    })

    http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cache.ServeHTTP(w, r, mux)
    }))
}

Configuration Reference

Pass any of the following functional options to titip.New(...):

Option Type Default Description
WithStorage(s) storage.Storage (Required) Storage backend implementation (e.g. storage/redis).
WithCacheStatusMode(mode) CacheStatusMode CacheStatusSimpleToken Emitted status format (CacheStatusRFC9211, CacheStatusSimpleToken, or CacheStatusNone).
WithCacheKey(cfg) CacheKey {} (standard) Primary cache key generation rules and query parameter filtering.
WithTagHeaderName(name) string "Cache-Tag" Response header inspected for surrogate cache tags.
WithBackgroundFetchTimeout(d) time.Duration 125s Maximum timeout budget for background revalidations (stale-while-revalidate).
WithStorageTimeout(d) time.Duration 1s Maximum time budget for storage reads/writes before fail-open bypass.
WithRespectClientCacheControl() bool false When enabled, honors client request Cache-Control: no-cache / no-store.
WithConvertHeadToGet(bool) bool true Converts origin HEAD cache misses to GET to prime the cache with body bytes.
WithAutoInvalidateMutatingMethods() bool false RFC 9111 §4.4: Auto-purges URI cache when mutating requests (POST/PUT/DELETE) succeed.
WithLogger(l) *slog.Logger slog.Default() Structured logger instance for diagnostic events.
WithMetrics(reg) prometheus.Registerer nil Prometheus registry for cache and ESI telemetry.
WithESI(opts...) ...esi.Option disabled Edge Side Includes processing configuration and options.

Cache Key & Query Parameter Normalization

Titip constructs normalized cache keys directly without expensive hashing. Use CacheKey to filter query parameters and strip tracking tags to prevent cache fragmentation:

cache, err := titip.New(
    titip.WithStorage(store),
    titip.WithCacheKey(titip.CacheKey{
        // Strips marketing query parameters (utm_*, fbclid, gclid, mc_eid, etc.)
        ExcludeMarketingParams: true,
        // Allowlist specific query parameters to include (or use ExcludedQueryParams for a denylist)
        IncludedQueryParams:    []string{"page", "sort", "filter"},
    }),
)

Cache-Status Diagnostics

Titip supports three Cache-Status modes configured via WithCacheStatusMode:

1. CacheStatusRFC9211 (Structured Header)

Emits structured diagnostics compliant with RFC 9211, supporting multi-tier cache chaining:

Cache-Status: titip; hit; ttl=240
Cache-Status: "Fastly"; hit, titip; hit; ttl=240
2. CacheStatusSimpleToken (Single Token Header)

Emits a concise single-token status header:

Token Description
HIT Served fresh directly from cache or matched downstream conditional 304 Not Modified.
MISS Cache miss: fetched from origin and stored in cache.
EXPIRED Expired cache entry was synchronously revalidated with origin and refreshed (200 OK).
REVALIDATED Expired cache entry was revalidated with origin via conditional headers (304 Not Modified).
UPDATING Stale cache entry served immediately while revalidating asynchronously in the background (stale-while-revalidate).
STALE Stale cache entry served as failover fallback due to origin error (stale-if-error).
BYPASS Caching explicitly bypassed (mutating method, client no-store, Range request, WebSocket).
DYNAMIC Evaluated for caching, but origin response is uncacheable (Set-Cookie, private, no-store).
3. CacheStatusNone

Disables the Cache-Status response header completely.

Cache Invalidation & Purge API

Titip provides a programmatic Go API for Hierarchical Path Purging, Surrogate Tag Purging, and Namespace Invalidation.

Programmatic Go API
// 1. Path Purge (purges /api/products and all its query string variants)
err := cache.Purge(ctx, "/api/products")

// 2. Exact Query Invalidation (purges only ?id=10, leaves other queries intact)
err := cache.Purge(ctx, "http://example.com/api/products?id=10", titip.WithSoftPurge())

// 3. Directory Wildcard (purges all cached paths under /assets/)
err := cache.Purge(ctx, "/assets/*")

// 4. Surrogate Tag Invalidation (invalidates all cached entries matching the tag)
err := cache.PurgeTag(ctx, "catalog")

// 5. Namespace Invalidation (invalidates all cached entries under the configured prefix)
err := cache.PurgeAll(ctx)

Tiered & Targeted Cache-Control (RFC 9213)

Titip supports RFC 9213 Targeted Cache-Control, allowing backend origins to define separate caching rules for the edge/proxy layer versus end-user browsers.

Precedence Hierarchy (First Match Wins)

$$\text{\textbf{Titip-Cache-Control}} ;\longrightarrow; \text{\textbf{CDN-Cache-Control (RFC 9213)}} ;\longrightarrow; \text{\textbf{Cache-Control (RFC 9111)}}$$

HTTP/1.1 200 OK
Titip-Cache-Control: public, max-age=86400, stale-while-revalidate=3600
Cache-Control: private, no-store
  • Titip (Intermediary): Caches the response in Redis for 24 hours (max-age=86400), shielding the origin from load.
  • Client (Browser): Receives Cache-Control: private, no-store, preventing sensitive data from persisting in local browser history.

Edge Side Includes (ESI)

Titip includes an Edge Side Includes (ESI 1.0) engine with parallel fragment fetching, circular loop protection, and SSRF prevention.

When ESI is active, Titip advertises capability to upstream origins by sending Surrogate-Capability: titip="ESI/1.0" per the W3C ESI 1.0 / Edge Architecture Specification. Origins can respond with Surrogate-Control: content="ESI/1.0" to direct ESI processing.

For standalone package documentation and options reference, see ESI Package Guide.

cache, err := titip.New(
    titip.WithStorage(store),
    titip.WithESI(
        esi.WithInternalFetcher(esi.HandlerFetcher(router)),
        esi.WithMaxDepth(3),
        esi.WithMaxTimeout(5 * time.Second),
    ),
)
Supported ESI Tags & Syntax
Syntax Description
<esi:include src="/fragment" /> Self-closing fragment include. Fetched concurrently.
<esi:include src="/fragment" alt="/fallback" onerror="continue" /> Include with fallback URL on failure or silent omission (onerror="continue").
<esi:include src="...">Fallback HTML</esi:include> Paired include with inline fallback block.
<!--esi <div>Visible only when ESI active</div> --> Unescapes enclosed HTML comments when ESI is enabled.
<esi:remove><p>Placeholder</p></esi:remove> Strips placeholder content intended for non-ESI clients.
<!--esi-comment text="..." --> Strips internal comments without emitting bytes.
ESI Functional Options (esi.Option)
Option Builder Default Description
esi.WithHeaderRequired(bool) false Process ESI only when origin sets Surrogate-Control: content="ESI/1.0".
esi.WithInternalFetcher(fn) nil Custom hook for in-memory virtual subrequests (e.g. esi.HandlerFetcher(r)).
esi.WithMaxDepth(uint32) 3 Maximum nesting depth for recursive ESI includes.
esi.WithMaxTimeout(duration) 30s Maximum time budget per fragment include fetch.
esi.WithMaxConcurrentRequests(int) 8 Maximum concurrent fetch goroutines per document.
esi.WithAllowPrivateIPs(bool) false SSRF guard: when false (default), blocks RFC 1918 / loopback / cloud metadata CIDRs.
esi.WithAllowedHosts(...string) [] List of allowed external hosts for domain includes (empty allows all public hosts).
esi.WithAllowPrivateIPsForAllowedHosts(bool) false Permits private IPs specifically for explicitly allowed hosts.
esi.WithMaxResponseSize(int64) 10MB Maximum allowed fragment body size in bytes.
esi.WithDisableForwardCookies(bool) false When false (default), forwards Set-Cookie headers from fragments to the client.
esi.WithPreserveETag(bool) false When true, weakens origin ETag (W/"...") and preserves Last-Modified for downstream 304. When false (default), strips ETag/Last-Modified downstream to guarantee fresh fragment execution.
esi.WithIncludeErrorMarker(string) "" HTML placeholder rendered on unhandled fetch errors.

Observability & Metrics

Titip exports comprehensive Prometheus metrics for request traffic, cache latencies, purge invalidations, and ESI fragment processing:

// Register with a Prometheus registry:
cache, err := titip.New(
    titip.WithStorage(store),
    titip.WithRegisterer(prometheus.DefaultRegisterer),
)
Exported Metrics
Metric Name Type Labels Description
titip_requests_total Counter status (hit, miss, stale_hit, revalidated, bypass, error) Total HTTP requests processed by Titip caching middleware.
titip_request_duration_seconds Histogram status Request latency distribution in seconds across cache statuses.
titip_purges_total Counter type (url, tag, all), mode (hard, soft), status (success, error) Total purge operations executed by type and mode.
titip_purged_entries_total Counter type (url, tag, all), mode (hard, soft) Total logical cache entries invalidated by purge operations.
titip_esi_fragments_total Counter status (success, fallback, error) Total ESI fragment includes processed (enabled when ESI is active).
titip_esi_duration_seconds Histogram mode (in_process, outbound) Latency distribution of ESI fragment fetching and document splicing.
# Example Prometheus Scrape Output:
titip_requests_total{status="hit"} 4125
titip_requests_total{status="miss"} 102
titip_requests_total{status="stale_hit"} 18
titip_requests_total{status="revalidated"} 12
titip_requests_total{status="bypass"} 5
titip_requests_total{status="error"} 0

titip_purges_total{mode="soft",status="success",type="url"} 45
titip_purged_entries_total{mode="soft",type="url"} 45

titip_esi_fragments_total{status="success"} 230
titip_esi_duration_seconds_bucket{mode="in_process",le="0.005"} 225

Testing & Concurrency Standards

Titip enforces continuous race detection and zero-leak concurrency standards:

# Run all unit and concurrency stress tests with race detector
go test -race -count=50 -v ./...

Contributing

We welcome contributions for new framework adapters and storage drivers.

Please read our Contributing Guide for architectural guidelines, interface contracts, and testing standards.

License

This project is licensed under the MIT License.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CacheKey added in v0.2.0

type CacheKey struct {
	// IncludeProtocol includes the request scheme ("http" or "https") in the cache key.
	// When true, HTTP and HTTPS requests reference distinct cache entries.
	IncludeProtocol bool

	// ExcludeHost excludes the HTTP Host / domain from the cache key.
	// When true, Host is omitted so multiple domains serving identical content share cache entries.
	ExcludeHost bool

	// ExcludeQueryString removes all query parameters from the cache key.
	// When true, all query parameters are stripped so requests with different query strings share cache.
	ExcludeQueryString bool

	// DisableQueryStringSort preserves the original query parameter ordering from the request URL.
	// When true, query parameter order is preserved as received from the client.
	DisableQueryStringSort bool

	// IncludedQueryParams specifies an allowlist of query parameter names to include in the cache key.
	// If set, only these specific parameters are included in the cache key.
	IncludedQueryParams []string

	// ExcludedQueryParams specifies a denylist of query parameter names to exclude from the cache key.
	// If set, all query parameters except these are included in the cache key.
	ExcludedQueryParams []string

	// ExcludeMarketingParams filters out standard advertising and tracking query parameters
	// (e.g. utm_source, utm_campaign, utm_medium, gclid, fbclid, ttclid).
	// When true, marketing tracking parameters are stripped from the cache key.
	ExcludeMarketingParams bool

	// IncludedHeaderNames specifies request header names whose values are appended to the primary cache key.
	//
	// Note: Do NOT include headers that the origin already manages via the HTTP "Vary" header
	// (e.g. "Accept-Encoding"), as Titip handles origin Vary negotiation automatically.
	//
	// Warning: NEVER include authentication tokens or credentials (e.g. "Authorization").
	// Specifying headers with high cardinality or wide ranges of values dramatically lowers the
	// cache hit rate and causes higher eviction churn.
	//
	// Best used for low-cardinality headers or A/B experiment buckets (e.g. "X-Region", "X-Experiment-Bucket").
	IncludedHeaderNames []string

	// IncludedCookieNames specifies cookie names whose values are appended to the cache key.
	//
	// Warning: NEVER include session identifiers, auth cookies, or credentials.
	// Including unique per-user cookies effectively creates per-user caches, destroying hit rates.
	//
	// Best used for low-cardinality user preferences or A/B testing groups (e.g. "ab_group", "currency", "theme", "locale").
	IncludedCookieNames []string

	// CaseInsensitivePath normalizes the URL path to lowercase in the primary cache key.
	// When true, requests with different path casing (e.g. /Products/Shoes vs /products/shoes) share the same cache entry.
	CaseInsensitivePath bool

	// IncludedQueryParamValues specifies an allowlist of specific parameter values.
	// A parameter key in this map is only included in the cache key if its value matches one of the specified allowed values.
	// Any value not in the list is omitted from the cache key.
	IncludedQueryParamValues map[string][]string
}

CacheKey defines the rules for assembling zero-hash canonical cache keys.

Every cached request automatically receives a cache key. A zero-value CacheKey{} or omitting WithCacheKey applies the standard RFC-compliant default: host included, protocol excluded, case-sensitive path, all query parameters retained, and sorted alphabetically.

type CacheStatusMode

type CacheStatusMode int

CacheStatusMode specifies the format of the emitted Cache-Status header.

const (
	// CacheStatusSimpleToken outputs single-token status header (e.g. HIT, MISS, EXPIRED, REVALIDATED, UPDATING, STALE, BYPASS, DYNAMIC) by default.
	CacheStatusSimpleToken CacheStatusMode = iota
	// CacheStatusRFC9211 outputs structured RFC-9211 Cache-Status header (e.g. Cache-Status: titip; hit; ttl=240).
	CacheStatusRFC9211
	// CacheStatusNone disables cache status header generation.
	CacheStatusNone
)

type Option

type Option func(*config)

Option configures Titip middleware options.

func WithAutoInvalidateMutatingMethods

func WithAutoInvalidateMutatingMethods() Option

WithAutoInvalidateMutatingMethods enables automatic invalidation of cached GET entries when successful mutating requests (POST, PUT, DELETE, PATCH) are received for the URI, matching the mandatory invalidation behavior defined in RFC 9111 Section 4.4. By default, this is disabled so applications can rely on explicit tag-based (Cache-Tag) or URL invalidation.

func WithBackgroundFetchTimeout added in v0.2.0

func WithBackgroundFetchTimeout(d time.Duration) Option

WithBackgroundFetchTimeout configures the maximum timeout for asynchronous background revalidation (stale-while-revalidate) origin fetches (defaults to 125s). Set to 0 or negative to disable background timeout enforcement.

func WithCacheKey added in v0.2.0

func WithCacheKey(k CacheKey) Option

WithCacheKey customizes the rules for assembling canonical cache keys (such as query parameter filtering, marketing tag removal, and header/cookie dimensions).

A cache key is always automatically generated for every request. If WithCacheKey is omitted, Titip applies standard default key generation (protocol-agnostic, host-aware, case-sensitive path, all query parameters retained, and sorted alphabetically).

func WithCacheStatusMode

func WithCacheStatusMode(mode CacheStatusMode) Option

WithCacheStatusMode configures Cache-Status header mode.

func WithConvertHeadToGet

func WithConvertHeadToGet(enable bool) Option

WithConvertHeadToGet configures whether HEAD cache misses and revalidations are converted to GET when fetching from the upstream origin to prime the cache (defaults to true). When false, HEAD misses query the origin as HEAD and are not saved to cache.

func WithESI

func WithESI(opts ...esi.Option) Option

WithESI enables ESI processing with the provided ESI options. If no options are provided, ESI is enabled with safe production defaults.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger configures the structured slog.Logger.

func WithMetrics

func WithMetrics(reg prometheus.Registerer) Option

WithMetrics configures the Prometheus metrics registerer.

func WithRespectClientCacheControl

func WithRespectClientCacheControl() Option

WithRespectClientCacheControl enables respecting client request Cache-Control directives (e.g. no-cache, no-store). By default, client cache directives are ignored to protect origin servers.

func WithStorage

func WithStorage(s storage.Storage) Option

WithStorage configures the backend cache storage engine.

func WithStorageTimeout

func WithStorageTimeout(d time.Duration) Option

WithStorageTimeout configures maximum timeout for storage operations (defaults to 1s).

func WithTagHeaderName

func WithTagHeaderName(name string) Option

WithTagHeaderName configures the response header inspected for cache tags (defaults to "Cache-Tag").

type PurgeOption

type PurgeOption func(*purgeConfig)

PurgeOption configures Purge, PurgeTag, or PurgeAll operations.

func WithSoftPurge

func WithSoftPurge() PurgeOption

WithSoftPurge marks entries as stale rather than evicting immediately (safe thundering-herd mode). The stale copy is preserved for stale-if-error fallback if the origin subsequently fails.

type Titip

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

Titip represents the HTTP caching middleware instance.

func New

func New(opts ...Option) (*Titip, error)

New creates a new Titip caching middleware instance.

func (*Titip) Close

func (t *Titip) Close(ctx context.Context) error

Close cleanly shuts down the middleware, awaiting background SWR revalidations.

func (*Titip) Purge

func (t *Titip) Purge(ctx context.Context, target string, opts ...PurgeOption) (int64, error)

Purge invalidates cache entries matching the specified path, URL, exact query variant, or wildcard.

The target supports four formats:

  • "/api/products" — purges the path and ALL query string variations
  • "/api/products?id=42" — purges only this exact query variant
  • "/assets/*" — wipes all cached paths under /assets/ (wildcard)
  • "https://example.com/api" — host-scoped path purge (include domain in target to scope by host)

By default, purge is a hard-delete (immediate physical eviction). Use WithSoftPurge() to mark entries as stale instead for safe thundering-herd protection.

Returns the total number of logical cache entries invalidated.

func (*Titip) PurgeAll

func (t *Titip) PurgeAll(ctx context.Context) (int64, error)

PurgeAll deletes every cache entry in the configured storage namespace.

func (*Titip) PurgeTag

func (t *Titip) PurgeTag(ctx context.Context, tag string, opts ...PurgeOption) (int64, error)

PurgeTag invalidates all cache entries tagged with the specified tag. The tag is treated as a literal string. To wipe the entire cache namespace, use PurgeAll.

func (*Titip) ServeHTTP

func (t *Titip) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.Handler)

ServeHTTP executes the Titip caching middleware pipeline for a request and forwards to next on cache miss or revalidation.

Directories

Path Synopsis
adapter
caddy module
internal
redis module
redis/caddy module

Jump to

Keyboard shortcuts

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