{"record":{"id":"808b5a1a411e8864","repo":"GopeedLab/gopeed","slug":"webview-page-is-closed","errorCode":null,"errorMessage":"webview page is closed","messagePattern":"webview page is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/webview/goprovider/provider.go","lineNumber":202,"sourceCode":"\treturn nil\n}\n\nfunc (p *pageWrapper) Execute(expression string, args ...any) (any, error) {\n\tstartedAt := time.Now()\n\tif args == nil {\n\t\targs = []any{}\n\t}\n\targsJSON, err := json.Marshal(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treqID := fmt.Sprintf(\"req-%d\", providerSeq.Add(1))\n\tch := make(chan evalResult, 1)\n\n\tp.mu.Lock()\n\tif p.closed {\n\t\tp.mu.Unlock()\n\t\treturn nil, fmt.Errorf(\"webview page is closed\")\n\t}\n\tp.pending[reqID] = ch\n\tp.mu.Unlock()\n\n\tscript := buildExecuteScript(p.callbackName, reqID, expression, string(argsJSON))\n\tif err := p.dispatch(func(w webview.WebView) error {\n\t\tw.Eval(script)\n\t\treturn nil\n\t}); err != nil {\n\t\tp.mu.Lock()\n\t\tdelete(p.pending, reqID)\n\t\tp.mu.Unlock()\n\t\treturn nil, err\n\t}\n\n\tresult := <-ch\n\tif result.err != nil {\n\t\tp.tracef(\"execute failed %dms err=%v\", time.Since(startedAt).Milliseconds(), result.err)","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/GopeedLab/gopeed/blob/7b7327ffb30816273a74b142cccc0bc10c5a4c67/internal/webview/goprovider/provider.go#L184-L220","documentation":"pageWrapper.Execute (internal/webview/goprovider/provider.go:186) marshals the call, allocates a reqID channel, then checks p.closed under the page mutex. Every Execute/Evaluate call made after Close() set that flag fails immediately with this error instead of dispatching to the dead webview.","triggerScenarios":"Calling page.Execute or the navigation-state polling after page.Close() returned; a concurrent Close() that wins the race while an Evaluate is being set up; keeping a stale page reference in application state after closing the window.","commonSituations":"Cleanup paths that close the page while background pollers (progress tickers, state watchers) are still running; promise chains that resolve after the user closed the window; caches of page objects that outlive their window.","solutions":["Stop all background tasks that touch the page before calling Close()","Treat Close as terminal: never reuse the wrapper, create a new page for the next navigation","Route every page access through your own guard that checks a 'closed' flag you set at shutdown"],"exampleFix":"// before\npage.Close()\n_, err := page.Execute(`() => location.href`) // \"webview page is closed\"\n\n// after\ncancelPollers()\nif err := page.Close(); err != nil { return err }\npage = nil // make stale references obvious; open a new page when needed","handlingStrategy":"validation","validationCode":"// Track page lifetime in the caller\ntype pageRef struct {\n    mu     sync.Mutex\n    page   enginewebview.Page\n    closed bool\n}\n\nfunc (r *pageRef) execute(expr string, args []any) (any, error) {\n    r.mu.Lock()\n    if r.closed || r.page == nil {\n        r.mu.Unlock()\n        return nil, errors.New(\"page already closed\")\n    }\n    r.mu.Unlock()\n    return r.page.Execute(expr, args)\n}","typeGuard":null,"tryCatchPattern":"val, err := r.execute(expr, args)\nif err != nil && strings.Contains(err.Error(), \"webview page is closed\") {\n    return nil, ErrSessionClosed // map to your own terminal condition; do not retry\n}","preventionTips":["Cancel pollers/watchers before Close()","Never cache page objects beyond their window's lifetime","Treat this error as terminal for the page — open a new one instead of retrying"],"tags":["webview","lifecycle","use-after-close","concurrency"],"backgroundTag":null,"analyzedSha":"7b7327ffb30816273a74b142cccc0bc10c5a4c67","analyzedAt":"2026-08-16T02:51:03.250Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}