MicrosoftEdge / MicrosoftEdge/WebView2Feedback
[Problem/Bug]: Multi-CoreWebView2 host: closing one window collateral-detaches another window's CDP target (Playwright integration repro)
@ambikakunnath is already working on this.
Since May 14, 2026.
- Dominant language
- PowerShell
- Stars
- 526
- Forks
- 67
- PR merge metrics
- No merged PRs in 30d
Description
What happened?
Application hosts multiple CoreWebView2 instances sharing a single CoreWebView2Environment (one user data folder). Launched with --remote-debugging-port=NNNN for automation via Playwright chromium.connectOverCDP('http://127.0.0.1:NNNN').
Calling CoreWebView2Controller.Close() on a non-primary (audience) instance causes the WebView2 CDP server to emit Target.targetDestroyed for the closed audience target — and within ~28ms — also marks the PRIMARY (operator) instance's CDP target as detached. The operator's CoreWebView2HostWindow OnClosed event NEVER fires; the operator's CoreWebView2.ProcessFailed event NEVER fires; and Application.Windows.Count correctly drops from N to N-1 reflecting only the audience window's destruction. At the .NET layer, the operator window is alive and unaffected.
But Playwright, observing CDP events from the shared remote-debugging-port, receives Target.detachedFromTarget for the operator's targetId and marks the operator's Page reference closed. Subsequent operations on the operator's Page reject with Target page, context or browser has been closed.
Empirical smoking-gun timing
From CDP diagnostic log captured via context.newCDPSession(page) listening for raw Target.targetCreated / Target.targetDestroyed events plus page.on('close') listeners:
[20:48:50.038] [CDP Target.targetInfoChanged] id=1DF9924CD6E0 attached=false url=https://app.local/#/hybrid-pro-display
[20:48:50.041] [page.on('close')] AUDIENCE url=https://app.local/#/hybrid-pro-display
[20:48:50.042] [CDP Target.targetDestroyed] id=1DF9924CD6E0
[20:48:50.070] [page.on('close')] PRIMARY OPERATOR PAGE CLOSED url=https://app.local/#/draw-control
That is: 28ms after Target.targetDestroyed fires for the audience HybridPro target (id 1DF9924CD6E0), the operator's Playwright Page (id B83FA7FA8A2F..., url /draw-control) receives a close event — despite the operator's .NET wrapper window remaining alive (verified via instrumentation across WebView2HostWindow.OnClosed, CoreWebView2.ProcessFailed, Application.Windows.Count, and Application.Current.MainWindow all reflecting the operator still present).
Both targets shared the same browserContextId (AB699118ADF57A12571DEC25D5553A3B), confirming they were registered against the same WebView2 CDP browser context (single CoreWebView2Environment). Operator pid 18328; audience pid 9644 — separate renderer processes, but the same browser process per WebView2 Process Model docs.
Logger output from the .NET host process goes silent ~4.3s BEFORE Playwright detects the operator Page closure — suggesting WebView2's internal CDP server enters an unresponsive state for the operator's target while keeping the .NET wrapper window alive at the application layer.
Importance
Important. Blocks end-to-end test development for any multi-window WebView2 app that uses cross-instance lifecycle operations (e.g., a "Close audience display" UI affordance). Workarounds tried so far either hang Playwright afterEach for 120s (default page.close() waits for Target.targetDestroyed that never resolves because the .NET host owns the window) or do not prevent the operator-target disruption even when runBeforeUnload: true is used.
Distinction from #4587 (related, distinct symptom)
MicrosoftEdge/WebView2Feedback#4587 reports a related symptom in the same architectural scenario:
- Multiple
CoreWebView2instances ✓ - Shared
CoreWebView2Environment✓ - Accessed via
--remote-debugging-port✓ - Cross-instance lifecycle ops trigger cross-control failure ✓
But #4587's manifestation is CoreWebView2.ProcessFailed firing on every control with ProcessFailedKind=BrowserProcessExited, ExitCode=-1073741819 (STATUS_ACCESS_VIOLATION) — the entire browser process crashes.
This issue's manifestation is SILENT: no ProcessFailed event fires anywhere, the browser process keeps running (other CoreWebView2 instances continue working at the .NET layer), but the operator's CDP target is detached from automation's perspective. Different fault line; related family.
The workaround comment on #4587 ("give the WebView2 control its own CoreWebView2Environment and User Data Folder") may apply here too; not yet empirically tested for the CDP-target-detach variant.
Expected behavior
Per WebView2 Process Model docs (https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/process-model):
"When the first
WebView2instance is created for a given user data folder, the browser process for the WebView2 Runtime processes collection that is associated with that user data folder will be started. All additional processes will be managed by the lifetime of that browser process."
And per CoreWebView2Controller.Close docs (https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2controller#close):
"Cleaning up the browser instance releases the resources powering the WebView. The browser instance is shut down if no other WebViews are using it. After running Close, all methods fail and event handlers stop running."
The documented contract: closing one CoreWebView2 should NOT shut down the shared browser instance when sibling CoreWebView2 instances still reference it. The observed behavior — sibling target detaches from the CDP perspective despite the sibling's .NET wrapper still being alive — appears to violate that contract at the CDP/target-registration layer.
Repro steps
- Create a WPF (or WinForms / WinUI) host application with at least 2
CoreWebView2instances in separate top-levelWindows. - Configure all instances to share one
CoreWebView2Environment(single user data folder) — i.e., callEnsureCoreWebView2Async(Nothing)(or pass the same explicit environment) in every host window. - Launch the app with
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9224set in the process environment. Wait for the operator's firstCoreWebView2to be ready; verify port 9224 is listening. - Connect Playwright from a Node test process:
const { chromium } = require('@playwright/test'); const browser = await chromium.connectOverCDP('http://127.0.0.1:9224'); const context = browser.contexts()[0]; const operatorPage = context.pages()[0]; - Attach a raw CDP session to capture lifecycle events:
const cdp = await context.newCDPSession(operatorPage); await cdp.send('Target.setDiscoverTargets', { discover: true }); cdp.on('Target.targetCreated', e => console.log('created', e.targetInfo.url)); cdp.on('Target.targetDestroyed', e => console.log('destroyed', e.targetId)); cdp.on('Target.targetInfoChanged', e => console.log('changed', e.targetInfo.url, e.targetInfo.attached)); operatorPage.on('close', () => console.log('OPERATOR PAGE close event')); - Drive the operator UI to spawn an audience
CoreWebView2instance in a separate WPFWindow(still using the sameCoreWebView2Environment). Confirm it appears incontext.pages(). - Wait for the audience to finish its initial paint.
- Close the audience from the .NET side by calling
CoreWebView2HostWindow.Close()(or equivalent — anything that disposes the audience'sCoreWebView2Controller). - Within the next 100ms, observe the diagnostic log:
Target.targetInfoChangedfor the audience target withattached=falseTarget.targetDestroyedfor the audience targetpage.on('close')fires on the OPERATOR Page within ~30ms of the audiencetargetDestroyed
- Attempt any operation on the operator Page:
Rejects withawait operatorPage.goto('https://app.local/#/draw-control');Target page, context or browser has been closed. - Verify .NET state from a debugger or log:
Application.Current.Windows.Countreflects only audience window removed (operator still listed)- operator
WebView2HostWindow.OnCloseddid NOT fire - operator
CoreWebView2.ProcessFaileddid NOT fire - operator
Application.Current.MainWindowis unchanged - The operator's WebView2 control becomes unresponsive at the host process layer (any custom logger output stops ~4s before Playwright observes the close)
- Repeat with
page.close({ runBeforeUnload: true })on the audience BEFORE calling the host-side close — afterEach completes in <1s instead of hanging 120s (default close), but the operator-target disruption still fires. - Repeat with separate
test.describe()blocks per test (no shareddescribe.serial) — cascade is fixture/context-shared at worker level; describe restructuring does not prevent the disruption.
Environment
- WebView2 SDK: 1.0.3912.50
- WebView2 Runtime: Microsoft Edge WebView2 148.0.3967.54
- Playwright: 1.x (latest
@playwright/test) - Framework: WPF on .NET 9 (net9.0-windows)
- OS: Windows 11 Pro build 26200
- Hardware: Intel i9-12900H
Anything else?
Tracked internally as DEFR-234. Full primary-source research and Playwright server/client source citations are at https://github.com/dsconyers/VB6-to-VBNET-Migration/blob/master/docs/research/defr-234-cdp-target-lifecycle/s135-research.md.
If a Microsoft-side maintainer can confirm whether CoreWebView2Controller.Close is expected to emit Target.detachedFromTarget for sibling CDP targets in any documented scenario, that would narrow the fix-design space. Currently exploring "separate CoreWebView2Environment per audience window" as an app-side mitigation, mirroring the workaround comment on #4587.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.