Fix Linux WebView login hang and User-Agent failure (#2106) (#2108)

- Set NativeWebDialog.UserAgent in AdapterCreated instead of passing a full
  User-Agent header to ApplicationNameForUserAgent (which caused WebKit2GTK
  webkit_settings_set_user_agent assertion failures on Linux).
- Set gtkArgs.ForceX11GdkBackend = true when requesting the GTK WebView
  environment.
- Recognize GTK initialization failures (InvalidOperationException 'Unable to
  initialize GTK', libgtk-3/libgdk-3 DllNotFoundException, and exceptions
  originating from GtkNativeWebViewDialog/AvaloniaGtk) in
  WebView2LoginErrorMessage so async void NativeWebDialog.Show() failures
  fault BrowserLoginAsync instead of leaving tcs.Task waiting forever,
  allowing StartAsyncInternal to fall back to LoginExternalDialog.
This commit is contained in:
William Tanksley Jr authored and GitHub committed 2026-10-26 09:46:32 -06:00
1 parent e1c52d910b
commit f61bbe5b68
3 files changed
+96 -7

No files matched your search

@@ -93,6 +93,15 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager
};
dialog.AdapterCreated += (s, e) =>
{
try
{
dialog.UserAgent = Configuration.Instance.GetDeviceRegistrationProfile().UserAgent;
}
catch (Exception ex)
{
Serilog.Log.Logger.Warning(ex, "Failed to set WebView User-Agent");
}
if (dialog.TryGetCookieManager() is NativeWebViewCookieManager cookieManager)
{
foreach (System.Net.Cookie c in shoiceIn.SignInCookies ?? [])
@@ -126,10 +135,13 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager
return null;
}
// NativeWebDialog can fault on a posted dispatcher job (e.g. missing libwebkit2gtk on Linux), which bypasses a try/catch around Show().
// NativeWebDialog.Show() is async void and can fault on a posted dispatcher job (e.g. missing libwebkit2gtk
// or GTK initialization failure on Linux), which bypasses a synchronous try/catch around Show() and would
// otherwise leave tcs.Task waiting forever.
void onUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
{
if (!WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(e.Exception))
if (!WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(e.Exception)
&& !WebView2LoginErrorMessage.TryFindInTree(e.Exception, out _))
return;
e.Handled = true;
try
@@ -166,7 +178,7 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager
else
dialog.Show();
}
catch (Exception ex) when (WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex))
catch (Exception ex)
{
tcs.TrySetException(ex);
}
@@ -190,11 +202,10 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager
break;
case AppleWKWebViewEnvironmentRequestedEventArgs appleArgs:
appleArgs.NonPersistentDataStore = true;
appleArgs.ApplicationNameForUserAgent = userAgent;
break;
case GtkWebViewEnvironmentRequestedEventArgs gtkArgs:
gtkArgs.EphemeralDataManager = true;
gtkArgs.ApplicationNameForUserAgent = userAgent;
gtkArgs.ForceX11GdkBackend = true;
break;
}
}
@@ -61,10 +61,15 @@ public static class WebView2LoginErrorMessage
if (e is DllNotFoundException dll && IsEmbeddedWebViewNativeDllFailure(dll))
return true;
// AvaloniaGtk.RunTask throws InvalidOperationException("Unable to initialize GTK") when GTK cannot start
// (for example on Wayland without X11 backend or when no GTK display is available).
if (e is InvalidOperationException && e.Message.Contains("Unable to initialize GTK", StringComparison.OrdinalIgnoreCase))
return true;
if (!StackMentionsEmbeddedSignInBrowser(e))
continue;
if (e is UnauthorizedAccessException)
if (e is UnauthorizedAccessException or InvalidOperationException or EntryPointNotFoundException or TypeInitializationException)
return true;
if (e is COMException com
@@ -81,7 +86,9 @@ public static class WebView2LoginErrorMessage
private static bool IsEmbeddedWebViewNativeDllFailure(DllNotFoundException e)
{
if (e.Message.Contains("libwebkit2gtk", StringComparison.OrdinalIgnoreCase)
|| e.Message.Contains("webkit2gtk", StringComparison.OrdinalIgnoreCase))
|| e.Message.Contains("webkit2gtk", StringComparison.OrdinalIgnoreCase)
|| e.Message.Contains("libgtk-3", StringComparison.OrdinalIgnoreCase)
|| e.Message.Contains("libgdk-3", StringComparison.OrdinalIgnoreCase))
return true;
return StackMentionsEmbeddedSignInBrowser(e);
@@ -118,7 +125,9 @@ public static class WebView2LoginErrorMessage
&& (stack.Contains("WebView2", StringComparison.Ordinal)
|| stack.Contains("CoreWebView2", StringComparison.Ordinal)
|| stack.Contains("NativeWebDialog", StringComparison.Ordinal)
|| stack.Contains("GtkNativeWebViewDialog", StringComparison.Ordinal)
|| stack.Contains("GtkWebView", StringComparison.Ordinal)
|| stack.Contains("AvaloniaGtk", StringComparison.Ordinal)
|| stack.Contains("WebViewControlAvalonia", StringComparison.Ordinal));
}
}
@@ -0,0 +1,69 @@
using LibationUiBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Runtime.CompilerServices;
namespace LibationUiBase.Tests;
[TestClass]
public class WebView2LoginErrorMessageTests
{
[TestMethod]
public void missing_webkit2gtk_dll_is_recognized_without_stack_trace()
{
var ex = new DllNotFoundException("Unable to load shared library 'libwebkit2gtk-4.1.so.0'");
Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex));
}
[TestMethod]
public void missing_gtk3_dll_is_recognized_without_stack_trace()
{
var ex = new DllNotFoundException("Unable to load shared library 'libgtk-3.so.0'");
Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex));
}
[TestMethod]
public void gtk_initialization_failure_is_recognized_without_stack_trace()
{
var ex = new InvalidOperationException("Unable to initialize GTK");
Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex));
}
[TestMethod]
public void invalid_operation_from_avalonia_gtk_webview_stack_is_recognized()
{
Exception? caught = null;
try
{
FakeAvaloniaGtkStack.ThrowFromGtkNativeWebViewDialog();
}
catch (Exception ex)
{
caught = ex;
}
Assert.IsNotNull(caught);
Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(caught));
var agg = new AggregateException(new Exception("outer"), caught);
Assert.IsTrue(WebView2LoginErrorMessage.TryFindInTree(agg, out var match));
Assert.IsNotNull(match);
}
[TestMethod]
public void unrelated_invalid_operation_is_not_treated_as_webview_failure()
{
var ex = new InvalidOperationException("Something else went wrong");
Assert.IsFalse(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex));
}
private static class FakeAvaloniaGtkStack
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowFromGtkNativeWebViewDialog()
=> throw new InvalidOperationException("Avalonia.X11 is not referenced");
}
}