Design Overview¶
Why always forward?¶
Conventional caching proxies serve from cache when the entry is fresh, skipping the origin entirely. This is the whole point of a cache — but it means the origin’s access control is bypassed for cached responses. If a user’s credentials are revoked, they can still receive cached content until it expires.
Relaycache’s security model is simpler and stricter:
The origin server is the sole authority on both content freshness and access control. Relaycache never serves a response body without the origin first confirming the request is valid.
Every request — regardless of cache state — is forwarded to the origin with
all original headers intact (including Authorization, cookies, etc.). The
cache is a bandwidth-saving mechanism only.
Request lifecycle¶
┌─────────────────────────────────────┐
│ Relaycache │
│ │
Client request ────►│ 1. Look up cache key │
(with auth) │ 2. Build forwarded request: │
│ - all client headers │
│ - merge validators from cache │
│ - strip Range if body cached │
│ 3. Forward to origin │──► Origin
│ │◄── 200 / 304 / 4xx
│ 4a. 200: store blob, return 200 │
│ 4b. 304 + proxy hit: return 200 │──► Client
│ or 206 (if range requested) │
│ 4c. 304 + client hit: return 304 │
│ 4d. anything else: pass through │
└─────────────────────────────────────┘
Headers added by Relaycache¶
On forwarded requests (to origin)¶
Header |
Value |
Purpose |
|---|---|---|
|
|
RFC 7230 §5.7.1 — identifies proxy in chain |
On responses (to client)¶
Header |
Value |
Purpose |
|---|---|---|
|
|
RFC 7230 §5.7.1 — identifies proxy in chain |
|
|
Whether body came from cache or origin |
|
the cache key string |
Debugging — shows which key was used including Vary dimensions |
What is and is not cached¶
Condition |
Cached? |
Reason |
|---|---|---|
|
✅ |
Standard cacheable response |
|
❌ |
RFC 7234: must not cache |
|
❌ |
User-personalised; would require per-credential storage |
|
❌ |
Too large |
Any non-GET method |
❌ |
Only GET bodies are idempotent to cache |
Any non-200 status |
❌ |
Error responses, redirects, etc. pass through |
Module structure¶
src/
main.rs Entry point: wires config → store → proxy → listener
config.rs CLI/env config parsing (clap, humantime, size parsing)
headers.rs All HTTP header logic (pure functions, well-tested)
proxy.rs Request handler: cache lookup → forward → store/serve
store.rs CacheStore: moka index + SQLite DB + blob files
Key invariants¶
Every request reaches the upstream. Never serve a response body without a round-trip to the upstream first.
Auth headers are never stored.
store::safe_response_headersstripsAuthorization,Set-Cookie,WWW-Authenticate, andProxy-*headers. Add a unit test verifying this ifUNSAFE_RESPONSE_HEADERSever changes.Vary: Authorization→ never cache. Enforced in bothheaders::vary_prevents_cachingandstore::insert.Blob files are immutable. Written once (content-addressed);
write_blobskips writing if the file already exists.WAL checkpoint on clean shutdown.
store::DbOp::ShutdowntriggersPRAGMA wal_checkpoint(TRUNCATE). Do not exit without sending this.