From 8684c87edfd54613a84ba89d03b954ddef0a526f Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Fri, 20 Mar 2026 01:34:51 -0700 Subject: [PATCH] cache headers along with response --- src/browser/HttpClient.zig | 66 ++++++++++++-------- src/network/cache/Cache.zig | 111 +++++++++++++++++++++++++++++++++- src/network/cache/FsCache.zig | 34 +++++++++++ 3 files changed, 184 insertions(+), 27 deletions(-) diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 29f610eb..52dd925d 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -33,6 +33,7 @@ const RobotStore = Robots.RobotStore; const WebBotAuth = @import("../network/WebBotAuth.zig"); const Cache = @import("../network/cache/Cache.zig"); +const CacheMetadata = Cache.CachedMetadata; const CachedResponse = Cache.CachedResponse; 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 // will load more resources. self.endTransfer(transfer); @@ -941,37 +959,36 @@ fn processMessages(self: *Client) !bool { continue; }; - if (self.network.cache) |*cache| { - var headers = &transfer.response_header.?; + cache: { + if (self.network.cache) |*cache| { + const headers = &transfer.response_header.?; - if (transfer.req.method == .GET and headers.status == 200) { - cache.put( + const metadata = try CacheMetadata.fromHeaders( transfer.req.url, - .{ - .url = transfer.req.url, - .content_type = headers.contentType() orelse "application/octet-stream", - .status = headers.status, - .stored_at = std.time.timestamp(), - .age_at_store = 0, - .max_age = 3600, - .etag = null, - .last_modified = null, - .must_revalidate = false, - .no_cache = false, - .immutable = false, - .vary = null, - }, + headers.status, + std.time.timestamp(), + header_list.items, + ) orelse break :cache; + + // TODO: Support Vary Keying + const cache_key = transfer.req.url; + + log.err(.browser, "http cache", .{ .key = cache_key, .metadata = metadata }); + + cache.put( + cache_key, + metadata, transfer.body.items, ) catch |err| log.warn(.http, "cache put failed", .{ .err = err }); 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; } @@ -1133,8 +1150,7 @@ pub const Response = struct { pub fn headerIterator(self: Response) HeaderIterator { return switch (self.inner) { .live => |live| live.responseHeaderIterator(), - // TODO: Cache HTTP Headers - .cached => unreachable, + .cached => |c| HeaderIterator{ .list = .{ .list = c.metadata.headers } }, }; } diff --git a/src/network/cache/Cache.zig b/src/network/cache/Cache.zig index 56bc6c10..79374e66 100644 --- a/src/network/cache/Cache.zig +++ b/src/network/cache/Cache.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const Http = @import("../http.zig"); const FsCache = @import("FsCache.zig"); /// 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 { url: [:0]const u8, content_type: []const u8, @@ -52,17 +102,74 @@ pub const CachedMetadata = struct { etag: ?[]const u8, // for If-Modified-Since last_modified: ?[]const u8, + // If non-null, must be incorporated into cache key. + vary: ?[]const u8, must_revalidate: bool, no_cache: bool, immutable: bool, - // If non-null, must be incorporated into cache key. - vary: ?[]const u8, + headers: []const Http.Header, + + 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 { allocator.free(self.url); 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.last_modified) |lm| allocator.free(lm); if (self.vary) |v| allocator.free(v); diff --git a/src/network/cache/FsCache.zig b/src/network/cache/FsCache.zig index db916cfa..c931e4a2 100644 --- a/src/network/cache/FsCache.zig +++ b/src/network/cache/FsCache.zig @@ -18,6 +18,7 @@ const std = @import("std"); const Cache = @import("Cache.zig"); +const Http = @import("../http.zig"); const CachedMetadata = Cache.CachedMetadata; const CachedResponse = Cache.CachedResponse; @@ -73,6 +74,12 @@ fn serializeMeta(writer: *std.Io.Writer, meta: *const CachedMetadata) !void { meta.immutable, }); 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 { @@ -145,6 +152,32 @@ fn deserializeMeta(allocator: std.mem.Allocator, file: std.fs.File) !CachedMetad 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 .{ .url = url, .content_type = content_type, @@ -158,6 +191,7 @@ fn deserializeMeta(allocator: std.mem.Allocator, file: std.fs.File) !CachedMetad .no_cache = no_cache, .immutable = immutable, .vary = vary, + .headers = headers, }; }