From 042df666465115e080a2573483cd09770dcdb5cd Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 5 Sep 2026 19:42:22 +0800 Subject: [PATCH] webapi: don't fire error event when a worker script fetch is aborted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit terminate() can cancel a still-inflight script fetch. That's a cancellation, not a failure: a terminated worker must fire no further events, and Chrome is silent here. We logged an error and fired an ErrorEvent — observable by Cloudflare challenge scripts, which create a blob-URL worker and terminate it in the same tick. On error.TransferCanceled, skip the log and the error event; same log skip in SharedWorkerGlobalScope. This was tried in https://github.com/lightpanda-io/browser/pull/3189, but is now ambiguous thanks to https://github.com/lightpanda-io/browser/pull/3368 which introduced a distinct `error.TransferCanceled`. --- src/browser/tests/worker/worker.html | 39 +++++++++++++++++++ .../webapi/SharedWorkerGlobalScope.zig | 12 ++++-- src/browser/webapi/Worker.zig | 20 +++++++--- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/browser/tests/worker/worker.html b/src/browser/tests/worker/worker.html index 5a8562968..29472fa72 100644 --- a/src/browser/tests/worker/worker.html +++ b/src/browser/tests/worker/worker.html @@ -553,3 +553,42 @@ }); } + + + + diff --git a/src/browser/webapi/SharedWorkerGlobalScope.zig b/src/browser/webapi/SharedWorkerGlobalScope.zig index b7f3b9cbb..2d07bb5be 100644 --- a/src/browser/webapi/SharedWorkerGlobalScope.zig +++ b/src/browser/webapi/SharedWorkerGlobalScope.zig @@ -250,10 +250,14 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { self._http_transfer = null; self.releaseScriptArena(); - log.err(.browser, "shared worker fetch error", .{ - .url = self._url, - .err = err, - }); + // TransferCanceled is teardown cancelling a still-inflight script fetch, + // not a load failure. + if (err != error.TransferCanceled) { + log.err(.browser, "shared worker fetch error", .{ + .url = self._url, + .err = err, + }); + } // The worker will never load and onconnect will never be registered. // Drain the buffered connects so they get dispatched (and dropped at the diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig index a1c5cd22f..b2cbab0db 100644 --- a/src/browser/webapi/Worker.zig +++ b/src/browser/webapi/Worker.zig @@ -268,10 +268,16 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { self._http_transfer = null; self.releaseScriptArena(); - log.err(.browser, "worker fetch error", .{ - .url = self._url, - .err = err, - }); + // TransferCanceled is not a load failure: terminate() (or worker teardown) + // cancelled a still-inflight script fetch. We shouldn't fireErrorEvent + // (or bother logging) + const canceled = err == error.TransferCanceled; + if (!canceled) { + log.err(.browser, "worker fetch error", .{ + .url = self._url, + .err = err, + }); + } // The worker will never load and onmessage will never be registered. // Drain any buffered messages so they get dispatched (and silently @@ -280,7 +286,9 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { self._script_loaded = true; self._worker_scope.drainPendingMessages(); - self.fireErrorEvent(@errorName(err), null); + if (!canceled) { + self.fireErrorEvent(@errorName(err), null); + } } fn releaseScriptArena(self: *Worker) void { @@ -481,7 +489,7 @@ pub const JsApi = struct { const testing = @import("../../testing.zig"); test "WebApi: Worker" { - testing.silenceLog(&.{.http}); + testing.silenceLog(&.{.http, .browser, .browser}); // Worker tests chain a worker-script fetch with a dynamic-import fetch // and a cross-context postMessage. The default 2 s assertion budget can