http: improve caching

1 - Centralized cache-awareness into Cache and pulled header details out of
    SqliteCache and HttpClient

2 - Added support for expires header

3 - Support caching more status types (but not all, since HttpClient would need
    to be aware of what caching a 3xx/206 means)

4 - Revalidate cares about  "not specified" vs "no-store" vs "stale"
    (e.g. expires=0 means "stale", not fallthrough the last-modified logic)
This commit is contained in:
Karl Seguin committed 2026-09-21 11:38:34 +08:00
1 parent b1ffc164a9
commit 3213342055
3 files changed
+446 -244

No files matched your search

+26 -44
View File
@@ -1249,15 +1249,10 @@ fn cacheLookup(self: *Client, transfer: *Transfer) !bool {
req.url;
transfer._cache_key = key;
const req_headers = try arena.alloc(http.Header, transfer.req_headers.items.len);
for (transfer.req_headers.items, req_headers) |hdr, *out| {
out.* = .{ .name = hdr.name, .value = hdr.value };
}
const cache_result = cache.get(arena.allocator(), .{
.url = key,
.timestamp = lp.datetime.timestamp(.real),
.request_headers = req_headers,
.request_headers = try transfer.cacheRequestHeaders(),
}) catch |e| blk: {
log.err(.cache, "failed to get", .{ .url = req.url, .err = e });
break :blk .miss;
@@ -1327,6 +1322,19 @@ fn cacheRevalidated(self: *Client, transfer: *Transfer) !bool {
return true;
}
// Everything the cache needs about this response. Names and values are
// borrowed from the transfer arena, which outlives the put.
fn cachePutRequest(transfer: *Transfer, rh: *http.ResponseHead) !?Cache.CachePutRequest {
return Cache.tryCache(transfer.arena.allocator(), .{
.timestamp = lp.datetime.timestamp(.real),
.url = transfer._cache_key,
.status = rh.status,
.content_type = rh.contentType(),
.headers = transfer.res.headers,
.request_headers = try transfer.cacheRequestHeaders(),
});
}
// Store a cacheable response at completion, from the materialized response
// headers and the buffered body. Failures are logged, never fatal — the
// consumer gets its response either way.
@@ -1347,49 +1355,13 @@ fn cacheStore(self: *Client, transfer: *Transfer) void {
// could have been disabled while waiting of the response
const cache = self.cache.active() orelse return;
const arena = transfer.arena;
const rh = &(transfer.res.header orelse return);
const headers = transfer.res.headers;
const vary = findHeader(headers, "vary");
const maybe_req = Cache.tryCache(
arena.allocator(),
lp.datetime.timestamp(.real),
transfer._cache_key,
rh.status,
rh.contentType(),
findHeader(headers, "cache-control"),
vary,
findHeader(headers, "age"),
findHeader(headers, "etag"),
findHeader(headers, "last-modified"),
findHeader(headers, "set-cookie") != null,
findHeader(headers, "authorization") != null,
) catch |err| {
const maybe_req = cachePutRequest(transfer, rh) catch |err| {
log.warn(.http, "cache eligibility", .{ .err = err });
return;
};
var req = maybe_req orelse return;
var vary_headers: std.ArrayList(http.Header) = .empty;
if (vary) |vary_str| {
for (transfer.req_headers.items) |hdr| {
var vary_iter = std.mem.splitScalar(u8, vary_str, ',');
while (vary_iter.next()) |part| {
const name = std.mem.trim(u8, part, &std.ascii.whitespace);
if (std.ascii.eqlIgnoreCase(hdr.name, name)) {
// name/value already live in transfer.arena
vary_headers.append(arena.allocator(), .{
.name = hdr.name,
.value = hdr.value,
}) catch return;
}
}
}
}
req.headers = headers;
req.vary_headers = vary_headers.items;
const req = maybe_req orelse return;
if (comptime lp.IS_DEBUG) {
log.debug(.browser, "http cache", .{ .key = transfer._cache_key, .put = req });
@@ -3646,6 +3618,16 @@ pub const Transfer = struct {
return null;
}
// The cache works in plain headers; req_headers carry a `source` alongside
// them that it has no use for.
fn cacheRequestHeaders(self: *const Transfer) ![]http.Header {
const headers = try self.arena.alloc(http.Header, self.req_headers.items.len);
for (self.req_headers.items, headers) |hdr, *out| {
out.* = .{ .name = hdr.name, .value = hdr.value };
}
return headers;
}
fn removeHeader(self: *Transfer, name: []const u8) void {
var i: usize = 0;
while (i < self.req_headers.items.len) {