From 2fd8a96c5d763d96e50fb0123ae67feba49db6f4 Mon Sep 17 00:00:00 2001 From: Levi Date: Fri, 18 Sep 2026 00:07:55 +0800 Subject: [PATCH] fix(webview): ignore load notifications from browser error documents --- .../webview/goprovider/events_script_test.go | 49 +++++++++++++++++++ internal/webview/goprovider/provider.go | 9 ++-- .../lib/app/rpc/webview_event_script.dart | 2 + .../lib/app/rpc/webview_event_script.dart | 2 + 4 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 internal/webview/goprovider/events_script_test.go diff --git a/internal/webview/goprovider/events_script_test.go b/internal/webview/goprovider/events_script_test.go new file mode 100644 index 00000000..7a32c5bf --- /dev/null +++ b/internal/webview/goprovider/events_script_test.go @@ -0,0 +1,49 @@ +//go:build cgo + +package goprovider + +import ( + enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview" + webview "github.com/GopeedLab/webview_go" + "github.com/dop251/goja" + "testing" + "time" +) + +func TestInternalErrorDocumentDoesNotReportPageEvents(t *testing.T) { + vm := goja.New() + if _, err := vm.RunString(` + var window = globalThis; + window.top = window; + var location = {protocol: "chrome-error:", href: "chrome-error://chromewebdata/"}; + var notifications = []; + var listeners = []; + function notify(payload) { notifications.push(payload); } + window.addEventListener = (name) => listeners.push(name); + `); err != nil { + t.Fatal(err) + } + if _, err := vm.RunString(buildLoadNotifyScript("notify")); err != nil { + t.Fatal(err) + } + for _, name := range []string{"notifications", "listeners"} { + value, err := vm.RunString(name + ".length") + if err != nil || value.ToInteger() != 0 { + t.Fatalf("internal error page registered %s: %v (%v)", name, value, err) + } + } + // The provider must wait for the native failure, preserving the failed URL. + page := newPageWrapper(enginewebview.OpenOptions{}) + got := make(chan string, 1) + _, err := page.On("load-error", func(e enginewebview.Event) { got <- e.Data["url"].(string) }) + if err != nil { + t.Fatal(err) + } + page.handleNativeEvent(webview.Event{Name: "load-error", URL: "https://example.test/failure", Message: "TLS handshake failed"}) + if err := page.waitForNavigation(time.Second, "load"); err == nil { + t.Fatal("native failure completed navigation successfully") + } + if url := <-got; url != "https://example.test/failure" { + t.Fatal(url) + } +} diff --git a/internal/webview/goprovider/provider.go b/internal/webview/goprovider/provider.go index 756b571b..08046154 100644 --- a/internal/webview/goprovider/provider.go +++ b/internal/webview/goprovider/provider.go @@ -369,7 +369,7 @@ func (p *pageWrapper) Close() error { } func (p *pageWrapper) tracef(format string, args ...any) { - if !strings.Contains(p.opts.Title, "youtube-sabr") && p.opts.Title != "WebView event contract" { + if !strings.Contains(p.opts.Title, "youtube-sabr") { return } fmt.Printf("goprovider "+format+"\n", args...) @@ -387,14 +387,12 @@ func (p *pageWrapper) waitForNavigation(timeout time.Duration, waitUntil string) case <-timer.C: return fmt.Errorf("webview navigation timeout") case <-p.sameDocument: - p.tracef("goto completed via same-document notification") return nil case <-p.domReady: if waitUntil == "domcontentloaded" { return nil } case <-p.loads: - p.tracef("goto completed via load notification") return nil } } @@ -479,7 +477,6 @@ func (p *pageWrapper) handleCallback(payload string) error { } return nil } - p.tracef("page event: event=%s load=%v url=%s sameDocument=%v", msg.Event, msg.Load, msg.URL, msg.SameDocument) if msg.Event == "domcontentloaded" { select { case p.domReady <- struct{}{}: @@ -607,6 +604,9 @@ func buildLoadNotifyScript(callbackName string) string { callbackJSON, _ := json.Marshal(callbackName) return fmt.Sprintf(`(() => { if (window !== window.top) return; + // WebView2 runs init scripts in its internal error document too. Its load + // must not complete goto() before the native NavigationCompleted failure. + if (location.protocol === "chrome-error:") return; const send = (message) => { const cb = globalThis[%s]; if (typeof cb === "function") cb(JSON.stringify(message)); @@ -650,7 +650,6 @@ func (p *pageWrapper) failPendingLocked() { } func (p *pageWrapper) handleNativeEvent(event webview.Event) { - p.tracef("native event: %+v", event) switch event.Name { case "load-error": p.events.Emit(enginewebview.Event{Name: "load-error", Data: map[string]any{"url": event.URL, "message": event.Message}}) diff --git a/ui/flutter/lib/app/rpc/webview_event_script.dart b/ui/flutter/lib/app/rpc/webview_event_script.dart index 94c4ff95..74b9c62f 100644 --- a/ui/flutter/lib/app/rpc/webview_event_script.dart +++ b/ui/flutter/lib/app/rpc/webview_event_script.dart @@ -5,6 +5,8 @@ String buildWebViewEventScript(String channel) => ''' (() => { if (window !== window.top) return; + // Browser error documents are reported by native load-error callbacks. + if (location.protocol === 'chrome-error:') return; const pending = []; const flush = () => { const bridge = window.flutter_inappwebview; diff --git a/ui/flutter_old/lib/app/rpc/webview_event_script.dart b/ui/flutter_old/lib/app/rpc/webview_event_script.dart index 6ef8557a..0bc265d4 100644 --- a/ui/flutter_old/lib/app/rpc/webview_event_script.dart +++ b/ui/flutter_old/lib/app/rpc/webview_event_script.dart @@ -4,6 +4,8 @@ import 'dart:convert'; String buildWebViewEventScript(String channel) => ''' (() => { if (window !== window.top) return; + // Browser error documents are reported by native load-error callbacks. + if (location.protocol === 'chrome-error:') return; const pending = []; const flush = () => { const bridge = window.flutter_inappwebview;