Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
325fa86
Add web request interception into BlazorWebView
mattleibow Jun 16, 2025
bb5aaf5
build green, public api later
mattleibow Jun 16, 2025
c04fdd0
add tests
mattleibow Jun 17, 2025
2602628
delete these
mattleibow Jun 17, 2025
a30d6fd
Yay, all but one
mattleibow Jun 17, 2025
213813f
Cheese be movin'
mattleibow Jun 17, 2025
81ba9fb
Update WinUIWebViewManager.cs
mattleibow Jun 17, 2025
bc36d12
Old typo
mattleibow Jun 17, 2025
f1451c6
new APIs
mattleibow Jun 18, 2025
ce1e661
sadness
mattleibow Jun 18, 2025
9ca34a8
Fix tests on android
mattleibow Jun 18, 2025
5a4b9aa
Merge branch 'net10.0' into dev/blazorwebview-interception
mattleibow Jun 18, 2025
2a19b46
tests pass on windows
mattleibow Jun 18, 2025
b440d48
Update BlazorWebViewTests.RequestInterception.cs
mattleibow Jun 19, 2025
ce26c31
Merge branch 'dev/blazorwebview-interception' of https://github.com/d…
mattleibow Jun 19, 2025
ed98e52
Logging!
mattleibow Jun 19, 2025
5e850ea
stuff
mattleibow Jun 19, 2025
af80457
:(
mattleibow Jun 19, 2025
a823e95
Update WebViewHelpers.Shared.cs
mattleibow Jun 19, 2025
e14c75a
Update BlazorWebViewTests.RequestInterception.cs
mattleibow Jun 19, 2025
c86cf1e
Update WebViewHelpers.Shared.cs
mattleibow Jun 19, 2025
a8ae236
Revert "Update WebViewHelpers.Shared.cs"
mattleibow Jun 19, 2025
d0d1ea5
Revert "Update BlazorWebViewTests.RequestInterception.cs"
mattleibow Jun 19, 2025
708040d
Update WebViewHelpers.Shared.cs
mattleibow Jun 19, 2025
2138bbb
Update AndroidManifest.xml
mattleibow Jun 19, 2025
bc6d896
Make things internal and move code
mattleibow Jun 19, 2025
7f7e110
Update AssemblyInfo.cs
mattleibow Jun 19, 2025
b75e102
Update PlatformWebViewWebResourceRequestedEventArgs.cs
mattleibow Jun 19, 2025
c0d0083
Update BlazorWebViewHandler.iOS.cs
mattleibow Jun 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions src/BlazorWebView/src/Maui/Android/WebKitWebViewClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Android.Runtime;
using Android.Webkit;
using Java.Net;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Platform;
using AWebView = Android.Webkit.WebView;

namespace Microsoft.AspNetCore.Components.WebView.Maui
Expand Down Expand Up @@ -83,10 +85,40 @@ private bool ShouldOverrideUrlLoadingCore(IWebResourceRequest? request)
}

var requestUri = request?.Url?.ToString();

var logger = _webViewHandler?.Logger;

logger?.LogDebug("Intercepting request for {Url}.", requestUri);

if (view is not null && request is not null && !string.IsNullOrEmpty(requestUri))
{
// 1. Check if the app wants to modify or override the request
var response = WebRequestInterceptingWebView.TryInterceptResponseStream(_webViewHandler, view, request, requestUri, logger);
if (response is not null)
{
return response;
}

// 2. Check if the request is for a Blazor resource
response = GetResponse(requestUri, _webViewHandler?.Logger);
if (response is not null)
{
return response;
}
}

// 3. Otherwise, we let the request go through as is
logger?.LogDebug("Request for {Url} was not handled.", requestUri);

return base.ShouldInterceptRequest(view, request);
}

private WebResourceResponse? GetResponse(string requestUri, ILogger? logger)
{
var allowFallbackOnHostPage = AppOriginUri.IsBaseOfPage(requestUri);
requestUri = QueryStringHelper.RemovePossibleQueryString(requestUri);

_webViewHandler?.Logger.HandlingWebRequest(requestUri);
logger?.HandlingWebRequest(requestUri);

if (requestUri != null &&
_webViewHandler != null &&
Expand All @@ -95,16 +127,16 @@ private bool ShouldOverrideUrlLoadingCore(IWebResourceRequest? request)
{
var contentType = headers["Content-Type"];

_webViewHandler?.Logger.ResponseContentBeingSent(requestUri, statusCode);
logger?.ResponseContentBeingSent(requestUri, statusCode);

return new WebResourceResponse(contentType, "UTF-8", statusCode, statusMessage, headers, content);
}
else
{
_webViewHandler?.Logger.ReponseContentNotFound(requestUri ?? string.Empty);
logger?.ResponseContentNotFound(requestUri ?? string.Empty);
}

return base.ShouldInterceptRequest(view, request);
return null;
}

public override void OnPageFinished(AWebView? view, string? url)
Expand Down
22 changes: 22 additions & 0 deletions src/BlazorWebView/src/Maui/BlazorWebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.FileProviders;
using Microsoft.Maui.Controls;
using Microsoft.Maui;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
Expand Down Expand Up @@ -65,6 +66,18 @@ public string StartPath
/// </summary>
public event EventHandler<BlazorWebViewInitializedEventArgs>? BlazorWebViewInitialized;

/// <summary>
/// Raised when a web resource is requested. This event allows the application to intercept the request and provide a
/// custom response.
/// The event handler can set the <see cref="WebViewWebResourceRequestedEventArgs.Handled"/> property to true
/// to indicate that the request has been handled and no further processing is needed. If the event handler does set this
/// property to true, it must also call the
/// <see cref="WebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.IO.Stream?)"/>
/// or <see cref="WebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.Threading.Tasks.Task{System.IO.Stream?})"/>
/// method to provide a response to the request.
/// </summary>
public event EventHandler<WebViewWebResourceRequestedEventArgs>? WebResourceRequested;

/// <inheritdoc />
#if ANDROID
[System.Runtime.Versioning.SupportedOSPlatform("android23.0")]
Expand Down Expand Up @@ -108,5 +121,14 @@ void IBlazorWebView.BlazorWebViewInitializing(BlazorWebViewInitializingEventArgs
/// <inheritdoc />
void IBlazorWebView.BlazorWebViewInitialized(BlazorWebViewInitializedEventArgs args) =>
BlazorWebViewInitialized?.Invoke(this, args);

/// <inheritdoc />
bool IWebRequestInterceptingWebView.WebResourceRequested(WebResourceRequestedEventArgs args)
{
var platformArgs = new PlatformWebViewWebResourceRequestedEventArgs(args);
var e = new WebViewWebResourceRequestedEventArgs(platformArgs);
WebResourceRequested?.Invoke(this, e);
return e.Handled;
}
}
}
2 changes: 1 addition & 1 deletion src/BlazorWebView/src/Maui/IBlazorWebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
/// <summary>
/// Defines a contract for a view that renders Blazor content.
/// </summary>
public interface IBlazorWebView : IView
public interface IBlazorWebView : IView, IWebRequestInterceptingWebView
{
/// <summary>
/// Gets the path to the HTML file to render.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@

<ItemGroup>
<Compile Include="..\SharedSource\**\*.cs" Link="Windows\SharedSource\%(Filename)%(Extension)" />
<Compile Include="..\..\..\Core\src\TaskExtensions.cs" Link="Utilities\TaskExtensions.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.WebResourceRequested -> System.EventHandler<Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs!>?
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.WebResourceRequested -> System.EventHandler<Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs!>?
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.WebResourceRequested -> System.EventHandler<Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs!>?
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.WebResourceRequested -> System.EventHandler<Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs!>?
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.WebResourceRequested -> System.EventHandler<Microsoft.Maui.Controls.WebViewWebResourceRequestedEventArgs!>?
102 changes: 62 additions & 40 deletions src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Web.WebView2.Core;
using Windows.ApplicationModel;
using Windows.Storage.Streams;
using Microsoft.Maui.Platform;
using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2;

namespace Microsoft.AspNetCore.Components.WebView.Maui
Expand All @@ -20,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
/// </summary>
internal class WinUIWebViewManager : WebView2WebViewManager
{
private readonly BlazorWebViewHandler _handler;
private readonly WebView2Control _webview;
private readonly string _hostPageRelativePath;
private readonly string _contentRootRelativeToAppRoot;
Expand Down Expand Up @@ -62,6 +64,7 @@ public WinUIWebViewManager(
ILogger logger)
: base(webview, services, dispatcher, fileProvider, jsComponents, contentRootRelativeToAppRoot, hostPagePathWithinFileProvider, webViewHandler, logger)
{
_handler = webViewHandler;
_logger = logger;
_webview = webview;
_hostPageRelativePath = hostPagePathWithinFileProvider;
Expand All @@ -71,52 +74,71 @@ public WinUIWebViewManager(
/// <inheritdoc />
protected override async Task HandleWebResourceRequest(CoreWebView2WebResourceRequestedEventArgs eventArgs)
{
// Unlike server-side code, we get told exactly why the browser is making the request,
// so we can be smarter about fallback. We can ensure that 'fetch' requests never result
// in fallback, for example.
var allowFallbackOnHostPage =
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Document ||
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Other; // e.g., dev tools requesting page source
var url = eventArgs.Request.Uri;

// Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method.
using var deferral = eventArgs.GetDeferral();
_logger.LogDebug("Intercepting request for {Url}.", url);

var requestUri = QueryStringHelper.RemovePossibleQueryString(eventArgs.Request.Uri);

_logger.HandlingWebRequest(requestUri);

var uri = new Uri(requestUri);
var relativePath = AppOriginUri.IsBaseOf(uri) ? AppOriginUri.MakeRelativeUri(uri).ToString() : null;

// Check if the uri is _framework/blazor.modules.json is a special case as the built-in file provider
// brings in a default implementation.
if (relativePath != null &&
string.Equals(relativePath, "_framework/blazor.modules.json", StringComparison.Ordinal) &&
await TryServeFromFolderAsync(eventArgs, allowFallbackOnHostPage: false, requestUri, relativePath))
{
_logger.ResponseContentBeingSent(requestUri, 200);
}
else if (TryGetResponseContent(requestUri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)
&& statusCode != 404)
// 1. First check if the app wants to modify or override the request.
if (WebRequestInterceptingWebView.TryInterceptResponseStream(_handler, _webview.CoreWebView2, eventArgs, url, _logger))
{
// First, call into WebViewManager to see if it has a framework file for this request. It will
// fall back to an IFileProvider, but on WinUI it's always a NullFileProvider, so that will never
// return a file.
var headerString = GetHeaderString(headers);
_logger.ResponseContentBeingSent(requestUri, statusCode);
eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse(content.AsRandomAccessStream(), statusCode, statusMessage, headerString);
return;
}
else if (relativePath != null)

// 2. If this is an app request, then assume the request is for a Blazor resource.
var requestUri = QueryStringHelper.RemovePossibleQueryString(url);
if (new Uri(requestUri) is Uri uri)
{
await TryServeFromFolderAsync(
eventArgs,
allowFallbackOnHostPage,
requestUri,
relativePath);
// Unlike server-side code, we get told exactly why the browser is making the request,
// so we can be smarter about fallback. We can ensure that 'fetch' requests never result
// in fallback, for example.
var allowFallbackOnHostPage =
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Document ||
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Other; // e.g., dev tools requesting page source

// Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method.
using var deferral = eventArgs.GetDeferral();

_logger.HandlingWebRequest(requestUri);

var relativePath = AppOriginUri.IsBaseOf(uri) ? AppOriginUri.MakeRelativeUri(uri).ToString() : null;

// Check if the uri is _framework/blazor.modules.json is a special case as the built-in file provider
// brings in a default implementation.
if (relativePath != null &&
string.Equals(relativePath, "_framework/blazor.modules.json", StringComparison.Ordinal) &&
await TryServeFromFolderAsync(eventArgs, allowFallbackOnHostPage: false, requestUri, relativePath))
{
_logger.ResponseContentBeingSent(requestUri, 200);
}
else if (TryGetResponseContent(requestUri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)
&& statusCode != 404)
{
// First, call into WebViewManager to see if it has a framework file for this request. It will
// fall back to an IFileProvider, but on WinUI it's always a NullFileProvider, so that will never
// return a file.
var headerString = GetHeaderString(headers);
_logger.ResponseContentBeingSent(requestUri, statusCode);
eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse(content.AsRandomAccessStream(), statusCode, statusMessage, headerString);
}
else if (relativePath != null)
{
await TryServeFromFolderAsync(
eventArgs,
allowFallbackOnHostPage,
requestUri,
relativePath);
}

// Notify WebView2 that the deferred (async) operation is complete and we set a response.
deferral.Complete();
return;
}

// Notify WebView2 that the deferred (async) operation is complete and we set a response.
deferral.Complete();
// 3. If the request is not handled by the app nor is it a local source, then we let the WebView2
// handle the request as it would normally do. This means that it will try to load the resource
// from the internet or from the local cache.

_logger.LogDebug("Request for {Url} was not handled.", url);
}

private async Task<bool> TryServeFromFolderAsync(
Expand Down Expand Up @@ -179,7 +201,7 @@ private async Task<bool> TryServeFromFolderAsync(
}
else
{
_logger.ReponseContentNotFound(requestUri);
_logger.ResponseContentNotFound(requestUri);
}

return false;
Expand Down
30 changes: 27 additions & 3 deletions src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Maui;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using UIKit;
using WebKit;
using RectangleF = CoreGraphics.CGRect;
Expand Down Expand Up @@ -259,12 +260,29 @@ public SchemeHandler(BlazorWebViewHandler webViewHandler)
[SupportedOSPlatform("ios11.0")]
public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)
{
var responseBytes = GetResponseBytes(urlSchemeTask.Request.Url?.AbsoluteString ?? "", out var contentType, statusCode: out var statusCode);
var url = urlSchemeTask.Request.Url.AbsoluteString;
if (string.IsNullOrEmpty(url))
{
return;
}

var logger = _webViewHandler.Logger;

logger.LogDebug("Intercepting request for {Url}.", url);

// 1. First check if the app wants to modify or override the request.
if (WebRequestInterceptingWebView.TryInterceptResponseStream(_webViewHandler, webView, urlSchemeTask, url, logger))
{
return;
}

// 2. If this is an app request, then assume the request is for a Blazor resource.
var responseBytes = GetResponseBytes(url, out var contentType, statusCode: out var statusCode);
if (statusCode == 200)
{
using (var dic = new NSMutableDictionary<NSString, NSString>())
{
dic.Add((NSString)"Content-Length", (NSString)(responseBytes.Length.ToString(CultureInfo.InvariantCulture)));
dic.Add((NSString)"Content-Length", (NSString)responseBytes.Length.ToString(CultureInfo.InvariantCulture));
dic.Add((NSString)"Content-Type", (NSString)contentType);
// Disable local caching. This will prevent user scripts from executing correctly.
dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
Expand All @@ -278,6 +296,12 @@ public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask
urlSchemeTask.DidReceiveData(NSData.FromArray(responseBytes));
urlSchemeTask.DidFinish();
}

// 3. If the request is not handled by the app nor is it a local source, then we let the WKWebView
// handle the request as it would normally do. This means that it will try to load the resource
// from the internet or from the local cache.

logger.LogDebug("Request for {Url} was not handled.", url);
}

private byte[] GetResponseBytes(string? url, out string contentType, out int statusCode)
Expand All @@ -303,7 +327,7 @@ private byte[] GetResponseBytes(string? url, out string contentType, out int sta
}
else
{
_webViewHandler?.Logger.ReponseContentNotFound(url);
_webViewHandler?.Logger.ResponseContentNotFound(url);

statusCode = 404;
contentType = string.Empty;
Expand Down
2 changes: 1 addition & 1 deletion src/BlazorWebView/src/SharedSource/Log.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal static partial class Log
public static partial void ResponseContentBeingSent(this ILogger logger, string requestUri, int statusCode);

[LoggerMessage(EventId = 6, Level = LogLevel.Debug, Message = "Response content was not found for web request to URI '{requestUri}'.")]
public static partial void ReponseContentNotFound(this ILogger logger, string requestUri);
public static partial void ResponseContentNotFound(this ILogger logger, string requestUri);

[LoggerMessage(EventId = 7, Level = LogLevel.Debug, Message = "Navigation event for URI '{uri}' with URL loading strategy '{urlLoadingStrategy}'.")]
public static partial void NavigationEvent(this ILogger logger, Uri uri, UrlLoadingStrategy urlLoadingStrategy);
Expand Down
4 changes: 2 additions & 2 deletions src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ private async Task<bool> TryInitializeWebView2()
});
#endif

_webview.CoreWebView2.AddWebResourceRequestedFilter($"{AppOrigin}*", CoreWebView2WebResourceContext.All);
_webview.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);

_webview.CoreWebView2.WebResourceRequested += async (s, eventArgs) =>
{
Expand Down Expand Up @@ -325,7 +325,7 @@ protected virtual Task HandleWebResourceRequest(CoreWebView2WebResourceRequested
}
else
{
_logger.ReponseContentNotFound(requestUri);
_logger.ResponseContentNotFound(requestUri);
}
#elif WEBVIEW2_MAUI
// No-op here because all the work is done in the derived WinUIWebViewManager
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
Loading
Loading