Environment
|
|
| Plugin |
flutter_inappwebview 6.1.5 / flutter_inappwebview_windows 0.6.0 (latest stable of both at time of filing; the 6.2.0-beta.3 / 0.7.0-beta.3 prereleases have not been checked for this behavior) |
| Flutter |
3.48.0-0.3.pre (channel master), Dart 3.13.0 |
| OS |
Windows 11 x64, build 26200 |
| WebView2 Runtime |
Evergreen, 151.0.4129.107 |
| App type |
Flutter Windows desktop app embedding InAppWebView (default composition-controller path) |
Current Behavior
On Windows, the plugin closes its CoreWebView2 controllers only in InAppWebView::~InAppWebView(). At application quit, that destructor runs during engine/plugin-registrar teardown, i.e. after the Flutter view HWND and the plugin's own host HWNDs have already been destroyed by the DestroyWindow cascade. The controllers are therefore never closed in an orderly way; every controller call in the destructor fails with 0x8007139F (HRESULT_FROM_WIN32(ERROR_INVALID_STATE)), and the actual browser/composition teardown is left to happen implicitly during process exit, off the message loop. We observe intermittent multi-second quit hangs, and have correlated this teardown pattern with a fail-fast crash family in dcomp.dll (exception code 0xE0464645) during process teardown in the field.
Mechanism (plugin source, v0.6.0)
-
Each webview's host window is a child of the Flutter view HWND.
windows/in_app_webview/in_app_webview_manager.cpp:111-115 (createInAppWebView):
auto hwnd = CreateWindowEx(0, windowClass_.lpszClassName, L"", 0, 0,
0, bounds.right - bounds.left, bounds.bottom - bounds.top,
plugin->registrar->GetView()->GetNativeWindow(), // <-- parent = Flutter view HWND
nullptr,
windowClass_.hInstance, nullptr);
This HWND is the parent window of the ICoreWebView2Controller / composition controller.
-
The plugin never observes the destruction of its host windows.
in_app_webview_manager.cpp:49-52: the host window class uses DefWindowProc directly, so WM_DESTROY on a host HWND is not handled anywhere:
windowClass_.lpszClassName = CustomPlatformView::CLASS_NAME;
windowClass_.lpfnWndProc = &DefWindowProc;
-
The only close path is the destructor.
windows/in_app_webview/in_app_webview.cpp:1866-1885:
InAppWebView::~InAppWebView()
{
debugLog("dealloc InAppWebView");
userContentController = nullptr; // -> ~UserContentController -> DevTools call on dead webview
if (webView) {
failedLog(webView->Stop()); // 1871: fails, 0x8007139F
}
HWND parentWindow = nullptr;
if (webViewCompositionController && webViewController &&
succeededOrLog(webViewController->get_ParentWindow(&parentWindow))) { // 1874: fails, 0x8007139F
DestroyWindow(parentWindow); // 1877: skipped (window already gone anyway)
}
if (webViewController) {
failedLog(webViewController->Close()); // 1880: no-op, controller already implicitly closed
}
...
}
These destructors run from InAppWebViewManager::~InAppWebViewManager() (in_app_webview_manager.cpp:238-246, webViews.clear() / keepAliveWebViews.clear()), i.e. only when the plugin itself is destroyed during engine shutdown. Keep-alive webviews share the same fate.
-
At quit, the ordering is inverted. The top-level window closes → the Flutter view HWND (and, via the child cascade, every plugin host HWND) is destroyed → WebView2 implicitly closes controllers whose parent window died → then plugin destruction runs the code above. The evidence is the destructor's own logging: get_ParentWindow on a controller returns ERROR_INVALID_STATE only after the controller has been closed, and DestroyWindow(parentWindow) (1877), the plugin's one piece of window cleanup, is unreachable because the guard at 1874 fails.
-
Consequence: Close() never runs against a live controller, so the WebView2 browser/composition disconnect happens implicitly during ExitProcess instead of on the message loop. In our quit battery this shows up as an intermittent (~1 in 10) hang where window teardown stalls and the process lingers ~25 s after the window is gone; in the field we see a matching fail-fast crash signature inside dcomp.dll (exception code 0xE0464645) during process teardown of the same app configuration. Note that WebView2's own guidance is to Close() the controller before its parent window is destroyed.
Runtime disposal is not affected: the dispose method-channel path runs the same destructor while the host HWND is still alive, so get_ParentWindow succeeds, the host is destroyed by the plugin, and Close() completes normally. The defect is specific to app-quit ordering.
Expected Behavior
Controllers are closed (ICoreWebView2Controller::Close) while their host HWNDs are still alive: no ERROR_INVALID_STATE errors at quit, and browser teardown completes on the message loop before process exit.
Steps To Reproduce
Any Flutter Windows app with a live InAppWebView at quit reproduces it:
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
void main() => runApp(const MaterialApp(
home: Scaffold(
body: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri('https://example.com')),
),
),
));
flutter run -d windows (debug, so the plugin's debugLog/failedLog output is visible).
- Wait for the page to load.
- Close the app window (X button / Alt+F4) while the webview is mounted.
- Observe stderr: three
Error -2147019873 lines per live webview instance, emitted from the plugin's teardown. Repeat the quit a number of times to also catch the intermittent teardown hang.
Reproduction rate for the error storm in our runs: 10/10 quits with webviews mounted; 0 errors in every control run without a mounted webview. One of the ten additionally hung in teardown (~25 s process lifetime after window destruction). Disposing/unmounting all InAppWebView widgets before allowing the window to close (so dispose runs over the method channel while the Flutter view HWND is alive) produced 0 errors and 0 hangs across 15 runs; that is our current downstream workaround and confirms the ordering mechanism.
Stacktrace/Logs
Quit with two InAppWebView instances mounted, stderr (debug build, plugin's own logging; three lines per instance):
flutter_inappwebview_windows\windows\in_app_webview\user_content_controller.cpp(403): Error -2147019873: The group or resource is not in the correct state to perform the requested operation.
flutter_inappwebview_windows\windows\in_app_webview\in_app_webview.cpp(1871): Error -2147019873: The group or resource is not in the correct state to perform the requested operation.
flutter_inappwebview_windows\windows\in_app_webview\in_app_webview.cpp(1874): Error -2147019873: The group or resource is not in the correct state to perform the requested operation.
flutter_inappwebview_windows\windows\in_app_webview\user_content_controller.cpp(403): Error -2147019873: The group or resource is not in the correct state to perform the requested operation.
flutter_inappwebview_windows\windows\in_app_webview\in_app_webview.cpp(1871): Error -2147019873: The group or resource is not in the correct state to perform the requested operation.
flutter_inappwebview_windows\windows\in_app_webview\in_app_webview.cpp(1874): Error -2147019873: The group or resource is not in the correct state to perform the requested operation.
-2147019873 = 0x8007139F = HRESULT_FROM_WIN32(ERROR_INVALID_STATE). The user_content_controller.cpp(403) line is ~UserContentController (via ~InAppWebView, line 1869) issuing CallDevToolsProtocolMethod("Page.removeScriptToEvaluateOnNewDocument", ...) against the already-closed webview (removeAllUserOnlyScripts/removeAllPluginScripts → removeScriptFromWebView, user_content_controller.cpp:379-406).
Matching stdout at the same moment (plugin dealloc chain, running during engine shutdown, i.e. after the HWNDs are gone):
flutter_inappwebview_windows\windows\webview_environment\webview_environment_manager.cpp(121): dealloc WebViewEnvironmentManager
flutter_inappwebview_windows\windows\in_app_webview\in_app_webview_manager.cpp(240): dealloc InAppWebViewManager
flutter_inappwebview_windows\windows\custom_platform_view\custom_platform_view.cc(201): dealloc CustomPlatformView
flutter_inappwebview_windows\windows\in_app_webview\in_app_webview.cpp(1868): dealloc InAppWebView
flutter_inappwebview_windows\windows\in_app_webview\user_content_controller.cpp(426): dealloc UserContentController
...
Field crash association (same app configuration, release builds): Windows Error Reporting fail-fast in dcomp.dll, exception code 0xE0464645, faulting during process teardown. We do not have a symbolized public stack to attach, but the crash disappears together with the error storm once webviews are disposed before window destruction.
Suggested fix
Close the controllers before the HWND destroy cascade reaches them, instead of in the destructor path:
- Give the host window class a real window procedure (
in_app_webview_manager.cpp:49-52 currently registers &DefWindowProc). In WM_DESTROY of the host HWND (delivered synchronously inside the cascade, while the HWND is still valid and the message loop is still running), look up the owning InAppWebView and run the close sequence there (webView->Stop(), webViewController->Close()), then mark it closed so ~InAppWebView (in_app_webview.cpp:1866-1885) doesn't repeat the calls. This matches WebView2's documented guidance to call Close before the parent window is destroyed, and it covers every teardown order (app quit, view destruction, plugin destruction) with one hook.
- Alternatively/additionally, close all live controllers (including
keepAliveWebViews) on the engine's shutdown notification, before view teardown. Note that a fix inside the existing destructors alone cannot work: by the time InAppWebViewManager::~InAppWebViewManager() runs at quit, the host windows are already gone.
Happy to test a patch against our reproduction setup.
Environment
flutter_inappwebview6.1.5 /flutter_inappwebview_windows0.6.0 (latest stable of both at time of filing; the6.2.0-beta.3/0.7.0-beta.3prereleases have not been checked for this behavior)InAppWebView(default composition-controller path)Current Behavior
On Windows, the plugin closes its
CoreWebView2controllers only inInAppWebView::~InAppWebView(). At application quit, that destructor runs during engine/plugin-registrar teardown, i.e. after the Flutter view HWND and the plugin's own host HWNDs have already been destroyed by theDestroyWindowcascade. The controllers are therefore never closed in an orderly way; every controller call in the destructor fails with0x8007139F(HRESULT_FROM_WIN32(ERROR_INVALID_STATE)), and the actual browser/composition teardown is left to happen implicitly during process exit, off the message loop. We observe intermittent multi-second quit hangs, and have correlated this teardown pattern with a fail-fast crash family indcomp.dll(exception code0xE0464645) during process teardown in the field.Mechanism (plugin source, v0.6.0)
Each webview's host window is a child of the Flutter view HWND.
windows/in_app_webview/in_app_webview_manager.cpp:111-115(createInAppWebView):This HWND is the parent window of the
ICoreWebView2Controller/ composition controller.The plugin never observes the destruction of its host windows.
in_app_webview_manager.cpp:49-52: the host window class usesDefWindowProcdirectly, soWM_DESTROYon a host HWND is not handled anywhere:windowClass_.lpszClassName = CustomPlatformView::CLASS_NAME; windowClass_.lpfnWndProc = &DefWindowProc;The only close path is the destructor.
windows/in_app_webview/in_app_webview.cpp:1866-1885:These destructors run from
InAppWebViewManager::~InAppWebViewManager()(in_app_webview_manager.cpp:238-246,webViews.clear()/keepAliveWebViews.clear()), i.e. only when the plugin itself is destroyed during engine shutdown. Keep-alive webviews share the same fate.At quit, the ordering is inverted. The top-level window closes → the Flutter view HWND (and, via the child cascade, every plugin host HWND) is destroyed → WebView2 implicitly closes controllers whose parent window died → then plugin destruction runs the code above. The evidence is the destructor's own logging:
get_ParentWindowon a controller returnsERROR_INVALID_STATEonly after the controller has been closed, andDestroyWindow(parentWindow)(1877), the plugin's one piece of window cleanup, is unreachable because the guard at 1874 fails.Consequence:
Close()never runs against a live controller, so the WebView2 browser/composition disconnect happens implicitly duringExitProcessinstead of on the message loop. In our quit battery this shows up as an intermittent (~1 in 10) hang where window teardown stalls and the process lingers ~25 s after the window is gone; in the field we see a matching fail-fast crash signature insidedcomp.dll(exception code0xE0464645) during process teardown of the same app configuration. Note that WebView2's own guidance is toClose()the controller before its parent window is destroyed.Runtime disposal is not affected: the
disposemethod-channel path runs the same destructor while the host HWND is still alive, soget_ParentWindowsucceeds, the host is destroyed by the plugin, andClose()completes normally. The defect is specific to app-quit ordering.Expected Behavior
Controllers are closed (
ICoreWebView2Controller::Close) while their host HWNDs are still alive: noERROR_INVALID_STATEerrors at quit, and browser teardown completes on the message loop before process exit.Steps To Reproduce
Any Flutter Windows app with a live
InAppWebViewat quit reproduces it:flutter run -d windows(debug, so the plugin'sdebugLog/failedLogoutput is visible).Error -2147019873lines per live webview instance, emitted from the plugin's teardown. Repeat the quit a number of times to also catch the intermittent teardown hang.Reproduction rate for the error storm in our runs: 10/10 quits with webviews mounted; 0 errors in every control run without a mounted webview. One of the ten additionally hung in teardown (~25 s process lifetime after window destruction). Disposing/unmounting all
InAppWebViewwidgets before allowing the window to close (sodisposeruns over the method channel while the Flutter view HWND is alive) produced 0 errors and 0 hangs across 15 runs; that is our current downstream workaround and confirms the ordering mechanism.Stacktrace/Logs
Quit with two
InAppWebViewinstances mounted, stderr (debug build, plugin's own logging; three lines per instance):-2147019873=0x8007139F=HRESULT_FROM_WIN32(ERROR_INVALID_STATE). Theuser_content_controller.cpp(403)line is~UserContentController(via~InAppWebView, line 1869) issuingCallDevToolsProtocolMethod("Page.removeScriptToEvaluateOnNewDocument", ...)against the already-closed webview (removeAllUserOnlyScripts/removeAllPluginScripts→removeScriptFromWebView,user_content_controller.cpp:379-406).Matching stdout at the same moment (plugin dealloc chain, running during engine shutdown, i.e. after the HWNDs are gone):
Field crash association (same app configuration, release builds): Windows Error Reporting fail-fast in
dcomp.dll, exception code0xE0464645, faulting during process teardown. We do not have a symbolized public stack to attach, but the crash disappears together with the error storm once webviews are disposed before window destruction.Suggested fix
Close the controllers before the HWND destroy cascade reaches them, instead of in the destructor path:
in_app_webview_manager.cpp:49-52currently registers&DefWindowProc). InWM_DESTROYof the host HWND (delivered synchronously inside the cascade, while the HWND is still valid and the message loop is still running), look up the owningInAppWebViewand run the close sequence there (webView->Stop(),webViewController->Close()), then mark it closed so~InAppWebView(in_app_webview.cpp:1866-1885) doesn't repeat the calls. This matches WebView2's documented guidance to callClosebefore the parent window is destroyed, and it covers every teardown order (app quit, view destruction, plugin destruction) with one hook.keepAliveWebViews) on the engine's shutdown notification, before view teardown. Note that a fix inside the existing destructors alone cannot work: by the timeInAppWebViewManager::~InAppWebViewManager()runs at quit, the host windows are already gone.Happy to test a patch against our reproduction setup.