Skip to content

Commit 82db52f

Browse files
committed
Update: client rendering the NotFoundPage if it's provided to the Router or when re-execution middleware is set.
1 parent 3dfd293 commit 82db52f

File tree

12 files changed

+84
-108
lines changed

12 files changed

+84
-108
lines changed

src/Components/Components/src/NavigationManager.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public event EventHandler<NotFoundEventArgs> OnNotFound
6262
private string? _uri;
6363
private bool _isInitialized;
6464
internal string NotFoundPageRoute { get; set; } = string.Empty;
65-
internal Type NotFoundPageType { get; set; } = default!;
6665

6766
/// <summary>
6867
/// Gets or sets the current base URI. The <see cref="BaseUri" /> is always represented as an absolute URI in string form with trailing slash.
@@ -212,7 +211,7 @@ private void NotFoundCore()
212211
}
213212
else
214213
{
215-
_notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute, NotFoundPageType)); // ToDo: sending only the type would be enough and then we would get the route later
214+
_notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute));
216215
}
217216
}
218217

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHand
66
Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void
77
Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func<string!, System.Threading.Tasks.Task!>! onNavigateTo) -> void
88
Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs
9-
Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(string! url, System.Type! notFoundPageType) -> void
10-
Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundPageType.get -> System.Type!
9+
Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(string! url) -> void
1110
Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string!
1211
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager!>! logger, System.IServiceProvider! serviceProvider) -> void
1312
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void

src/Components/Components/src/Routing/NotFoundEventArgs.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,12 @@ public sealed class NotFoundEventArgs : EventArgs
1313
/// </summary>
1414
public string Path { get; }
1515

16-
/// <summary>
17-
/// Gets the type of NotFoundPage component.
18-
/// </summary>
19-
public Type NotFoundPageType { get; }
20-
2116
/// <summary>
2217
/// Initializes a new instance of <see cref="NotFoundEventArgs" />.
2318
/// </summary>
24-
public NotFoundEventArgs(string url, Type notFoundPageType)
19+
public NotFoundEventArgs(string url)
2520
{
2621
Path = url;
27-
NotFoundPageType = notFoundPageType;
2822
}
2923

3024
}

src/Components/Components/src/Routing/Router.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ public async Task SetParametersAsync(ParameterView parameters)
156156
throw new InvalidOperationException($"The type {NotFoundPage.FullName} " +
157157
$"does not have a {typeof(RouteAttribute).FullName} applied to it.");
158158
}
159+
160+
var routeAttribute = (RouteAttribute)routeAttributes[0];
161+
if (routeAttribute.Template != null)
162+
{
163+
NavigationManager.NotFoundPageRoute = routeAttribute.Template;
164+
}
159165
}
160166

161167
if (!_onNavigateCalled)

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Builder;
45
using Microsoft.AspNetCore.Components.Endpoints.Rendering;
56
using Microsoft.AspNetCore.Components.Rendering;
67
using Microsoft.AspNetCore.Components.RenderTree;
@@ -82,27 +83,14 @@ private async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs ar
8283
{
8384
if (_httpContext.Response.HasStarted)
8485
{
85-
if (args.NotFoundPageType == null || string.IsNullOrEmpty(args.Path))
86-
{
87-
throw new InvalidOperationException("The NotFoundPageType and Path must be specified in the NotFoundEventArgs to render NotFoundPage when the response has started.");
88-
}
89-
var instance = Activator.CreateInstance(args.NotFoundPageType) as IComponent;
90-
if (instance == null)
91-
{
92-
throw new InvalidOperationException($"The type {args.NotFoundPageType.FullName} does not implement IComponent.");
93-
}
94-
if (_notFoundComponentId == -1)
95-
{
96-
_notFoundComponentId = AssignRootComponentId(instance);
97-
}
9886
if (string.IsNullOrEmpty(_notFoundUrl))
9987
{
100-
_notFoundUrl = $"{baseUri}{args.Path.TrimStart('/')}";
88+
_notFoundUrl = GetNotFoundUrl(baseUri, args);
10189
}
10290
var defaultBufferSize = 16 * 1024;
10391
await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
10492
using var bufferWriter = new BufferedTextWriter(writer);
105-
HandleNotFoundAfterResponseStarted(bufferWriter, _httpContext, _notFoundUrl, _notFoundComponentId);
93+
HandleNotFoundAfterResponseStarted(bufferWriter, _httpContext, _notFoundUrl);
10694
await bufferWriter.FlushAsync();
10795
}
10896
else
@@ -116,6 +104,22 @@ private async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs ar
116104
SignalRendererToFinishRendering();
117105
}
118106

107+
private string GetNotFoundUrl(string baseUri, NotFoundEventArgs args)
108+
{
109+
string path = args.Path;
110+
if (string.IsNullOrEmpty(path))
111+
{
112+
var pathFormat = _httpContext.Items[nameof(StatusCodePagesOptions)] as string;
113+
if (string.IsNullOrEmpty(pathFormat))
114+
{
115+
throw new InvalidOperationException("The NotFoundPage route must be specified or re-execution middleware has to be set to render NotFoundPage when the response has started.");
116+
}
117+
118+
path = pathFormat;
119+
}
120+
return $"{baseUri}{path.TrimStart('/')}";
121+
}
122+
119123
private async Task OnNavigateTo(string uri)
120124
{
121125
if (_httpContext.Response.HasStarted)

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,19 +226,20 @@ private static void HandleExceptionAfterResponseStarted(HttpContext httpContext,
226226
writer.Write("</template><blazor-ssr-end></blazor-ssr-end></blazor-ssr>");
227227
}
228228

229-
private static void HandleNotFoundAfterResponseStarted(TextWriter writer, HttpContext httpContext, string notFoundUrl, int componentId)
229+
private static void HandleNotFoundAfterResponseStarted(TextWriter writer, HttpContext httpContext, string notFoundUrl)
230230
{
231231
writer.Write("<blazor-ssr><template type=\"not-found\"");
232-
writer.Write($" componentId=\"{componentId}\"");
233-
writer.Write(">");
234-
writer.Write(HtmlEncoder.Default.Encode(OpaqueRedirection.CreateProtectedRedirectionUrl(httpContext, notFoundUrl)));
235-
writer.Write("</template><blazor-ssr-end></blazor-ssr-end></blazor-ssr>");
232+
WriteResponseTemplate(writer, httpContext, notFoundUrl);
236233
}
237234

238235
private static void HandleNavigationAfterResponseStarted(TextWriter writer, HttpContext httpContext, string destinationUrl)
239236
{
240237
writer.Write("<blazor-ssr><template type=\"redirection\"");
238+
WriteResponseTemplate(writer, httpContext, destinationUrl);
239+
}
241240

241+
private static void WriteResponseTemplate(TextWriter writer, HttpContext httpContext, string destinationUrl)
242+
{
242243
if (string.Equals(httpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
243244
{
244245
writer.Write(" from=\"form-post\"");

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer
5252
// wait for the non-streaming tasks (these ones), then start streaming until full quiescence.
5353
private readonly List<Task> _nonStreamingPendingTasks = new();
5454

55-
private int _notFoundComponentId = -1;
5655
private string _notFoundUrl = string.Empty;
5756

5857
public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)

src/Components/Web.JS/src/Rendering/StreamingRendering.ts

Lines changed: 29 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { synchronizeDomContent } from './DomMerging/DomSync';
88

99
let enableDomPreservation = true;
1010
let navigationEnhancementCallbacks: NavigationEnhancementCallbacks;
11-
let currentNotFoundRenderAbortController: AbortController | null;
12-
const acceptHeader = 'text/html; blazor-enhanced-nav=on';
1311

1412
export function attachStreamingRenderingListener(options: SsrStartOptions | undefined, callbacks: NavigationEnhancementCallbacks) {
1513
navigationEnhancementCallbacks = callbacks;
@@ -49,61 +47,10 @@ class BlazorStreamingUpdate extends HTMLElement {
4947
} else {
5048
switch (node.getAttribute('type')) {
5149
case 'redirection':
52-
// We use 'replace' here because it's closest to the non-progressively-enhanced behavior, and will make the most sense
53-
// if the async delay was very short, as the user would not perceive having been on the intermediate page.
54-
const destinationUrl = toAbsoluteUri(node.content.textContent!);
55-
const isFormPost = node.getAttribute('from') === 'form-post';
56-
const isEnhancedNav = node.getAttribute('enhanced') === 'true';
57-
if (isEnhancedNav && isWithinBaseUriSpace(destinationUrl)) {
58-
// At this point the destinationUrl might be an opaque URL so we don't know whether it's internal/external or
59-
// whether it's even going to the same URL we're currently on. So we don't know how to update the history.
60-
// Defer that until the redirection is resolved by performEnhancedPageLoad.
61-
const treatAsRedirectionFromMethod = isFormPost ? 'post' : 'get';
62-
const fetchOptions = undefined;
63-
performEnhancedPageLoad(destinationUrl, /* interceptedLink */ false, fetchOptions, treatAsRedirectionFromMethod);
64-
} else {
65-
if (isFormPost) {
66-
// The URL is not yet updated. Push a whole new entry so that 'back' goes back to the pre-redirection location.
67-
// WARNING: The following check to avoid duplicating history entries won't work if the redirection is to an opaque URL.
68-
// We could change the server-side logic to return URLs in plaintext if they match the current request URL already,
69-
// but it's arguably easier to understand that history non-duplication only works for enhanced nav, which is also the
70-
// case for non-streaming responses.
71-
if (destinationUrl !== location.href) {
72-
location.assign(destinationUrl);
73-
}
74-
} else {
75-
// The URL was already updated on the original link click. Replace so that 'back' goes to the pre-redirection location.
76-
location.replace(destinationUrl);
77-
}
78-
}
50+
redirect(node, false);
7951
break;
8052
case 'not-found':
81-
const componentId = node.getAttribute('componentId');
82-
if (!componentId) {
83-
console.error('Streaming content for not-found response does not have a componentId attribute.');
84-
break;
85-
}
86-
wrapDocumentInStreamingMarkers(componentId);
87-
const notFoundUrl = toAbsoluteUri(node.content.textContent!);
88-
89-
if (componentId) {
90-
currentNotFoundRenderAbortController?.abort();
91-
currentNotFoundRenderAbortController = new AbortController();
92-
const abortSignal = currentNotFoundRenderAbortController.signal;
93-
fetch(notFoundUrl, Object.assign(<RequestInit>{
94-
signal: abortSignal,
95-
mode: 'no-cors',
96-
headers: {
97-
'accept': acceptHeader,
98-
},
99-
}, undefined))
100-
.then(response => response.text())
101-
.then(html => {
102-
const docFrag = document.createRange().createContextualFragment(html);
103-
insertStreamingContentIntoDocument(componentId, docFrag);
104-
})
105-
.catch(error => console.error('Failed to fetch not-found content:', error));
106-
}
53+
redirect(node, true);
10754
break;
10855
case 'error':
10956
// This is kind of brutal but matches what happens without progressive enhancement
@@ -116,19 +63,33 @@ class BlazorStreamingUpdate extends HTMLElement {
11663
}
11764
}
11865

119-
function wrapDocumentInStreamingMarkers(componentIdAsString: string): void {
120-
// Add a comment before the <head>
121-
const markerStart = document.createComment(`bl:${componentIdAsString}`);
122-
const head = document.querySelector('head');
123-
document.documentElement.insertBefore(markerStart, head);
124-
125-
// Add a comment after the <body> or after the <head> if <body> is null
126-
const markerEnd = document.createComment(`/bl:${componentIdAsString}`);
127-
const body = document.querySelector('body');
128-
if (body && body.parentNode) {
129-
body.parentNode.insertBefore(markerEnd, body.nextSibling);
130-
} else if (head && head.parentNode) {
131-
head.parentNode.insertBefore(markerEnd, head.nextSibling);
66+
function redirect(node: HTMLTemplateElement, changeUrl: boolean): void {
67+
// We use 'replace' here because it's closest to the non-progressively-enhanced behavior, and will make the most sense
68+
// if the async delay was very short, as the user would not perceive having been on the intermediate page.
69+
const destinationUrl = toAbsoluteUri(node.content.textContent!);
70+
const isFormPost = node.getAttribute('from') === 'form-post';
71+
const isEnhancedNav = node.getAttribute('enhanced') === 'true';
72+
if (isEnhancedNav && isWithinBaseUriSpace(destinationUrl)) {
73+
// At this point the destinationUrl might be an opaque URL so we don't know whether it's internal/external or
74+
// whether it's even going to the same URL we're currently on. So we don't know how to update the history.
75+
// Defer that until the redirection is resolved by performEnhancedPageLoad.
76+
const treatAsRedirectionFromMethod = isFormPost ? 'post' : 'get';
77+
const fetchOptions = undefined;
78+
performEnhancedPageLoad(destinationUrl, /* interceptedLink */ false, fetchOptions, treatAsRedirectionFromMethod, changeUrl);
79+
} else {
80+
if (isFormPost) {
81+
// The URL is not yet updated. Push a whole new entry so that 'back' goes back to the pre-redirection location.
82+
// WARNING: The following check to avoid duplicating history entries won't work if the redirection is to an opaque URL.
83+
// We could change the server-side logic to return URLs in plaintext if they match the current request URL already,
84+
// but it's arguably easier to understand that history non-duplication only works for enhanced nav, which is also the
85+
// case for non-streaming responses.
86+
if (destinationUrl !== location.href) {
87+
location.assign(destinationUrl);
88+
}
89+
} else {
90+
// The URL was already updated on the original link click. Replace so that 'back' goes to the pre-redirection location.
91+
location.replace(destinationUrl);
92+
}
13293
}
13394
}
13495

src/Components/Web.JS/src/Services/NavigationEnhancement.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ function onDocumentSubmit(event: SubmitEvent) {
192192
}
193193
}
194194

195-
export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post') {
195+
export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post', changeUrl: boolean = true) {
196196
performingEnhancedPageLoad = true;
197197

198198
// First, stop any preceding enhanced page load
@@ -257,7 +257,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
257257
// For 301/302/etc redirections to internal URLs, the browser will already have followed the chain of redirections
258258
// to the end, and given us the final content. We do still need to update the current URL to match the final location,
259259
// then let the rest of enhanced nav logic run to patch the new content into the DOM.
260-
if (response.redirected || treatAsRedirectionFromMethod) {
260+
if (changeUrl && (response.redirected || treatAsRedirectionFromMethod)) {
261261
const treatAsGet = treatAsRedirectionFromMethod ? (treatAsRedirectionFromMethod === 'get') : isGetRequest;
262262
if (treatAsGet) {
263263
// For gets, the intermediate (redirecting) URL is already in the address bar, so we have to use 'replace'
@@ -274,12 +274,12 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
274274

275275
// For enhanced nav redirecting to an external URL, we'll get a special Blazor-specific redirection command
276276
const externalRedirectionUrl = response.headers.get('blazor-enhanced-nav-redirect-location');
277-
if (externalRedirectionUrl) {
277+
if (changeUrl && externalRedirectionUrl) {
278278
location.replace(externalRedirectionUrl);
279279
return;
280280
}
281281

282-
if (!response.redirected && !isGetRequest && isSuccessResponse) {
282+
if (changeUrl && !response.redirected && !isGetRequest && isSuccessResponse) {
283283
// If this is the result of a form post that didn't trigger a redirection.
284284
if (!isForSamePath(response.url, currentContentUrl)) {
285285
// In this case we don't want to push the currentContentUrl to the history stack because we don't know if this is a location
@@ -296,7 +296,9 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
296296
}
297297

298298
// Set the currentContentUrl to the location of the last completed navigation.
299-
currentContentUrl = response.url;
299+
if (changeUrl) {
300+
currentContentUrl = response.url;
301+
}
300302

301303
const responseContentType = response.headers.get('content-type');
302304
if (responseContentType?.startsWith('text/html') && initialContent) {

src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,20 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute(
187187
{
188188
var newNext = RerouteHelper.Reroute(app, routeBuilder, next);
189189
return new StatusCodePagesMiddleware(next,
190-
Options.Create(new StatusCodePagesOptions() {
190+
Options.Create(new StatusCodePagesOptions()
191+
{
191192
HandleAsync = CreateHandler(pathFormat, queryFormat, newNext),
192-
CreateScopeForErrors = createScopeForErrors
193+
CreateScopeForErrors = createScopeForErrors,
194+
PathFormat = pathFormat
193195
})).Invoke;
194196
});
195197
}
196198

197199
var options = new StatusCodePagesOptions
198200
{
199201
HandleAsync = CreateHandler(pathFormat, queryFormat),
200-
CreateScopeForErrors = createScopeForErrors
202+
CreateScopeForErrors = createScopeForErrors,
203+
PathFormat = pathFormat
201204
};
202205
var wrappedOptions = new OptionsWrapper<StatusCodePagesOptions>(options);
203206
return app.UseMiddleware<StatusCodePagesMiddleware>(wrappedOptions);

0 commit comments

Comments
 (0)