Chapter Orientation
After Chapter 100's three automatic behaviors, caching is the largest and most flavorful. Once the http_client_cache layer hangs a Store (xhttpcache) into the Client configuration, lookup, hit delivery, freshness judgment, conditional validation, storage, partial-response composition, and unsafe-method invalidation are all automatic — not one line of your code changes; the second request for the same URL comes from local. The protocol side follows RFC 9111 exactly: the request's Cache-Control (no-store/no-cache/only-if-cached/age constraints) and the response's storability are all handled by the shared cache-policy parser — the client invents no approximate semantics. The storage object stays public — applications, proxies, and custom persistence backends use the same Store contract directly. This chapter covers configuration, the four call modes, the storage contract, and concurrent conditional commits.
101.1引入
Why should the client have caching built in instead of "the business storing responses itself"? Three reasons.Correctness: storability judgment (no-store/Vary: */authenticated responses/shared-private rules), freshness computation (max-age/Expires/age correction), conditional validation (ETag/Last-Modified 304 merging) — every one is precise RFC rulework; a hand-written "store a Map, expire by time" errs in colorful ways against real response headers.Composability: partial-content composition for Range requests (locally held fragments + origin backfill) must cooperate with the storage structure — impossible to bolt on.Invalidation: invalidating same-origin related representations after successful unsafe methods (Location/Content-Location) is part of the caching protocol. xhttp implements these rules once; configure one Store and everything benefits.
101.2概念
Assembly and call modes
Call-level four modes: XHTTP_CLIENT_CACHE_DEFAULT (honor request/response/storage policies — the normal path), DISABLED (bypass reads, writes, and invalidation for this call), RELOAD (force origin validation or refetch; a successful response can still update the cache — the "refresh" button), ONLY (no network; with no record it synthesizes 504 — offline mode). The four modes stack with request-header semantics without conflict — the only-if-cached request header is handled by the same policy parser.
The storage contract: what enters the Store
- Private cache by default;
Sharedexplicitly switches to shared semantics (fields and authorization rules differ). - Never stored:
no-store, non-cacheable status codes,Vary: *, authenticated/private responses forbidden to shared stores. - Heuristic lifetime: with no explicit freshness, a limited heuristic on
Last-Modifiedmay be enabled — 10% by default, at most one day, globally disableable (conservative defaults; privacy and correctness first). - Grow on demand: the encoding body grows only after truly arriving, nothing reserved per Call;
MaxBody(default 8 MiB) is the hard cap on a single origin capture — over the cap stops capturing but fails open by default (the network response still delivers, just uncached); small bodies start at 256 B and double. - Release discipline: after storage or replay completes, candidate records, field snapshots, and captured bodies are released before the final state is published — they do not linger with the user-held result reference.
Conditional commits and concurrency
304 merging, HEAD metadata updates, and Range fragment composition all do not use plain Put overwrites — they use the Store's Replace/RemoveRecord current-version conditions: when a 304 commit conflicts (a concurrent updater won), the current request still delivers its own validated snapshot while the cache keeps the concurrent updater's result — never lose an update to save your own view. This optimistic concurrency shares the idea with Chapter 18's container invalidation rules: if someone changed it between your read and your write and the write condition fails, each goes its own way.
Range composition and unsafe invalidation
Local Range composition: requesting Range: bytes=0-99 while a 0-199 record exists locally — delivered by slicing straight from cache (no network); when only fragments exist locally, the "local fragment + origin backfill" composition is bounded by MaxRanges (default 16) — sorting work, temporary arrays, and multipart amplification all bounded.Unsafe invalidation: a successful POST/PUT/DELETE response invalidates the target URI, together with same-origin Location/Content-Location representations the response permits invalidating — read-after-write consistency is guaranteed by protocol, not by application reminders.
The partition key
PartitionKey is copied at submission and used to isolate the same URI per user/site/tenant — the primary key includes the partition.Why not rely on Vary alone: Vary distinguishes by request headers (e.g., Vary: Authorization), but "which headers should distinguish" is declared by the responder — undeclared means shared; the partition key is the caller's deliberate isolation (the bottom line of multi-tenant clients). Isolation of private data is the caller's decision; the protocol tool is only a tool.
101.3示例
First complete program: cache assembly and capacity configuration
The program below is from examples/http/client_cache — Store mounting and caps:
#include <stdio.h> #include <xrt/http_client_runtime.h> /* 配置高层 HTTP Client 使用共享缓存,并展示调用级分区与只读模式。 */ int main(void) { xnetengineconfig EngineConfig; xhttpclientconfig ClientConfig; xhttpcalloptions CallOptions; xhttpcache* pCache; xnetengine* pEngine; xhttpclient* pClient; pCache = xrtHttpCacheCreate(NULL); if ( pCache == NULL ) { return 1; } xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { xrtNetEngineDestroy(pEngine); xrtHttpCacheRelease(pCache); return 2; } xrtHttpClientConfigInit(&ClientConfig); ClientConfig.Cache.Store = pCache; ClientConfig.Cache.MaxBody = 16u * 1024u * 1024u; ClientConfig.Cache.Strict = false; pClient = xrtHttpClientCreate(pEngine, &ClientConfig); xrtHttpCacheRelease(pCache); if ( pClient == NULL ) { xrtNetEngineDestroy(pEngine); return 3; } xrtHttpCallOptionsInit(&CallOptions); CallOptions.Cache.Mode = XHTTP_CLIENT_CACHE_ONLY; CallOptions.Cache.PartitionKey = XRT_STR_LITERAL("tenant:example"); printf( "cache=%s mode=%u partition=%.*s\n", xrtHttpClientCache(pClient) != NULL ? "enabled" : "disabled", (unsigned int)CallOptions.Cache.Mode, (int)CallOptions.Cache.PartitionKey.Size, CallOptions.Cache.PartitionKey.Data ); xrtHttpClientDestroy(pClient); if ( !xrtNetEngineDestroy(pEngine) ) { return 4; } return 0; }
What just happened. (1) xrtHttpCacheCreate(NULL) builds the in-memory Store (NULL takes the default configuration — capacity and eviction live in xhttpcacheconfig); placed into ClientConfig.Cache.Store — the Client holds the reference from creation. (2) Cache.MaxBody = 16 MiB overrides the default 8 MiB — the single-origin capture cap is the anchor of capacity planning: it decides "how large a response is worth caching", in concert with the Store's total capacity (another configuration). (3) Request code afterward is identical to no-cache — hits stay off the network, stale entries validate automatically, Info's Wire bytes of 0 means hit (the diagnostics face's way to spot a cache hit). (4) HttpClientCache(pClient) lends the same handle back (statistics, explicit eviction); without a Store configured, the layer never enters the hot path.
Second complete program: the cache-policy tour
The second program is from examples/http/cache_policy — the protocol layer of policy parsing and verdicts:
#include <xrt/http_cache_policy.h> #include <stdio.h> #include <string.h> /* 展示共享缓存对带 Authorization 的新鲜响应作出存储和复用计划。 */ int main(void) { static const xhttpfield ResponseFields[] = { { XRT_STR_INIT("Cache-Control"), XRT_STR_INIT("public, max-age=60") } }; xhttpcachecontrol Request; xhttpcachecontrol Response; xhttpcachetime Time; xhttpcacheage Age; xhttpcachefreshness Freshness; xhttpcachestoreinput StoreInput; xhttpcachestoreplan StorePlan; xhttpcacheuseinput UseInput; xhttpcacheuseplan UsePlan; xrtHttpCacheControlInit(&Request); xrtHttpCacheTimeInit(&Time); memset(&Age, 0, sizeof(Age)); Freshness.Lifetime = UINT64_C(60000000); Freshness.Source = XHTTP_CACHE_FRESHNESS_MAX_AGE; if ( !xrtHttpCacheControlParse( ResponseFields, 1, &Response ) || !xrtHttpCacheStoreInputInit( &StoreInput, XRT_STR_LITERAL("GET"), 200, true ) ) { return 1; } StoreInput.Flags |= XHTTP_CACHE_STORE_AUTHORIZATION; if ( xrtHttpCacheStorePlan( &Request, &Response, &Time, &StoreInput, &StorePlan ) != XHTTP_CACHE_STORE_KEEP ) { return 1; } if ( !xrtHttpCacheUseInputInit( &UseInput, 200, true ) || (xrtHttpCacheUsePlan( &Request, &Response, &Age, &Freshness, &UseInput, &UsePlan ) != XHTTP_CACHE_USE_STORED) ) { return 1; } printf( "store=%d use=%d actions=0x%08x\n", (int)StorePlan.Decision, (int)UsePlan.Decision, (unsigned int)UsePlan.Actions ); return 0; }
What just happened. (1) Cache policy is an independent protocol module (http_cache_policy): request Cache-Control directives, response storability, freshness and age — the client cache layer is merely its consumer (the same pattern as Chapter 91's Body reusing Plan and Chapter 97's compression reusing the negotiation layer). (2) This sample validates policy verdicts offline: given request/response headers — storable? fresh for how long? needs validation? — every step of caching behavior is a unit-testable pure function, the engineering guarantee that "automatic behavior never runs wild". (3) The tour family also includes cache_control (directive parsing), cache_time (age computation), cache_validate (validator selection), cache_range (Range composition), cache_store (storage contract), cache_status (status-header generation) — six protocol modules hold up automatic caching, all independently trimmable.
101.4契约
- Assembly: Store into the Client configuration makes everything automatic (lookup/delivery/validation/storage/composition/invalidation); the Client holds the reference; unconfigured, the layer never enters the hot path.
- Four modes: DEFAULT/Disabled/Reload/Only (no record synthesizes 504); unified with request-header semantics by one parser, no approximations.
- Primary key: method + effective URI + partition key + the Vary fields the record actually declares.
- Not stored: no-store/non-cacheable statuses/Vary:*/authenticated-private forbidden to shared stores.
- Heuristics: default 10% of Last-Modified, one-day cap, disableable.
- Caps: MaxBody is the hard cap on a single origin capture (default 8 MiB), fail-open beyond; MaxRanges default 16 bounds composition complexity; grow on demand, zero reservation.
- Conditional commits: 304/HEAD/Range use versioned conditional Replace — on conflict, deliver your snapshot, keep the concurrent updater.
- Invalidation: unsafe success invalidates the target URI + same-origin Location/Content-Location the response permits.
- Partitioning: PartitionKey copied at submission; private isolation relies on partitions, not Vary.
- Trimming:
HTTP_CLIENT_CACHEdepends on the shared cache protocol family (policy/control/time/validate/range/store/status independent).
Common Mistakes
Symptom: setting MaxBody = 16 MiB and believing the cache occupies at most 16 MiB — after hours of running, memory climbs to hundreds of MiB.
Cause: MaxBody is the single-origin capture cap (how large a response still gets cached); the Store's total capacity and eviction live in xhttpcacheconfig (the configuration at Store creation). Two knobs, two jobs.
ClientConfig.Cache.MaxBody = 16u * 1024u * 1024u; /* assumed this is total capacity */ pCache = xrtHttpCacheCreate(NULL); /* default total capacity untouched */
xhttpcacheconfig CacheConfig; xrtHttpCacheConfigInit(&CacheConfig); CacheConfig.MaxBytes = 64u * 1024u * 1024u; /* Store total capacity + eviction */ pCache = xrtHttpCacheCreate(&CacheConfig); ClientConfig.Cache.Store = pCache; ClientConfig.Cache.MaxBody = 16u * 1024u * 1024u; /* single-capture cap */
Symptom: a private resource requested by user A gets a direct hit when user B requests the same URL — cross-user data leak.
Cause: the cache primary key has no user dimension by default; Vary: Authorization works only if the responder declares it — undeclared means shared. Multi-user/multi-tenant must supply partition keys.
/* all users, one Client, one Store, no partitions */ pClient = xrtHttpClientCreate(pEngine, &Config); /* B hits A's private response */
/* one partition key per user (or tenant) - the primary key includes the partition */ xrtHttpCallOptionsInit(&Options); Options.Cache.PartitionKey = user_partition(user); pCall = xrtHttpClientDo(pClient, pReq, &Options, on_done, pCtx); /* private isolation is the caller's deliberate decision - the tool doesn't guess for you */
Symptom: in offline mode (CACHE_ONLY) with no record, 504 returns — the code walks the "gateway failure" alert path.
Cause: this 504 is synthesized local semantics: "network forbidden and no cache" — it is not a network error but the deterministic outcome of offline policy. Handling belongs in the "offline empty" branch (prompt/degraded UI), not retry or alerts.
if ( status == 504 ) { alert_gateway_down(); /* an offline miss taken as a network fault */ }
if ( offline_mode && status == 504 ) { show_offline_empty(url); /* locally synthesized 504: offline and uncached */ }
Programming Exercises
Fire twice at the same URL on a local service: compare the second request's Info.ResponseWireBytes (should be 0 — hit) and the age information in the response headers. Then a third with RELOAD. Acceptance criteria: the three Wire byte counts match mode semantics; the hit body is byte-identical to the origin's first response.
The server returns a response with an ETag; request again after expiry (or forced RELOAD) — via packet capture or server logs, confirm the second request carries conditional headers, the response is 304, and the client delivers the merged complete representation (status 200, not 304). Acceptance criteria: the caller never sees the internal 304 (contract verified); the hit body matches the origin's.
Implement fetch_offline_fallback(Url): first CACHE_ONLY (use on hit); on 504, fall back to normal mode, go to network, and write the result to cache. Test the whole flow with the network cut. Acceptance criteria: with cache, works offline; without, an explicit offline error (distinguishing "disconnected" from "server 5xx"); cache writes respect the response's storability (no-store responses never enter — verifiable from later offline behavior).
Summary
| Topic | Quick reference |
|---|---|
| Assembly | Store into the Client configuration makes everything automatic; the Client holds the reference; unconfigured, zero cost |
| Four modes | DEFAULT / Disabled / Reload (source refetch may still update) / Only (no record synthesizes 504) |
| Primary key | method + effective URI + partition + declared Vary; the partition is the caller's isolation tool |
| Not stored | no-store/non-cacheable statuses/Vary:*/authenticated-private forbidden to shared |
| Heuristics | Last-Modified 10% - one-day cap - disableable (conservative defaults) |
| Two cap knobs | MaxBody = single capture (fail-open beyond); Store config = total capacity + eviction |
| Conditional commits | 304/HEAD/Range use versioned conditional Replace; conflicts keep the concurrent updater |
| Range composition | local fragments + origin backfill; MaxRanges default 16 bounds complexity |
| Invalidation | unsafe success -> target URI + same-origin Location/Content-Location |
| Protocol family | policy/control/time/validate/range/store/status - independently trimmable, unit-testable |
📖 API references in this chapter:xhttp-http_cache (10 entries)xhttp-http_client (101 entries)
Click for full signatures, parameter constraints, return values and error notes