diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 81883a5f..d0e2539b 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -915,23 +915,6 @@ 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); @@ -966,28 +949,17 @@ fn processMessages(self: *Client) !bool { continue; }; - cache: { - if (self.network.cache) |*cache| { - const headers = &transfer.response_header.?; + if (transfer.pending_cache_metadata) |metadata| { + const cache = &self.network.cache.?; - const metadata = try CacheMetadata.fromHeaders( - transfer.req.url, - headers.status, - std.time.timestamp(), - header_list.items, - ) orelse break :cache; + // TODO: Support Vary Keying + const cache_key = transfer.req.url; - // TODO: Support Vary Keying - const cache_key = transfer.req.url; - - log.err(.browser, "http cache", .{ .key = cache_key, .metadata = metadata }); - - cache.put( - metadata, - transfer.body.items, - ) 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", .{ .key = cache_key, .metadata = metadata }); + cache.put(metadata, transfer.pending_cache_body.items) catch |err| { + log.warn(.http, "cache put failed", .{ .err = err }); + }; + log.debug(.browser, "http.cache.put", .{ .url = transfer.req.url }); } } @@ -1184,7 +1156,8 @@ pub const LiveTransfer = struct { // total bytes received in the response, including the response status line, // the headers, and the [encoded] body. bytes_received: usize = 0, - body: std.ArrayListUnmanaged(u8) = .empty, + pending_cache_body: std.ArrayList(u8) = .empty, + pending_cache_metadata: ?CacheMetadata = null, aborted: bool = false, @@ -1236,6 +1209,8 @@ pub const LiveTransfer = struct { self._notified_fail = false; self.response_header = null; self.bytes_received = 0; + self.pending_cache_metadata = null; + self.pending_cache_body = .empty; self._tries += 1; } @@ -1451,6 +1426,40 @@ pub const LiveTransfer = struct { return err; }; + if (transfer.client.network.cache != null and transfer.req.method == .GET) { + const rh = &transfer.response_header.?; + const allocator = transfer.arena.allocator(); + + const maybe_cm = try Cache.tryCache( + allocator, + std.time.timestamp(), + transfer.url, + rh.status, + rh.contentType(), + if (conn.getResponseHeader("cache-control", 0)) |h| h.value else null, + if (conn.getResponseHeader("vary", 0)) |h| h.value else null, + if (conn.getResponseHeader("etag", 0)) |h| h.value else null, + if (conn.getResponseHeader("last-modified", 0)) |h| h.value else null, + if (conn.getResponseHeader("age", 0)) |h| h.value else null, + conn.getResponseHeader("set-cookie", 0) != null, + conn.getResponseHeader("authorization", 0) != null, + ); + + if (maybe_cm) |cm| { + var header_list: std.ArrayList(Net.Header) = .empty; + var it = transfer.responseHeaderIterator(); + while (it.next()) |hdr| { + try header_list.append(allocator, .{ + .name = try allocator.dupe(u8, hdr.name), + .value = try allocator.dupe(u8, hdr.value), + }); + } + + transfer.pending_cache_metadata = cm; + transfer.pending_cache_metadata.?.headers = header_list.items; + } + } + transfer.req.notification.dispatch(.http_response_header_done, &.{ .transfer = transfer, }); @@ -1611,10 +1620,12 @@ pub const LiveTransfer = struct { } const chunk = buffer[0..chunk_len]; - transfer.body.appendSlice(transfer.arena.allocator(), chunk) catch |err| { - log.err(.http, "cache body append", .{ .err = err, .req = transfer }); - return Net.writefunc_error; - }; + if (transfer.pending_cache_metadata != null) { + transfer.pending_cache_body.appendSlice(transfer.arena.allocator(), chunk) catch |err| { + log.err(.http, "cache body append", .{ .err = err, .req = transfer }); + return Net.writefunc_error; + }; + } transfer.req.data_callback(Response.fromLive(transfer), chunk) catch |err| { log.err(.http, "data_callback", .{ .err = err, .req = transfer }); diff --git a/src/network/cache/Cache.zig b/src/network/cache/Cache.zig index ce375852..98bea107 100644 --- a/src/network/cache/Cache.zig +++ b/src/network/cache/Cache.zig @@ -122,63 +122,6 @@ pub const CachedMetadata = struct { cache_control: CacheControl, vary: ?Vary, headers: []const Http.Header, - - pub fn fromHeaders( - url: [:0]const u8, - status: u16, - timestamp: i64, - headers: []const Http.Header, - ) !?CachedMetadata { - var cc: ?CacheControl = null; - 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"; - - // Only cache 200 for now. Technically, we can cache others. - switch (status) { - 200 => {}, - else => return null, - } - - for (headers) |hdr| { - if (std.ascii.eqlIgnoreCase(hdr.name, "cache-control")) { - cc = CacheControl.parse(hdr.value) orelse return null; - } 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); - // Vary: * means the response cannot be cached - if (vary) |v| if (v == .wildcard) return null; - } 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; - } else if (std.ascii.eqlIgnoreCase(hdr.name, "set-cookie")) { - // Don't cache if has Set-Cookie. - return null; - } else if (std.ascii.eqlIgnoreCase(hdr.name, "authorization")) { - // Don't cache if has Authorization. - return null; - } - } - - return .{ - .url = url, - .content_type = content_type, - .status = status, - .stored_at = timestamp, - .age_at_store = age_at_store, - .etag = etag, - .last_modified = last_modified, - .cache_control = cc orelse return null, - .vary = vary, - .headers = headers, - }; - } }; pub const CacheRequest = struct { @@ -194,3 +137,36 @@ pub const CachedResponse = struct { metadata: CachedMetadata, data: CachedData, }; + +pub fn tryCache( + arena: std.mem.Allocator, + timestamp: i64, + url: [:0]const u8, + status: u16, + content_type: ?[]const u8, + cache_control: ?[]const u8, + vary: ?[]const u8, + etag: ?[]const u8, + last_modified: ?[]const u8, + age: ?[]const u8, + has_set_cookie: bool, + has_authorization: bool, +) !?CachedMetadata { + if (status != 200) return null; + if (has_set_cookie) return null; + if (has_authorization) return null; + const cc = CacheControl.parse(cache_control orelse return null) orelse return null; + + return .{ + .url = url, + .content_type = if (content_type) |ct| try arena.dupe(u8, ct) else "application/octet-stream", + .status = status, + .stored_at = timestamp, + .age_at_store = if (age) |a| std.fmt.parseInt(u64, a, 10) catch 0 else 0, + .cache_control = cc, + .vary = if (vary) |v| Vary.parse(v) else null, + .etag = if (etag) |e| try arena.dupe(u8, e) else null, + .last_modified = if (last_modified) |lm| try arena.dupe(u8, lm) else null, + .headers = &.{}, + }; +}