MicrosoftEdge / MicrosoftEdge/WebView2Feedback

[Problem/Bug]: Multi-CoreWebView2 host: closing one window collateral-detaches another window's CDP target (Playwright integration repro)

Open
#5,588 0 comments 0 reactions 1 assignee View on GitHub

@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 CoreWebView2 instances ✓
  • 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 WebView2 instance 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
  1. Create a WPF (or WinForms / WinUI) host application with at least 2 CoreWebView2 instances in separate top-level Windows.
  2. Configure all instances to share one CoreWebView2Environment (single user data folder) — i.e., call EnsureCoreWebView2Async(Nothing) (or pass the same explicit environment) in every host window.
  3. Launch the app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9224 set in the process environment. Wait for the operator's first CoreWebView2 to be ready; verify port 9224 is listening.
  4. 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];
    
  5. 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'));
    
  6. Drive the operator UI to spawn an audience CoreWebView2 instance in a separate WPF Window (still using the same CoreWebView2Environment). Confirm it appears in context.pages().
  7. Wait for the audience to finish its initial paint.
  8. Close the audience from the .NET side by calling CoreWebView2HostWindow.Close() (or equivalent — anything that disposes the audience's CoreWebView2Controller).
  9. Within the next 100ms, observe the diagnostic log:
    • Target.targetInfoChanged for the audience target with attached=false
    • Target.targetDestroyed for the audience target
    • page.on('close') fires on the OPERATOR Page within ~30ms of the audience targetDestroyed
  10. Attempt any operation on the operator Page:
    await operatorPage.goto('https://app.local/#/draw-control');
    
    Rejects with Target page, context or browser has been closed.
  11. Verify .NET state from a debugger or log:
    • Application.Current.Windows.Count reflects only audience window removed (operator still listed)
    • operator WebView2HostWindow.OnClosed did NOT fire
    • operator CoreWebView2.ProcessFailed did NOT fire
    • operator Application.Current.MainWindow is unchanged
    • The operator's WebView2 control becomes unresponsive at the host process layer (any custom logger output stops ~4s before Playwright observes the close)
  12. 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.
  13. Repeat with separate test.describe() blocks per test (no shared describe.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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.