HTTP cache headers
The companion crate gpui-query-http turns your server’s HTTP cache headers into a gpui-query CachePolicy. Two pieces work together: a parser that derives the policy from response headers, and an in-memory HttpCache you can layer over any HTTP client. Together they let the server drive cache TTLs and enable cheap 304 Not Modified refetches after relaunch.
cargo add gpui-query-httpServer wins
Section titled “Server wins”A fetcher normally returns Result<T, E> and the resource keeps whatever CachePolicy the caller asked for. When the server knows better (it sent Cache-Control: max-age=30, after all), return Result<Fetched<T>, E> from a *_with_policy hook and the server’s policy overrides the caller’s on success.
Parse the response headers with cache_policy_from_headers, hand the result to Fetched::with_policy, and consume the resource with use_query_with_policy:
use gpui_query::core::{CachePolicy, Fetched};use gpui_query::hook::use_query_with_policy;use gpui_query_http::cache_policy_from_headers;
async fn fetch_user(id: u64) -> Result<Fetched<User>, MyError> { let resp = my_client.get(&format!("/users/{id}")).await?; let policy = cache_policy_from_headers(resp.headers()) .unwrap_or(CachePolicy::NoCache); Ok(Fetched::with_policy(resp.json::<User>().await?, policy))}The resource adopts the server’s TTL for as long as that value is fresh. use_query_with_policy is the server-wins variant of use_query; on refetch, fetch_query_with_policy reapplies the same override.
cache_policy_from_headers rules
Section titled “cache_policy_from_headers rules”cache_policy_from_headers(headers: &http::HeaderMap) -> Result<CachePolicy, ParseError> reads Cache-Control per [RFC 9111]. Rules, in priority order:
Cache-Control: no-storeorno-cache(bare or any value) →CachePolicy::NoCache. These win immediately: a malformed directive that follows them (no-store, max-age=abc) must not surface as a parse error.Cache-Control: max-age=N(seconds) →CachePolicy::Ttl { ttl_ms: N * 1000 }. Ifstale-while-revalidate=Mis also present →CachePolicy::StaleWhileRevalidate { ttl_ms: N * 1000, stale_ms: M * 1000 }.- Otherwise →
CachePolicy::NoCache(no usable cache directives).
A few specifics:
s-maxage(the shared-cache directive) is treated likemax-ageand takes precedence when both are present.- Directive names are matched case-insensitively (
Max-Age=60works). - Values may be quoted (
max-age="600"). - Multiple
Cache-Controlheaders combine; unknown directives (public,private) are ignored.
The three variants map exactly onto the policies described in Caching:
pub enum CachePolicy { NoCache, Ttl { ttl_ms: u64 }, StaleWhileRevalidate { ttl_ms: u64, stale_ms: u64 },}HttpCache over any backend
Section titled “HttpCache over any backend”HttpCache<B> is a URL-keyed in-memory cache layered over an HttpBackend. Fresh entries short-circuit the network entirely; stale entries are revalidated with conditional headers and a 304 Not Modified re-serves the cached body without a new transfer.
use gpui_query_http::{HttpCache, ReqwestBackend};
let backend = ReqwestBackend::from_client(client);let cache: HttpCache<ReqwestBackend> = HttpCache::new(backend);
let (body, policy, meta) = cache.fetch("/api/users/42").await?;HttpCache::fetch(&self, url: &str) -> Result<(Bytes, CachePolicy, Option<CacheMeta>), HttpError> returns the body, the policy in effect for this response, and the cached CacheMeta when the entry is cacheable.
Library-agnostic by design
Section titled “Library-agnostic by design”HttpBackend is a trait that abstracts a single conditional GET. The crate ships one optional backend, ReqwestBackend, behind the reqwest cargo feature; reqwest is never a hard dependency. Any other request library can implement HttpBackend and plug into HttpCache::new:
pub trait HttpBackend: Send + Sync { type Error: std::error::Error + Send + Sync + 'static; fn fetch( &self, url: &str, conditionals: Conditionals, ) -> impl Future<Output = Result<BackendResponse, Self::Error>> + Send;}Dispatch is static (HttpCache<B: HttpBackend>), so there is no Box<dyn> overhead, and the cache never requires tokio.
Persisting ETags for cheap 304s
Section titled “Persisting ETags for cheap 304s”CacheMeta is the HTTP cache metadata extracted from a response. It holds the validators a backend resends to make a refetch cheap:
pub struct CacheMeta { pub etag: Option<String>, pub last_modified: Option<String>, pub stored_at: std::time::SystemTime, pub fresh_for: Duration, pub stale_for: Duration,}With the persist feature on gpui-query, attach a serialized CacheMeta to a fetched value with Fetched::with_meta. It lands in PersistedEntry::meta and round-trips back through persistence, so after a relaunch the next fetch can send If-None-Match / If-Modified-Since and turn into a 304 with no body transfer:
use gpui_query::core::Fetched;
let fetched = Fetched::with_policy(data, policy) .with_meta(serde_json::to_value(&meta).unwrap());This is what closes the loop with Persistence: the ETag survives a process restart, hydration re-primes the cache, and the first refetch after launch costs a 304 instead of a full download.
Next steps
Section titled “Next steps”- Caching: how
NoCache,Ttl, andStaleWhileRevalidatedecide freshness. - Persistence: how
metatravels throughPersistedEntryand back viahydrate.