cache headers along with response

This commit is contained in:
Muki Kiboigo
2026-03-20 01:34:51 -07:00
parent 00e0c22a76
commit 8684c87edf
3 changed files with 184 additions and 27 deletions

View File

@@ -33,6 +33,7 @@ const RobotStore = Robots.RobotStore;
const WebBotAuth = @import("../network/WebBotAuth.zig"); const WebBotAuth = @import("../network/WebBotAuth.zig");
const Cache = @import("../network/cache/Cache.zig"); const Cache = @import("../network/cache/Cache.zig");
const CacheMetadata = Cache.CachedMetadata;
const CachedResponse = Cache.CachedResponse; const CachedResponse = Cache.CachedResponse;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -907,6 +908,23 @@ fn processMessages(self: *Client) !bool {
} }
} }
const allocator = transfer.arena.allocator();
var header_list: std.ArrayList(Net.Header) = .empty;
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
header_list.append(
allocator,
.{
.name = try allocator.dupe(u8, hdr.name),
.value = try allocator.dupe(u8, hdr.value),
},
) catch |err| {
log.warn(.http, "cache header collect failed", .{ .err = err });
break;
};
}
// release it ASAP so that it's available; some done_callbacks // release it ASAP so that it's available; some done_callbacks
// will load more resources. // will load more resources.
self.endTransfer(transfer); self.endTransfer(transfer);
@@ -941,37 +959,36 @@ fn processMessages(self: *Client) !bool {
continue; continue;
}; };
if (self.network.cache) |*cache| { cache: {
var headers = &transfer.response_header.?; if (self.network.cache) |*cache| {
const headers = &transfer.response_header.?;
if (transfer.req.method == .GET and headers.status == 200) { const metadata = try CacheMetadata.fromHeaders(
cache.put(
transfer.req.url, transfer.req.url,
.{ headers.status,
.url = transfer.req.url, std.time.timestamp(),
.content_type = headers.contentType() orelse "application/octet-stream", header_list.items,
.status = headers.status, ) orelse break :cache;
.stored_at = std.time.timestamp(),
.age_at_store = 0, // TODO: Support Vary Keying
.max_age = 3600, const cache_key = transfer.req.url;
.etag = null,
.last_modified = null, log.err(.browser, "http cache", .{ .key = cache_key, .metadata = metadata });
.must_revalidate = false,
.no_cache = false, cache.put(
.immutable = false, cache_key,
.vary = null, metadata,
},
transfer.body.items, transfer.body.items,
) catch |err| log.warn(.http, "cache put failed", .{ .err = err }); ) catch |err| log.warn(.http, "cache put failed", .{ .err = err });
log.debug(.browser, "http.cache.put", .{ .url = transfer.req.url }); log.debug(.browser, "http.cache.put", .{ .url = transfer.req.url });
} }
} }
transfer.req.notification.dispatch(.http_request_done, &.{
.transfer = transfer,
});
processed = true;
} }
transfer.req.notification.dispatch(.http_request_done, &.{
.transfer = transfer,
});
processed = true;
} }
return processed; return processed;
} }
@@ -1133,8 +1150,7 @@ pub const Response = struct {
pub fn headerIterator(self: Response) HeaderIterator { pub fn headerIterator(self: Response) HeaderIterator {
return switch (self.inner) { return switch (self.inner) {
.live => |live| live.responseHeaderIterator(), .live => |live| live.responseHeaderIterator(),
// TODO: Cache HTTP Headers .cached => |c| HeaderIterator{ .list = .{ .list = c.metadata.headers } },
.cached => unreachable,
}; };
} }

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Http = @import("../http.zig");
const FsCache = @import("FsCache.zig"); const FsCache = @import("FsCache.zig");
/// A browser-wide cache for resources across the network. /// A browser-wide cache for resources across the network.
@@ -39,6 +40,55 @@ pub fn put(self: *Cache, key: []const u8, metadata: CachedMetadata, body: []cons
}; };
} }
pub const CacheControl = struct {
max_age: ?u64 = null,
must_revalidate: bool = false,
no_cache: bool = false,
no_store: bool = false,
immutable: bool = false,
pub fn parse(value: []const u8) CacheControl {
var cc: CacheControl = .{};
var iter = std.mem.splitScalar(u8, value, ',');
while (iter.next()) |part| {
const directive = std.mem.trim(u8, part, &std.ascii.whitespace);
if (std.ascii.eqlIgnoreCase(directive, "no-store")) {
cc.no_store = true;
} else if (std.ascii.eqlIgnoreCase(directive, "no-cache")) {
cc.no_cache = true;
} else if (std.ascii.eqlIgnoreCase(directive, "must-revalidate")) {
cc.must_revalidate = true;
} else if (std.ascii.eqlIgnoreCase(directive, "immutable")) {
cc.immutable = true;
} else if (std.ascii.startsWithIgnoreCase(directive, "max-age=")) {
cc.max_age = std.fmt.parseInt(u64, directive[8..], 10) catch null;
} else if (std.ascii.startsWithIgnoreCase(directive, "s-maxage=")) {
// s-maxage takes precedence over max-age
cc.max_age = std.fmt.parseInt(u64, directive[9..], 10) catch cc.max_age;
}
}
return cc;
}
};
pub const Vary = union(enum) {
wildcard: void,
value: []const u8,
pub fn parse(value: []const u8) Vary {
if (std.mem.eql(u8, value, "*")) return .wildcard;
return .{ .value = value };
}
pub fn toString(self: Vary) []const u8 {
return switch (self) {
.wildcard => "*",
.value => |v| v,
};
}
};
pub const CachedMetadata = struct { pub const CachedMetadata = struct {
url: [:0]const u8, url: [:0]const u8,
content_type: []const u8, content_type: []const u8,
@@ -52,17 +102,74 @@ pub const CachedMetadata = struct {
etag: ?[]const u8, etag: ?[]const u8,
// for If-Modified-Since // for If-Modified-Since
last_modified: ?[]const u8, last_modified: ?[]const u8,
// If non-null, must be incorporated into cache key.
vary: ?[]const u8,
must_revalidate: bool, must_revalidate: bool,
no_cache: bool, no_cache: bool,
immutable: bool, immutable: bool,
// If non-null, must be incorporated into cache key. headers: []const Http.Header,
vary: ?[]const u8,
pub fn fromHeaders(
url: [:0]const u8,
status: u16,
timestamp: i64,
headers: []const Http.Header,
) !?CachedMetadata {
var cc: CacheControl = .{};
var vary: ?Vary = null;
var etag: ?[]const u8 = null;
var last_modified: ?[]const u8 = null;
var age_at_store: u64 = 0;
var content_type: []const u8 = "application/octet-stream";
for (headers) |hdr| {
if (std.ascii.eqlIgnoreCase(hdr.name, "cache-control")) {
cc = CacheControl.parse(hdr.value);
} else if (std.ascii.eqlIgnoreCase(hdr.name, "etag")) {
etag = hdr.value;
} else if (std.ascii.eqlIgnoreCase(hdr.name, "last-modified")) {
last_modified = hdr.value;
} else if (std.ascii.eqlIgnoreCase(hdr.name, "vary")) {
vary = Vary.parse(hdr.value);
} else if (std.ascii.eqlIgnoreCase(hdr.name, "age")) {
age_at_store = std.fmt.parseInt(u64, hdr.value, 10) catch 0;
} else if (std.ascii.eqlIgnoreCase(hdr.name, "content-type")) {
content_type = hdr.value;
}
}
// return null for uncacheable responses
if (cc.no_store) return null;
if (vary) |v| if (v == .wildcard) return null;
const resolved_max_age = cc.max_age orelse return null;
return .{
.url = url,
.content_type = content_type,
.status = status,
.stored_at = timestamp,
.age_at_store = age_at_store,
.max_age = resolved_max_age,
.etag = etag,
.last_modified = last_modified,
.must_revalidate = cc.must_revalidate,
.no_cache = cc.no_cache,
.immutable = cc.immutable,
.vary = if (vary) |v| v.toString() else null,
.headers = headers,
};
}
pub fn deinit(self: CachedMetadata, allocator: std.mem.Allocator) void { pub fn deinit(self: CachedMetadata, allocator: std.mem.Allocator) void {
allocator.free(self.url); allocator.free(self.url);
allocator.free(self.content_type); allocator.free(self.content_type);
for (self.headers) |header| {
allocator.free(header.name);
allocator.free(header.value);
}
allocator.free(self.headers);
if (self.etag) |e| allocator.free(e); if (self.etag) |e| allocator.free(e);
if (self.last_modified) |lm| allocator.free(lm); if (self.last_modified) |lm| allocator.free(lm);
if (self.vary) |v| allocator.free(v); if (self.vary) |v| allocator.free(v);

View File

@@ -18,6 +18,7 @@
const std = @import("std"); const std = @import("std");
const Cache = @import("Cache.zig"); const Cache = @import("Cache.zig");
const Http = @import("../http.zig");
const CachedMetadata = Cache.CachedMetadata; const CachedMetadata = Cache.CachedMetadata;
const CachedResponse = Cache.CachedResponse; const CachedResponse = Cache.CachedResponse;
@@ -73,6 +74,12 @@ fn serializeMeta(writer: *std.Io.Writer, meta: *const CachedMetadata) !void {
meta.immutable, meta.immutable,
}); });
try writer.flush(); try writer.flush();
try writer.print("{d}\n", .{meta.headers.len});
for (meta.headers) |hdr| {
try writer.print("{s}\n{s}\n", .{ hdr.name, hdr.value });
}
try writer.flush();
} }
fn deserializeMetaOptionalString(bytes: []const u8) ?[]const u8 { fn deserializeMetaOptionalString(bytes: []const u8) ?[]const u8 {
@@ -145,6 +152,32 @@ fn deserializeMeta(allocator: std.mem.Allocator, file: std.fs.File) !CachedMetad
break :blk try deserializeMetaBoolean(line); break :blk try deserializeMetaBoolean(line);
}; };
const headers = blk: {
const line = try reader.takeDelimiter('\n') orelse return error.Malformed;
const count = std.fmt.parseInt(usize, line, 10) catch return error.Malformed;
const hdrs = try allocator.alloc(Http.Header, count);
errdefer allocator.free(hdrs);
for (hdrs) |*hdr| {
const name = try reader.takeDelimiter('\n') orelse return error.Malformed;
const value = try reader.takeDelimiter('\n') orelse return error.Malformed;
hdr.* = .{
.name = try allocator.dupe(u8, name),
.value = try allocator.dupe(u8, value),
};
}
break :blk hdrs;
};
errdefer {
for (headers) |hdr| {
allocator.free(hdr.name);
allocator.free(hdr.value);
}
allocator.free(headers);
}
return .{ return .{
.url = url, .url = url,
.content_type = content_type, .content_type = content_type,
@@ -158,6 +191,7 @@ fn deserializeMeta(allocator: std.mem.Allocator, file: std.fs.File) !CachedMetad
.no_cache = no_cache, .no_cache = no_cache,
.immutable = immutable, .immutable = immutable,
.vary = vary, .vary = vary,
.headers = headers,
}; };
} }