package httpcache
import (
"strconv"
"strings"
"time"
)
// Parse returns the TTL implied by a Cache-Control header value.
// ok is false when the response must not be cached.
func Parse(header string) (ttl time.Duration, ok bool) {
var maxAge, sMaxAge int
var haveMax, haveShared bool
for _, raw := range strings.Split(header, ",") {
directive := strings.ToLower(strings.TrimSpace(raw))
if directive == "" {
continue
}
switch {
case directive == "no-store", directive == "no-cache", directive == "private":
return 0, false
case strings.HasPrefix(directive, "max-age"):
if secs, valid := directiveSeconds(directive); valid {
maxAge, haveMax = secs, true
}
case strings.HasPrefix(directive, "s-maxage"):
if secs, valid := directiveSeconds(directive); valid {
sMaxAge, haveShared = secs, true
}
}
}
if haveShared {
return time.Duration(sMaxAge) * time.Second, true
}
if haveMax {
return time.Duration(maxAge) * time.Second, true
}
return 0, false
}
func directiveSeconds(directive string) (int, bool) {
parts := strings.SplitN(directive, "=", 2)
if len(parts) != 2 {
return 0, false
}
secs, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil || secs < 0 {
return 0, false
}
return secs, true
}
package httpcache
import (
"sync"
"time"
)
type entry struct {
value []byte
expiresAt time.Time
}
type TTLCache struct {
mu sync.RWMutex
items map[string]entry
}
func NewTTLCache() *TTLCache {
return &TTLCache{items: make(map[string]entry)}
}
func (c *TTLCache) Get(key string) ([]byte, bool) {
c.mu.RLock()
e, found := c.items[key]
c.mu.RUnlock()
if !found || time.Now().After(e.expiresAt) {
return nil, false
}
return e.value, true
}
func (c *TTLCache) Set(key string, value []byte, ttl time.Duration) {
if ttl <= 0 {
return
}
c.mu.Lock()
c.items[key] = entry{value: value, expiresAt: time.Now().Add(ttl)}
c.mu.Unlock()
}
package httpcache
import (
"bytes"
"io"
"net/http"
)
type CachingTransport struct {
Next http.RoundTripper
Cache *TTLCache
}
func (t *CachingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Method != http.MethodGet {
return t.next().RoundTrip(req)
}
key := req.URL.String()
if body, hit := t.Cache.Get(key); hit {
return cacheResponse(req, body), nil
}
resp, err := t.next().RoundTrip(req)
if err != nil {
return nil, err
}
ttl, ok := Parse(resp.Header.Get("Cache-Control"))
if !ok || resp.StatusCode != http.StatusOK {
return resp, nil
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
t.Cache.Set(key, body, ttl)
resp.Body = io.NopCloser(bytes.NewReader(body))
return resp, nil
}
func (t *CachingTransport) next() http.RoundTripper {
if t.Next != nil {
return t.Next
}
return http.DefaultTransport
}
func cacheResponse(req *http.Request, body []byte) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"X-Cache": []string{"HIT"}},
Body: io.NopCloser(bytes.NewReader(body)),
Request: req,
}
}
This snippet shows how an HTTP client can honor a server's Cache-Control header by parsing its directives into a concrete time-to-live and storing responses in a TTL-aware in-memory cache. The three tabs collaborate: a parser that turns the header text into a time.Duration, a concurrent cache keyed by URL, and a caching transport that ties them together.
In cachecontrol.go, Parse scans the comma-separated directives that make up a Cache-Control value. It lower-cases and trims each token, then handles the two flags that force a zero TTL — no-store and no-cache — before extracting the numeric argument from max-age or the fallback s-maxage. The directiveSeconds helper splits on = and uses strconv.Atoi, guarding against malformed or negative values by returning ok=false. Treating a negative or unparsable age as "not cacheable" is deliberate: a cache that guesses a TTL from a broken header risks serving stale data, so the safe default is to skip caching.
The TTLCache in ttlcache.go is a small map guarded by a sync.RWMutex. Each stored entry records an expiresAt timestamp computed once at insert time rather than a duration, so reads become a cheap time.Now().After comparison. Get takes a read lock for the common path and returns a miss when the entry has expired; Set ignores non-positive TTLs so that uncacheable responses never enter the map. Lazy expiration like this avoids a background sweeper goroutine, at the cost of expired entries lingering in memory until overwritten or explicitly evicted.
In transport.go, CachingTransport implements http.RoundTripper, which lets it drop into any http.Client transparently. It only caches idempotent GET requests, returns a cached *http.Response on a hit, and otherwise delegates to the wrapped transport. After a successful response it calls Parse on the response's own Cache-Control header and stores the body via Set only when the TTL is positive. Because the body is an io.ReadCloser that can be consumed once, cacheResponse buffers it and hands out a fresh bytes.Reader on every retrieval. This pattern is useful when a client wants server-driven caching without pulling in a full HTTP cache library, and it keeps the freshness policy exactly where the origin server defined it.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.