From 5b994c10cd5b48bd0a7ad852c84c9f0e307151b1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 24 Sep 2026 12:51:51 +0800 Subject: [PATCH] http: preserve preload script errors Preloaded modules would discard errors in the hope that re-fetching the script when it was needed, might work. This reverses that decision and stores the error state. When a preload fails, that failure sticks. This does mean that a transient failure doesn't get a second chance. But it also means we don't log the failure multiple times (I noticed this when using adblocking and seeing duplicate failures for the same endpoint). Since we don't keep trying, it cuts down the CDP flow too. --- src/browser/ScriptManager.zig | 61 +++++++++++++++++-- src/browser/ScriptManagerBase.zig | 47 +++++++++++--- .../tests/fixtures/preload_failed.html | 8 +++ src/browser/tests/fixtures/preload_failed.js | 1 + .../tests/fixtures/preload_failed_module.js | 1 + 5 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 src/browser/tests/fixtures/preload_failed.html create mode 100644 src/browser/tests/fixtures/preload_failed.js create mode 100644 src/browser/tests/fixtures/preload_failed_module.js diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index a0437c64a..08fced465 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -199,7 +199,7 @@ fn waitForPreload(self: *ScriptManager, url: [:0]const u8) ?*Script { _ = client.tickSync(200) catch return null; continue; }, - .done => |script| { + .done, .failed => |script| { // Preload scripts are single-use. We return it and it becomes // the caller's responsibility to free. _ = self.preloaded_scripts.remove(url); @@ -318,7 +318,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e if (mode != .normal) { var preloaded = self.takePreload(remote_url); if (preloaded == null and kind == .module) { - preloaded = self.base.takeModuleHint(remote_url); + preloaded = try self.base.takeModuleHint(remote_url); } if (preloaded) |pre| { if (comptime lp.IS_DEBUG) { @@ -545,6 +545,7 @@ const PreloadedScript = struct { const State = union(enum) { loading: *Script, done: *Script, + failed: *Script, }; pub fn deinit(self: PreloadedScript) void { @@ -583,10 +584,12 @@ const PreloadedScript = struct { log.warn(.http, "script fetch error", .{ .err = err, .req = script.url, .extra = "preload", .status = script.status }); } + script.status = 0; // status == 0 is correctly treated as an error throughout + script.complete = true; + const self: *ScriptManager = @fieldParentPtr("base", script.manager); - _ = self.preloaded_scripts.remove(script.url); + self.preloaded_scripts.getPtr(script.url).?.state = .{ .failed = script }; script.queueHintEvent(.@"error"); - script.deinit(); } // Owner-driven teardown killed this preload fetch via Transfer.kill, which @@ -726,6 +729,52 @@ test "ScriptManager: preload whose submit fails synchronously releases its arena const url = "http://127.0.0.1:9582/fails-at-submit.js"; // A fetch was started (and failed), so the hint's error event fires. try testing.expectEqual(true, try sm.preloadScript(null, url)); - // errorCallback consumed the entry; nothing dangles in the map. - try testing.expectEqual(false, sm.preloaded_scripts.contains(url)); + // The failed entry stays for a