MAUI Hybrid issue with HTTP GET request from BlazorWebView

22 hours ago 1
ARTICLE AD BOX

I am working on a MAUI Hybrid solution, with the standard projects: Web (Server) / Shared / Mobile. My issue is when I run the app on the Android emulator.

Here is my MainActivity.cs:

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] public class MainActivity : MauiAppCompatActivity { }

Here is my MainApplication.cs:

[Application] public class MainApplication : MauiApplication { public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) { } protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); }

Here is my PermissionManagingBlazorWebChromeClient.cs (customized since I need to get permission from the user to use the geolocation API:

internal class PermissionManagingBlazorWebChromeClient : WebChromeClient, IActivityResultCallback { // This class implements a permission requesting workflow that matches workflow recommended // by the official Android developer documentation. // See: https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions // The current implementation supports location, camera, and microphone permissions. To add your own, // update the s_rationalesByPermission dictionary to include your rationale for requiring the permission. // If necessary, you may need to also update s_requiredPermissionsByWebkitResource to define how a specific // Webkit resource maps to an Android permission. // In a real app, you would probably use more convincing rationales tailored toward what your app does. private const string CameraAccessRationale = "This app requires access to your camera. Please grant access to your camera when requested."; private const string LocationAccessRationale = "This app requires access to your location. Please grant access to your precise location when requested."; private const string MicrophoneAccessRationale = "This app requires access to your microphone. Please grant access to your microphone when requested."; private static readonly Dictionary<string, string> s_rationalesByPermission = new() { [Manifest.Permission.Camera] = CameraAccessRationale, [Manifest.Permission.AccessFineLocation] = LocationAccessRationale, [Manifest.Permission.RecordAudio] = MicrophoneAccessRationale, // Add more rationales as you add more supported permissions. }; private static readonly Dictionary<string, string[]> s_requiredPermissionsByWebkitResource = new() { [PermissionRequest.ResourceVideoCapture] = new[] { Manifest.Permission.Camera }, [PermissionRequest.ResourceAudioCapture] = new[] { Manifest.Permission.ModifyAudioSettings, Manifest.Permission.RecordAudio }, // Add more Webkit resource -> Android permission mappings as needed. }; private readonly WebChromeClient _blazorWebChromeClient; private readonly ComponentActivity _activity; private readonly ActivityResultLauncher _requestPermissionLauncher; private Action<bool>? _pendingPermissionRequestCallback; public PermissionManagingBlazorWebChromeClient(WebChromeClient blazorWebChromeClient, ComponentActivity activity) { _blazorWebChromeClient = blazorWebChromeClient; _activity = activity; _requestPermissionLauncher = _activity.RegisterForActivityResult(new ActivityResultContracts.RequestPermission(), this); } public override void OnCloseWindow(WebView? window) { _blazorWebChromeClient.OnCloseWindow(window); _requestPermissionLauncher.Unregister(); } public override void OnGeolocationPermissionsShowPrompt(string? origin, GeolocationPermissions.ICallback? callback) { ArgumentNullException.ThrowIfNull(callback, nameof(callback)); RequestPermission(Manifest.Permission.AccessFineLocation, isGranted => callback.Invoke(origin, isGranted, false)); } public override void OnPermissionRequest(PermissionRequest? request) { ArgumentNullException.ThrowIfNull(request, nameof(request)); if (request.GetResources() is not { } requestedResources) { request.Deny(); return; } RequestAllResources(requestedResources, grantedResources => { if (grantedResources.Count == 0) { request.Deny(); } else { request.Grant(grantedResources.ToArray()); } }); } private void RequestAllResources(Memory<string> requestedResources, Action<List<string>> callback) { if (requestedResources.Length == 0) { // No resources to request - invoke the callback with an empty list. callback(new()); return; } var currentResource = requestedResources.Span[0]; var requiredPermissions = s_requiredPermissionsByWebkitResource.GetValueOrDefault(currentResource, Array.Empty<string>()); RequestAllPermissions(requiredPermissions, isGranted => { // Recurse with the remaining resources. If the first resource was granted, use a modified callback // that adds the first resource to the granted resources list. RequestAllResources(requestedResources[1..], !isGranted ? callback : grantedResources => { grantedResources.Add(currentResource); callback(grantedResources); }); }); } private void RequestAllPermissions(Memory<string> requiredPermissions, Action<bool> callback) { if (requiredPermissions.Length == 0) { // No permissions left to request - success! callback(true); return; } RequestPermission(requiredPermissions.Span[0], isGranted => { if (isGranted) { // Recurse with the remaining permissions. RequestAllPermissions(requiredPermissions[1..], callback); } else { // The first required permission was not granted. Fail now and don't attempt to grant // the remaining permissions. callback(false); } }); } private void RequestPermission(string permission, Action<bool> callback) { // This method implements the workflow described here: // https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions if (ContextCompat.CheckSelfPermission(_activity, permission) == Permission.Granted) { callback.Invoke(true); } else if (_activity.ShouldShowRequestPermissionRationale(permission) && s_rationalesByPermission.TryGetValue(permission, out var rationale)) { new AlertDialog.Builder(_activity) .SetTitle("Enable app permissions")! .SetMessage(rationale)! .SetNegativeButton("No thanks", (_, _) => callback(false))! .SetPositiveButton("Continue", (_, _) => LaunchPermissionRequestActivity(permission, callback))! .Show(); } else { LaunchPermissionRequestActivity(permission, callback); } } private void LaunchPermissionRequestActivity(string permission, Action<bool> callback) { if (_pendingPermissionRequestCallback is not null) { throw new InvalidOperationException("Cannot perform multiple permission requests simultaneously."); } _pendingPermissionRequestCallback = callback; _requestPermissionLauncher.Launch(permission); } void IActivityResultCallback.OnActivityResult(Java.Lang.Object isGranted) { var callback = _pendingPermissionRequestCallback; _pendingPermissionRequestCallback = null; callback?.Invoke((bool)isGranted); } #region Unremarkable overrides // See: https://github.com/dotnet/maui/issues/6565 public override JniPeerMembers JniPeerMembers => _blazorWebChromeClient.JniPeerMembers; public override Bitmap? DefaultVideoPoster => _blazorWebChromeClient.DefaultVideoPoster; public override View? VideoLoadingProgressView => _blazorWebChromeClient.VideoLoadingProgressView; public override void GetVisitedHistory(IValueCallback? callback) => _blazorWebChromeClient.GetVisitedHistory(callback); public override bool OnConsoleMessage(ConsoleMessage? consoleMessage) => _blazorWebChromeClient.OnConsoleMessage(consoleMessage); public override bool OnCreateWindow(WebView? view, bool isDialog, bool isUserGesture, Message? resultMsg) => _blazorWebChromeClient.OnCreateWindow(view, isDialog, isUserGesture, resultMsg); public override void OnGeolocationPermissionsHidePrompt() => _blazorWebChromeClient.OnGeolocationPermissionsHidePrompt(); public override void OnHideCustomView() => _blazorWebChromeClient.OnHideCustomView(); public override bool OnJsAlert(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsAlert(view, url, message, result); public override bool OnJsBeforeUnload(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsBeforeUnload(view, url, message, result); public override bool OnJsConfirm(WebView? view, string? url, string? message, JsResult? result) => _blazorWebChromeClient.OnJsConfirm(view, url, message, result); public override bool OnJsPrompt(WebView? view, string? url, string? message, string? defaultValue, JsPromptResult? result) => _blazorWebChromeClient.OnJsPrompt(view, url, message, defaultValue, result); public override void OnPermissionRequestCanceled(PermissionRequest? request) => _blazorWebChromeClient.OnPermissionRequestCanceled(request); public override void OnProgressChanged(WebView? view, int newProgress) => _blazorWebChromeClient.OnProgressChanged(view, newProgress); public override void OnReceivedIcon(WebView? view, Bitmap? icon) => _blazorWebChromeClient.OnReceivedIcon(view, icon); public override void OnReceivedTitle(WebView? view, string? title) => _blazorWebChromeClient.OnReceivedTitle(view, title); public override void OnReceivedTouchIconUrl(WebView? view, string? url, bool precomposed) => _blazorWebChromeClient.OnReceivedTouchIconUrl(view, url, precomposed); public override void OnRequestFocus(WebView? view) => _blazorWebChromeClient.OnRequestFocus(view); public override void OnShowCustomView(View? view, ICustomViewCallback? callback) => _blazorWebChromeClient.OnShowCustomView(view, callback); public override bool OnShowFileChooser(WebView? webView, IValueCallback? filePathCallback, FileChooserParams? fileChooserParams) => _blazorWebChromeClient.OnShowFileChooser(webView, filePathCallback, fileChooserParams); #endregion }

Here is my MainPage.xaml:

<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:DocumentsSolutions.Mobile" xmlns:route="clr-namespace:DocumentsSolutions.Mobile.Components" xmlns:shared="clr-namespace:DocumentsSolutions.Mobile.Shared;assembly=DocumentsSolutions.Mobile.Shared" x:Class="DocumentsSolutions.Mobile.MainPage" BackgroundColor="{DynamicResource PageBackgroundColor}" SafeAreaEdges="Container"> <!-- SafeAreaEdges hack: https://github.com/dotnet/maui/issues/33103 --> <BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html"> <BlazorWebView.RootComponents> <RootComponent Selector="#app" ComponentType="{x:Type route:Routes}" /> </BlazorWebView.RootComponents> </BlazorWebView> </ContentPage>

Here is my MainPage.xaml.cs:

public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing; blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized; } private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e); private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e); }

