How to Fix 404 Not Found ForceLoad Hack Blazor Code

To manage URIs and navigation from Blazor code, developers must be proficient with the NavigationManager. We would like to draw attention to a minor but significant aspect of the NavigateTo function that NavigationManager offers in this blog post.

TLDR;

It’s critical to comprehend the forceLoad parameter on the NavigateTo method of the NavigationManager. Regardless of whether the URI would typically be handled by the client-side router, setting this Boolean to true in code compels the browser to load the newly-requested page from the server and instructs the NavigationManager to forego its customary client-side routing.

Backstory (MultiHead Solution Structure)

My preference when creating Blazor applications is to use an application architecture called “MultiHead,” or Dual Mode. In actuality, this means that every page, component, and piece of content is housed in a different project assembly. Subsequently, ASP.NET Core Razor components from this Razor class library are referenced and consumed by distinct Blazor WebAssembly and Blazor Server (SignalR) projects. I deploy the “Client” (Wasm) project for end users instead of the “Server” (SignalR) project for a number of reasons, the primary one being that I find it easier to debug using the latter during development. Every project “head” is indicated by the yellow arrows in the picture. Next, all of the Razor pages, ViewModels deriving from ComponentBase, and wwwroot content (i.e., css, js, and images) that are shared by both heads are contained in the “App” project, which is indicated in blue. But I’m getting off topic. That should not be confused with the “Shared” project, which has code that is shared by the Web API (“Api”) and “App” projects in the solution.

This method, in my opinion, is effective for over 95% of the necessary code files. But occasionally, depending on the deployment model being used, there are differences in behavior that call for different NuGet packages or strategies.

WebAssembly versus SignalR Deployment Model Differences

As was previously mentioned, some code is unique to the deployment model, for example, when integrating code with Identity Server 4. For instance, only WebAssembly (found in the Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace) uses the RemoteAuthenticatorView component. It is obviously nonsensical to refer to the authentication library from the Server (SignalR) project that was created for WebAssembly. Similarly, there are ways to use HttpContext in the Server project that do not apply to WebAssembly. More subtle code differences can also occur, as demonstrated by the Router component, which is *almost* but not quite applicable to both projects:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(MainLayoutViewModel).Assembly" PreferExactMatches="@true"
            AdditionalAssemblies="new[] { typeof(MSC.YourApp.Server.Program).Assembly }">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

The component that allows routing to Razor components in a Blazor app is the Router component (seen above), which is used by the App component. The Router component uses the AdditionalAssemblies parameter to specify, surprise, additional assemblies to use when looking for routable components. In this case, really the only thing that needs to change between deployment modes is the way the AdditionalAssemblies attribute uses the Server project. For the client-side project, this attribute value is then reversed.

However, both heads can utilize the same RedirectToLogin component with a little bit of conditional logic:

protected override void OnInitialized()
{
    if (AppSettings.IsWebAssembly)
    {
    NavigationManager.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}");
    }
    else
    {
    NavigationManager.NavigateTo($"LoginIDP?redirectUri={Uri.EscapeDataString(NavigationManager.Uri)}", 
        forceLoad: true);
    }
}

The code above performs a straightforward check to determine whether the application is utilizing a SignalR connection or WebAssembly to run entirely on the client. Naturally, the configuration of all this authentication could easily fill a blog post or two, but that knowledge is not required to comprehend the issue we will now address.

The Problem

Look closely at the code above, especially at NavigationManager’s NavigateTo method. It’s crucial to understand that failing to set the forceLoad parameter to true will result in a 404 Page Not Found error. “Client-side routing” is used by the router component, which explains why. That page does not exist in the client-side code, despite the fact that we gave the Router component’s AdditionalAssemblies parameter the assembly that contains the LoginIDP page it wishes to navigate to. Say what?

As you can see, the LoginIDP page’s actual file name is LoginIDP.cshtml. In the context of Blazor server projects,.cshtml files hold code that is compiled and run on the server. As opposed to this, a.NET runtime that is integrated with the browser compiles Razor pages (.razor) into.dll files, which are then sent to the client side for execution.

Red Herring Alert

When I first encountered this 404 Not Found problem, the LoginIDP page would load if I navigated to it using an anchor tag that contained only the word “LoginIDP” and did not include the redirectUri parameter as stated in the code above. What then did I do? I typed the URL to the LoginIDP page into the address bar of my browser, this time omitting the redirectUri parameter, and hit Enter. That also worked! Nevertheless, using the NavigationManager with the redirectUri parameter did not function.

After a long day, it was getting late, and I was certain that the parameter routing was the cause of the issue. I attempted every workaround I could find, including changing my OnGetAsync(string redirectUri) to use [BindProperty(SupportsGet = true)] instead of adding “options.Conventions.AddPageRoute” in Startup and adding optional page route parameters. After trying everything, I came across this article through Google searches:

Naturally, I reasoned, that had to be it; perhaps it was a Microsoft regression bug! I lost more valuable time on this wrong turn of events than I would like to admit. Let’s just say that falling asleep was looking pretty good by the time I discovered and comprehended the real cause and remedy.

The Solution

To restate the solution in more detail, the trick is to disable client-side routing by setting the forceLoad parameter to true. Whether the URI is something the client-side router would typically handle or not, doing this will compel the application to load the requested page from the server.

Now that the server-side code is running, it is safe to use HttpContext to instruct the LoginIDP page to challenge the current request using the OpenIdConnect scheme and verify the user’s authentication status, as shown in the code below.

public async Task OnGetAsync(string redirectUri)
{
  if (string.IsNullOrWhiteSpace(redirectUri))
  {
    redirectUri = Url.Content("~/");
  }

  if (HttpContext.User.Identity.IsAuthenticated)
  {
    Response.Redirect(redirectUri);
  }

  await HttpContext.ChallengeAsync(
    OpenIdConnectDefaults.AuthenticationScheme,
    new AuthenticationProperties { RedirectUri = redirectUri });
}

Well Done!

If you were unaware of this NavigationManager forceLoad nuance before, you have now added a little more in-depth knowledge to your Blazor toolkit. As always, I hope this post is helpful to you at some point or that it will help you avoid wasting time and frustration in certain situations!

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *