-
Notifications
You must be signed in to change notification settings - Fork 2k
[BlazorWebView] Add opt-in static content caching #35706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 208 commits
aba36f5
fad2855
89301e8
a714f92
e63aea9
bcae41f
30e208d
1358b45
2373539
bed9b74
254559d
f0cd21e
4e99b98
ec18117
4165fa6
5a71293
542c280
5c088b2
89696f3
bc580f6
54b2666
9b4d624
551ffce
9ba0aa5
cd98879
4995ec3
df47b74
9402fd5
e2f4db2
3f65eac
6e3b46c
d2b629e
83abf0b
b5b4b74
291eb87
c3940dd
2009b65
b253b2d
93ddfc8
62e479a
b3c588b
9b7ea1e
a7fcad2
0e67249
bc561a8
f439916
45aaa43
770302d
283bd0b
ecfc02c
2f1d8e1
b2608d1
bc24081
6583afa
ab42dd9
514a218
bafa9b2
35c32e6
3a30809
9b68c5c
03199eb
8bc5408
8174c33
01dea78
c0ed4fd
57f6afe
162a619
1980c5b
13eb50a
8fa4d88
f9ea49b
4a8ed17
a736b1f
972c159
7c8ca43
88276cb
bfc342b
8f23f3a
1ac86f8
1a25b58
e03fb5b
73df0ca
eec854f
66b1871
dab137a
7eeee8a
4f57c1c
423ba42
87b3de6
ace083b
96fd203
97b3600
ead9fe0
7dbe75e
dca3e57
6e5c2ae
08bc902
fb067d5
834dcb3
e281e7f
726df58
3f05ecc
c21ceb1
b9c0368
d5331bc
61a2742
68c32e7
799defa
7653022
cbb6cff
4264190
58bc51a
a55df8a
dd6da09
2268aff
a534b9c
8424680
08164ae
fb465af
90596f8
aadddd9
c040542
5bd640b
3342b5d
de7f5e5
862cb61
7497244
ae3a345
1ca82e2
9de2fd0
3dedb03
d9b52f2
1b60279
3ea1511
2ee8663
14d1d26
8bd2353
0900b22
3ad732f
ec90ee8
bfeb56f
deb0a9a
d7ef096
c73e1b5
338f0bc
b358bcb
e70a453
449b15d
c50e87a
c263dbc
e892fcd
b5302b1
e9dd92e
85c1463
789d6db
9dffd57
7b23243
6a977da
f759f7d
0206d1f
03364e4
a39e2f3
22b1cd0
c51a74d
86530cf
80a7e2e
fb04326
704d74f
e4adf66
c2dc570
f303a11
a58bc62
f3867c6
3bc6559
9a01d1e
d5bd003
9de1fe7
c1cfbd1
d2d6c2d
fe25444
03ab44d
e7d8e56
a47bf13
c8d91b3
a6e2d41
0217296
eb49525
65c852f
e0df1f6
a89dd1c
e14454e
99bb5e0
e931f6a
bae31a9
3fd432c
84fd031
07f8955
54ec7fe
581ddeb
d30fcb9
b03a979
5802de8
88f30bf
6f1987c
c9e1a3c
d54398d
976cd8f
eda8191
34554f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
| using System.Runtime.Versioning; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.AspNetCore.Components.Web; | ||
| using Microsoft.Extensions.FileProviders; | ||
| using Microsoft.Maui; | ||
|
|
@@ -67,6 +67,24 @@ public string StartPath | |
| /// <inheritdoc cref="IBlazorWebView.RootComponents" /> | ||
| public RootComponentsCollection RootComponents { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a callback that determines the <c>Cache-Control</c> header value used for static content | ||
| /// (such as images, fonts, or stylesheets) served from the app's content root. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Public API Surface Design — The documented scope is narrower than the implemented scope, in a way that can break apps. The doc says the callback determines the header "for static content (such as images, fonts, or stylesheets) served from the app's content root", but the callback is invoked for every app-origin response on all platforms: the host page (
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Public API Surface Design — The doc scopes the callback to "static content (such as images, fonts, or stylesheets)" and the argument type is named Concrete failure: a developer follows the documented intent and returns Document here that the callback also receives the host page and |
||
| /// <para> | ||
| /// By default no callback is set and all served content uses <c>no-cache, max-age=0, must-revalidate, | ||
| /// no-store</c>, which disables WebView caching. Provide a callback to opt specific resources into caching, | ||
| /// which can avoid repeated file reads and reduce image reload flicker when navigating between pages. | ||
| /// Return <see langword="null"/> or an empty string from the callback to keep the default behavior for a | ||
| /// given request. Cache entries remain subject to platform limits, expiration, and eviction. | ||
| /// </para> | ||
|
Comment on lines
+71
to
+79
|
||
| /// <para> | ||
| /// The callback is invoked from the platform's request handling, which may run on a background thread, so it | ||
| /// must not access UI state directly. If the callback throws, the exception is logged and the request falls | ||
| /// back to the default header. | ||
| /// </para> | ||
| /// </summary> | ||
| public Func<BlazorWebViewStaticContentRequest, string?>? StaticContentCacheControlProvider { get; set; } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Public API Surface Design — API shape is inconsistent with the existing extensibility points on this control and is not forward-compatible. Every other request/navigation hook on |
||
|
|
||
| /// <summary> | ||
| /// Allows customizing how links are opened. | ||
| /// By default, opens internal links in the webview and external links in an external app. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| using System; | ||
|
|
||
| namespace Microsoft.AspNetCore.Components.WebView.Maui | ||
| { | ||
| /// <summary> | ||
| /// Describes a request for static content served by a <see cref="BlazorWebView"/>. An instance is passed to the | ||
| /// callback set on <see cref="BlazorWebView.StaticContentCacheControlProvider"/> so the application can decide | ||
| /// which <c>Cache-Control</c> header value to send for the resource. | ||
| /// </summary> | ||
| public sealed class BlazorWebViewStaticContentRequest | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="BlazorWebViewStaticContentRequest"/> class. | ||
| /// </summary> | ||
| /// <param name="uri">The absolute URI of the requested static content.</param> | ||
| /// <param name="contentType">The resolved MIME content type of the requested static content.</param> | ||
| public BlazorWebViewStaticContentRequest(Uri uri, string contentType) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(uri); | ||
| ArgumentNullException.ThrowIfNull(contentType); | ||
|
|
||
| Uri = uri; | ||
| ContentType = contentType; | ||
| } | ||
|
Comment on lines
+17
to
+24
|
||
|
|
||
| /// <summary> | ||
| /// Gets the absolute URI of the requested static content. | ||
| /// </summary> | ||
| public Uri Uri { get; } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Cross-Platform Behavioral Consistency —
Concrete failure: an app writing the natural check Either document on this property that only path/query are portable and the scheme/host are platform-specific, or normalize what is handed to the callback so all platforms agree. |
||
|
|
||
| /// <summary> | ||
| /// Gets the resolved MIME content type of the requested static content, for example <c>image/png</c>. | ||
| /// </summary> | ||
| public string ContentType { get; } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,9 @@ | ||
| #nullable enable | ||
| override Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.ConnectHandler(Android.Webkit.WebView! platformView) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.set -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.BlazorWebViewStaticContentRequest(System.Uri! uri, string! contentType) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.ContentType.get -> string! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.Uri.get -> System.Uri! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,8 @@ | ||
| #nullable enable | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.set -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.BlazorWebViewStaticContentRequest(System.Uri! uri, string! contentType) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.ContentType.get -> string! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.Uri.get -> System.Uri! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,8 @@ | ||
| #nullable enable | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.set -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.BlazorWebViewStaticContentRequest(System.Uri! uri, string! contentType) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.ContentType.get -> string! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.Uri.get -> System.Uri! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,8 @@ | ||
| #nullable enable | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.set -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.BlazorWebViewStaticContentRequest(System.Uri! uri, string! contentType) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.ContentType.get -> string! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.Uri.get -> System.Uri! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,8 @@ | ||
| #nullable enable | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.set -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.BlazorWebViewStaticContentRequest(System.Uri! uri, string! contentType) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.ContentType.get -> string! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.Uri.get -> System.Uri! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,8 @@ | ||
| #nullable enable | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView.StaticContentCacheControlProvider.set -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.BlazorWebViewStaticContentRequest(System.Uri! uri, string! contentType) -> void | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.ContentType.get -> string! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest.Uri.get -> System.Uri! | ||
| Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView.StaticContentCacheControlProvider.get -> System.Func<Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewStaticContentRequest!, string?>? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| using System; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Microsoft.AspNetCore.Components.WebView.Maui | ||
| { | ||
| internal static class StaticContentCacheControl | ||
| { | ||
| // Historical default that disables all WebView caching of served content so that user scripts are always | ||
| // re-executed. It is applied unless the application opts a resource into caching via | ||
| // BlazorWebView.StaticContentCacheControlProvider. See https://github.com/dotnet/maui/issues/8279 | ||
| internal const string Default = "no-cache, max-age=0, must-revalidate, no-store"; | ||
|
|
||
|
Kebechet marked this conversation as resolved.
|
||
| // Returns the application-provided Cache-Control override for the request, or null to use the default. | ||
| internal static string? ResolveOverride(IBlazorWebView? blazorWebView, string requestUri, string contentType, ILogger? logger) | ||
| { | ||
| var provider = blazorWebView?.StaticContentCacheControlProvider; | ||
| if (provider is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| // The request handlers run on background threads, so guard against a malformed URI rather than letting | ||
| // an unexpected UriFormatException surface as a crash. If parsing fails we keep the default header. | ||
| if (!Uri.TryCreate(requestUri, UriKind.Absolute, out var uri)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| string? cacheControl; | ||
| try | ||
| { | ||
| cacheControl = provider(new BlazorWebViewStaticContentRequest(uri, contentType)); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| // The provider is arbitrary application code invoked from the native request-handling path. On Windows | ||
| // it runs inside an async void handler, where an escaped exception would also skip deferral.Complete() | ||
| // and hang the request. A faulty provider must not take down static asset serving, so keep the default. | ||
| logger?.StaticContentCacheControlProviderFailed(requestUri, ex); | ||
| return null; | ||
| } | ||
|
|
||
| // An empty or whitespace-only string is deliberately treated like null (keep the default): such a | ||
| // Cache-Control value is non-standard and engine-dependent, and is more likely an accidental result of | ||
| // string manipulation than an intentional opt-in. Explicit directives are the supported way to enable caching. | ||
| if (string.IsNullOrWhiteSpace(cacheControl)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| // Values containing CR/LF are also rejected: some platforms concatenate the value into a raw response | ||
| // header block, so a stray newline would produce a malformed response or allow header injection. | ||
| if (cacheControl.Contains('\r', StringComparison.Ordinal) || cacheControl.Contains('\n', StringComparison.Ordinal)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Null Safety and Defensive Coding — Two of the three rejection paths are silent, which leaves developers with no way to diagnose an ignored value. If an app returns a value containing CR/LF (e.g. built with
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness Verification — The CR/LF rejection (and the whitespace rejection above it) silently returns Concrete scenario: a provider builds its value from configuration and returns Add a log call (e.g. a |
||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return cacheControl; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Cross-Platform Behavioral Consistency — Android WebView does not use its HTTP cache for responses returned from
shouldInterceptRequest; the headers on aWebResourceResponseare surfaced to JS (fetch/XHR) but are not interpreted by the WebView for caching. Concrete scenario: an app optsimg.pngintomax-age=3600viaStaticContentCacheControlProvider, the new device test passes because thefetchresponse echoesmax-age=3600, but every subsequent page navigation still callsShouldInterceptRequestand re-reads the asset from the file provider — so the flicker reported in #8279 is not actually fixed on Android. Please verify on-device that a second load of a cacheable asset does not re-enter this method (see the companion comment on the test file); if the WebView does not honor it, either document the platform limitation on the public API or serve intercepted responses from an app-level cache.