Here is my MainPage.xaml.Android.cs:

public partial class MainPage { // To manage Android permissions, update AndroidManifest.xml to include the permissions and // features required by your app. You may have to perform additional configuration to enable // use of those APIs from the WebView, as is done below. A custom WebChromeClient is needed // to define what happens when the WebView requests a set of permissions. See // PermissionManagingBlazorWebChromeClient.cs to explore the approach taken in this example. private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e) { } private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e) { if (e.WebView.Context?.GetActivity() is not ComponentActivity activity) { throw new InvalidOperationException($"The permission-managing WebChromeClient requires that the current activity be a '{nameof(ComponentActivity)}'."); } e.WebView.Settings.JavaScriptEnabled = true; e.WebView.Settings.AllowContentAccess = true; e.WebView.Settings.AllowFileAccess = true; e.WebView.Settings.AllowFileAccessFromFileURLs = true; e.WebView.Settings.AllowUniversalAccessFromFileURLs = true; e.WebView.Settings.DomStorageEnabled = true; e.WebView.Settings.DatabaseEnabled = true; e.WebView.Settings.MediaPlaybackRequiresUserGesture = false; e.WebView.Settings.SetGeolocationEnabled(true); e.WebView.Settings.SetGeolocationDatabasePath(e.WebView.Context?.FilesDir?.Path); e.WebView.SetWebChromeClient(new PermissionManagingBlazorWebChromeClient(e.WebView.WebChromeClient!, activity)); } }

I added the following in the csproj for the Mobile project:

<ItemGroup Condition="$(TargetFramework.StartsWith('net10.0-android')) != true"> <Compile Remove="**\**\*.Android.cs" /> <None Include="**\**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> <ItemGroup Condition="$(TargetFramework.StartsWith('net10.0-ios')) != true AND $(TargetFramework.StartsWith('net10.0-maccatalyst')) != true"> <Compile Remove="**\**\*.iOS.cs" /> <None Include="**\**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> <ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true "> <Compile Remove="**\*.Windows.cs" /> <None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup>

My API is very simple in the Web (Server) Program.cs file:

var api = app.MapGroup("/api"); api.MapGet("/documents", async ([FromServices] IMainDataService data, [FromServices] HttpContextAccessor context, [FromServices] UserManager<ApplicationUser> userManager, int range, int selectedItemId) => { var results = await data.GetAllDocumentsInRangeAsync(range, selectedItemId); //context.HttpContext.Response.Headers.AccessControlAllowOrigin = "*"; return Results.Ok(results); }).RequireAuthorization();

As you can see, I even tried to set the AccessControlAllowOrigin myself, but it does not change the behavior explained below.

In the Shared project, I have a shared razor page "Home.razor", from which I have a piece of jQuery code to retrieve some data from the Web project.

It is a simple $.get as follows:

var requestData = []; $.get(baseUrl + '/api/documents', { "range": distance, "selectedItemId": selectedItemId }) .done(function (data) { console.log("DOCUMENTS RECEIVED"); $.each(data, function (index, value) { var reqData = { name: value.name, displayIndex: value.displayIndex, itemId: value.id }; requestData.push(reqData); console.log("Added " + reqData.name + " to list of documents..."); }); });

When I run the Web (Server) project, the HTTP GET request is executed and it loads the data as expected.

But when I run it from the Mobile project (Android), I get a weird behavior. Initially it said there were issues with CORS. Which I solved with AddCors() in the Web project / Program.cs:

builder.Services.AddCors(options => { options.AddDefaultPolicy( policy => { policy.AllowAnyHeader(); policy.SetIsOriginAllowedToAllowWildcardSubdomains(); policy.AllowAnyMethod(); policy.AllowAnyOrigin(); policy.SetPreflightMaxAge(TimeSpan.FromSeconds(3600)); }); }); ... app.UseCors();

When I run the app in the Android emulator, it does not seem that the HTTP GET request is fully processed. Here is what I get from the trace:

Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler: Debug: Intercepting request for https://localhost:7087/api/documents?range=10&selectedItemId=1. Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler: Debug: Handling web request to URI 'https://localhost:7087/api/documents'. Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler: Debug: Response content was not found for web request to URI 'https://localhost:7087/api/documents'.

And here is what I get in the DevTools "Provisional headers are shown":

enter image description here

There is no HTTP Status Code, and no Response headers.

In the CORS policy, I tried to specify the Origins or to use the method AllowAnyOrigin() I get the same result.

I am using VS2026, .NET 10 and jQuery 4.0.0 min:

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>

Am I missing a customization on the BlazorWebView? Or something else?

Read Entire Article