Skip to content

HttpNavigationManager no longer uses NavigationException #61306

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

Merged
merged 23 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0be84e4
Rise even instead of throwing.
ilonatommy Apr 3, 2025
2e5f7dc
Clean up, delay not needed.
ilonatommy Apr 3, 2025
5ebaaac
Fix typo + test old way of workign as well.
ilonatommy Apr 3, 2025
1ea43be
Update name to be full namespace + remove readonly.
ilonatommy Apr 4, 2025
e9bd57e
Feedback - interactive SSR updates required exposing some methods.
ilonatommy Apr 7, 2025
ebc2537
Fix missing xml.
ilonatommy Apr 7, 2025
dff3c70
Fix build of tests.
ilonatommy Apr 7, 2025
19d3fe9
Fix nullable.
ilonatommy Apr 7, 2025
def3e5c
Feedback - limit public API changes.
ilonatommy Apr 8, 2025
9f79f51
Handle the case when response started.
ilonatommy Apr 8, 2025
e647f2a
Proposal of fixing external navigation.
ilonatommy Apr 9, 2025
5e6d104
Update src/Components/test/testassets/Components.TestServer/RazorComp…
ilonatommy Apr 9, 2025
8413a40
Merge branch 'main' into fix-59451
ilonatommy Apr 10, 2025
9f7fceb
Merge branch 'fix-59451' of https://github.com/ilonatommy/aspnetcore …
ilonatommy Apr 10, 2025
aea9826
Feedback.
ilonatommy Apr 11, 2025
bafa937
More effective stopping of the renderer.
ilonatommy Apr 14, 2025
3521c8f
POST cannot safely redirect like GET does, the body should be preserved.
ilonatommy Apr 14, 2025
a3ca937
Reuse the logic from navigation exception.
ilonatommy Apr 14, 2025
5d7eb68
Editing the ongoing render batch is not possible - for non-streaming …
ilonatommy Apr 14, 2025
69426fd
Missing change for the last commit.
ilonatommy Apr 15, 2025
e0b407c
Rename switch to match http and remote navigator.
ilonatommy Apr 15, 2025
dc45f3e
Adjust test for the new behavior.
ilonatommy Apr 15, 2025
b36db8b
Fix exception - driven navigation.
ilonatommy Apr 15, 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
2 changes: 1 addition & 1 deletion src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri
Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttribute.SupplyParameterFromPersistentComponentStateAttribute() -> void
Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions
static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<TService>(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,68 @@ namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager
{
private const string _enableThrowNavigationException = "Microsoft.AspNetCore.Components.Endpoints.HttpNavigationManager.EnableThrowNavigationException";

private static bool _throwNavigationException =>
AppContext.TryGetSwitch(_enableThrowNavigationException, out var switchValue) && switchValue;

void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri);

protected override void NavigateToCore(string uri, NavigationOptions options)
{
var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri;
throw new NavigationException(absoluteUriString);
if (_throwNavigationException)
{
throw new NavigationException(absoluteUriString);
}
else
{
if (!IsInternalUri(absoluteUriString))
{
// it's an external navigation, avoid Uri validation exception
BaseUri = GetBaseUriFromAbsoluteUri(absoluteUriString);
}
Uri = absoluteUriString;
NotifyLocationChanged(isInterceptedLink: false);
}
}

// ToDo: the following are copy-paste, consider refactoring to a common place
private bool IsInternalUri(string uri)
{
var normalizedBaseUri = NormalizeBaseUri(BaseUri);
return uri.StartsWith(normalizedBaseUri, StringComparison.OrdinalIgnoreCase);
}

private static string GetBaseUriFromAbsoluteUri(string absoluteUri)
{
// Find the position of the first single slash after the scheme (e.g., "https://")
var schemeDelimiterIndex = absoluteUri.IndexOf("://", StringComparison.Ordinal);
if (schemeDelimiterIndex == -1)
{
throw new ArgumentException($"The provided URI '{absoluteUri}' is not a valid absolute URI.");
}

// Find the end of the authority section (e.g., "https://example.com/")
var authorityEndIndex = absoluteUri.IndexOf('/', schemeDelimiterIndex + 3);
if (authorityEndIndex == -1)
{
// If no slash is found, the entire URI is the authority (e.g., "https://example.com")
return NormalizeBaseUri(absoluteUri + "/");
}

// Extract the base URI up to the authority section
return NormalizeBaseUri(absoluteUri.Substring(0, authorityEndIndex + 1));
}

private static string NormalizeBaseUri(string baseUri)
{
var lastSlashIndex = baseUri.LastIndexOf('/');
if (lastSlashIndex >= 0)
{
baseUri = baseUri.Substring(0, lastSlashIndex + 1);
}

return baseUri;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -84,6 +85,26 @@ private void SetNotFoundResponse(object? sender, EventArgs args)
SignalRendererToFinishRendering();
}

private void OnNavigateTo(object? sender, LocationChangedEventArgs args)
{
if (_httpContext.Response.HasStarted)
{
// We cannot redirect after the response has already started
RenderMetaRefreshTag(args.Location);
}
else
{
_httpContext.Response.Redirect(args.Location);
}
SignalRendererToFinishRendering();
}

private void RenderMetaRefreshTag(string location)
{
var metaTag = $"<meta http-equiv=\"refresh\" content=\"0;url={location}\" />";
_httpContext.Response.WriteAsync(metaTag);
}

private void UpdateNamedSubmitEvents(in RenderBatch renderBatch)
{
if (renderBatch.NamedEventChanges is { } changes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ internal async Task InitializeStandardComponentServicesAsync(
if (navigationManager != null)
{
navigationManager.OnNotFound += SetNotFoundResponse;
navigationManager.LocationChanged += OnNavigateTo;
}

var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>();
Expand Down
68 changes: 66 additions & 2 deletions src/Components/Server/src/Circuits/RemoteNavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ internal sealed partial class RemoteNavigationManager : NavigationManager, IHost
private readonly ILogger<RemoteNavigationManager> _logger;
private IJSRuntime _jsRuntime;
private bool? _navigationLockStateBeforeJsRuntimeAttached;
private const string _enableThrowNavigationException = "Microsoft.AspNetCore.Components.Endpoints.HttpNavigationManager.EnableThrowNavigationException";
private static bool _throwNavigationException =>
AppContext.TryGetSwitch(_enableThrowNavigationException, out var switchValue) && switchValue;

public event EventHandler<Exception>? UnhandledException;

Expand Down Expand Up @@ -88,7 +91,21 @@ protected override void NavigateToCore(string uri, NavigationOptions options)
if (_jsRuntime == null)
{
var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri;
throw new NavigationException(absoluteUriString);
if (_throwNavigationException)
{
throw new NavigationException(absoluteUriString);
}
else
{
if (!IsInternalUri(absoluteUriString))
{
// it's an external navigation, avoid Uri validation exception
BaseUri = GetBaseUriFromAbsoluteUri(absoluteUriString);
}
Uri = absoluteUriString;
NotifyLocationChanged(isInterceptedLink: false);
return;
}
}

_ = PerformNavigationAsync();
Expand Down Expand Up @@ -123,13 +140,60 @@ async Task PerformNavigationAsync()
}
}

private bool IsInternalUri(string uri)
{
var normalizedBaseUri = NormalizeBaseUri(BaseUri);
return uri.StartsWith(normalizedBaseUri, StringComparison.OrdinalIgnoreCase);
}

private static string GetBaseUriFromAbsoluteUri(string absoluteUri)
{
// Find the position of the first single slash after the scheme (e.g., "https://")
var schemeDelimiterIndex = absoluteUri.IndexOf("://", StringComparison.Ordinal);
if (schemeDelimiterIndex == -1)
{
throw new ArgumentException($"The provided URI '{absoluteUri}' is not a valid absolute URI.");
}

// Find the end of the authority section (e.g., "https://example.com/")
var authorityEndIndex = absoluteUri.IndexOf('/', schemeDelimiterIndex + 3);
if (authorityEndIndex == -1)
{
// If no slash is found, the entire URI is the authority (e.g., "https://example.com")
return NormalizeBaseUri(absoluteUri + "/");
}

// Extract the base URI up to the authority section
return NormalizeBaseUri(absoluteUri.Substring(0, authorityEndIndex + 1));
}

private static string NormalizeBaseUri(string baseUri)
{
var lastSlashIndex = baseUri.LastIndexOf('/');
if (lastSlashIndex >= 0)
{
baseUri = baseUri.Substring(0, lastSlashIndex + 1);
}

return baseUri;
}

/// <inheritdoc />
public override void Refresh(bool forceReload = false)
{
if (_jsRuntime == null)
{
var absoluteUriString = ToAbsoluteUri(Uri).AbsoluteUri;
throw new NavigationException(absoluteUriString);
if (_throwNavigationException)
{
throw new NavigationException(absoluteUriString);
}
else
{
Uri = absoluteUriString;
NotifyLocationChanged(isInterceptedLink: false);
return;
}
}

_ = RefreshAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1399,4 +1399,16 @@ public void CanPersistMultiplePrerenderedStateDeclaratively_Auto_PersistsOnWebAs
Browser.Equal("restored 2", () => Browser.FindElement(By.Id("auto-2")).Text);
Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-auto-2")).Text);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void NavigatesWithInteractivityByRequestRedirection(bool controlFlowByException)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Components.Endpoints.EnableThrowNavigationException", isEnabled: controlFlowByException);
Navigate($"{ServerPathBase}/routing/ssr-navigate-to");
Browser.Equal("Click submit to navigate to home", () => Browser.Exists(By.Id("test-info")).Text);
Browser.Click(By.Id("redirectButton"));
Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,18 @@ public void CanUseServerAuthenticationStateByDefault()
Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text);
Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text);
}

[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
public void NavigatesWithoutInteractivityByRequestRedirection(bool controlFlowByException, bool isStreaming)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Components.Endpoints.EnableThrowNavigationException", isEnabled: controlFlowByException);
string streaming = isStreaming ? $"streaming-" : "";
Navigate($"{ServerPathBase}/routing/ssr-{streaming}navigate-to");
Browser.Equal("Click submit to navigate to home", () => Browser.Exists(By.Id("test-info")).Text);
Browser.Click(By.Id("redirectButton"));
Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@page "/routing"
<h3>Routing test cases</h3>
<h3 id="test-info">Routing test cases</h3>

<ul>
<li>
Expand All @@ -24,12 +24,6 @@
<a href="routing/complex-segment(value)">Complex segments</a>
</li>
<li>
<a href="routing/not-found-ssr">Not found page for Static Server Rendering</a>
</li>
<li>
<a href="routing/not-found-webassembly">Not found page for Interactive WebAssembly rendering</a>
</li>
<li>
<a href="routing/not-found-server">Not found page for Interactive Server rendering</a>
<a href="routing/ssr-navigate-to">Redirect on SSR page</a>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@page "/routing/ssr-navigate-to"
@using Microsoft.AspNetCore.Components.Forms
@inject NavigationManager NavigationManager

<p id="test-info">Click submit to navigate to home</p>
<form method="post" @onsubmit="Submit" @formname="MyUniqueFormName">
<AntiforgeryToken />
<button type="submit" id="redirectButton" class="btn btn-primary">Redirect</button>
</form>

@code {
private void Submit()
{
NavigationManager.NavigateTo("/subdir/routing");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@page "/routing/ssr-streaming-navigate-to"
@attribute [StreamRendering]
@using Microsoft.AspNetCore.Components.Forms
@inject NavigationManager NavigationManager

<p id="test-info">Click submit to navigate to home</p>
<form method="post" @onsubmit="Submit" @formname="MyUniqueFormName">
<AntiforgeryToken />
<button type="submit" id="redirectButton" class="btn btn-primary">Redirect</button>
</form>

@code {
private async Task Submit()
{
await Task.Delay(1000); // Simulate some async work to let the streaming begin
NavigationManager.NavigateTo("/subdir/routing");
}
}

Loading