Skip to content

[Windows] Controllers closed only in destructors, after host HWNDs are destroyed at app quit (ERROR_INVALID_STATE storm, teardown deferred to process exit) #2884

Description

@amantinband

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)

  1. 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.

  2. 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;
  3. 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.

  4. 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.

  5. 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')),
    ),
  ),
));
  1. flutter run -d windows (debug, so the plugin's debugLog/failedLog output is visible).
  2. Wait for the page to load.
  3. Close the app window (X button / Alt+F4) while the webview is mounted.
  4. 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/removeAllPluginScriptsremoveScriptFromWebView, 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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions