Skip to content
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

NavigationManager.NavigateTo does not trigger NavigationException as expected, causing redirection failure when using Htmxor #64

Open
CoCoNuTeK opened this issue Sep 11, 2024 · 3 comments

Comments

@CoCoNuTeK
Copy link

Whenever I attempt to call RedirectManager.RedirectTo, I expect NavigationManager.NavigateTo(uri) to throw a NavigationException, which should be handled by the framework to facilitate the redirection to the specified URL.

However, instead of triggering the expected NavigationException, what gets triggered is an HtmxorNavigationException. This leads to an Object reference not set to an instance of an object error, which ultimately prevents the redirection from working properly when the Htmxor package is active.

Expected Behavior:

  • NavigationManager.NavigateTo(uri) should trigger the NavigationException, allowing the framework to handle the redirection seamlessly.

Actual Behavior:

  • The HtmxorNavigationException is thrown instead, causing a null reference error and breaking the redirection flow.

Steps to Reproduce:

  1. Attempt to call RedirectManager.RedirectTo with Htmxor active.
  2. Observe that NavigationException is not triggered, and redirection fails due to HtmxorNavigationException.
@egil
Copy link
Owner

egil commented Sep 11, 2024

Thanks for the report. What is interesting is that HtmxorNavigationException inherits from NavigationException, so the behavior should be preserved, at least in my testing.

Can you provide a minimal runnable code sample that I can use to understand your scenario better?

@CoCoNuTeK
Copy link
Author

Using your pizza example code, you can place RedirectManager.RedirectTo("/myorders"); inside the OnInitializedAsync method of Index.razor. Redirections don't seem to work with RedirectManager at all. My idea was to use HTMX redirection via HX-Redirect, which performs a client-side redirect to a new location. I'm unsure how to trigger that directly from the @code section, but it would likely provide a smoother experience, especially with Blazor SSR.

In contrast, Blazor's typical way to handle redirection is by throwing a NavigationManagerException, which the framework manages as a redirect. Is there any way to return custom headers from a Blazor component? While Razor pages handle HTML injection well with direct routing, I'm not sure how to modify the headers (e.g., to include an HTTP request header for the HX-Redirect).

@egil
Copy link
Owner

egil commented Sep 12, 2024

Using your pizza example code, you can place RedirectManager.RedirectTo("/myorders"); inside the OnInitializedAsync method of Index.razor. Redirections don't seem to work with RedirectManager at all.

I assume you are talking about the IdentityRedirectManager type? Looks like you are not the only one that experience this (dotnet/aspnetcore#53996).

Anyway, Htmxor do the same thing as Blazor SSR, but it provides its own HtmxorNavigationManager (registrered by default) that throws HtmxorNavigationException that has additional data included which allows Hrmxor to redirect in the expected way.

So, the behavior should be identical to Blazor SSR, but do check if your code is using HtmxorNavigationManager.

The key part of the HtmxorRenderer that handles this is here:

public static ValueTask<RenderedComponentHtmlContent> HandleNavigationException(HttpContext httpContext, HtmxorNavigationException navigationException)
{
var htmxContext = httpContext.GetHtmxContext();
if (httpContext.Response.HasStarted)
{
// If we're not doing streaming SSR, this has no choice but to be a fatal error because there's no way to
// communicate the redirection to the browser.
// If we are doing streaming SSR, this should not generally happen because if you navigate during the initial
// synchronous render, the response would not yet have started, and if you do so during some later async
// phase, we would already have exited this method since streaming SSR means not awaiting quiescence.
throw new InvalidOperationException(
"A navigation command was attempted during prerendering after the server already started sending the response. " +
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.");
}
else if (IsPossibleExternalDestination(httpContext.Request, navigationException.Location))
{
// For progressively-enhanced nav, we prefer to use opaque redirections for external URLs rather than
// forcing the request to be retried, since that allows post-redirect-get to work, plus avoids a
// duplicated request. The client can't rely on receiving this header, though, since non-Blazor endpoints
// wouldn't return it.
// Originally Blazor would return a blazor-enhanced-nav-redirect-location header. Here we rely on Htmx's
// handling of headers for htmx requests and uses the built in browser redirect for non-htmx requests.
// TODO: validate that this works as intended.
if (htmxContext.Request.IsHtmxRequest)
{
htmxContext.Response.Redirect(OpaqueRedirection.CreateProtectedRedirectionUrl(httpContext, navigationException.Location));
}
else
{
httpContext.Response.Redirect(OpaqueRedirection.CreateProtectedRedirectionUrl(httpContext, navigationException.Location));
}
return new ValueTask<RenderedComponentHtmlContent>(RenderedComponentHtmlContent.Empty);
}
var navOptions = navigationException.Options;
if (htmxContext.Request.IsHtmxRequest)
{
if (navOptions.ForceLoad)
{
htmxContext.Response.Redirect(navigationException.RequestedLocation);
if (navOptions.ReplaceHistoryEntry)
{
htmxContext.Response.ReplaceUrl(navigationException.RequestedLocation);
}
}
else
{
if (navOptions.ReplaceHistoryEntry)
{
htmxContext.Response.ReplaceUrl(navigationException.RequestedLocation);
}
else
{
htmxContext.Response.Redirect(navigationException.RequestedLocation);
}
}
}
else
{
httpContext.Response.Redirect(navigationException.Location);
}
return new ValueTask<RenderedComponentHtmlContent>(RenderedComponentHtmlContent.Empty);
}

I'm unsure how to trigger that directly from the @code section, but it would likely provide a smoother experience, especially with Blazor SSR.

You need access to the HtmxContext object that is created with each request, and is available as an injected service as well as a cascading value, Then, e.g. during OnInitialized, you can set HX response headers that will cause htmx to do client side stuff, e.g.:

// note: this is untested code
[CascadingParameter]
public HtmxContext Context { get; set; }

protected override void OnInitialized()
{
  Context.Response.Location("/url/to/other/page");
}

Btw. I have been away from this project for a few months, simply have not had the time, so some of this is based on vague memories and may not all be correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants