Blazor Server 404 page when data is not found, with HTTP status code
Asked Answered
M

2

6

This question has been asked many times over the last few years, but all answers appear to either:

  1. Relate to the route not being found (in this case I refer strictly to data)
  2. Require use of the IHttpContextAccessor (which Microsoft docs explicitly state not to do)
  3. Redirecting to a resource that doesn't return the correct 404 headers (no good for SEO)
  4. Show a friendly error in the current component (no good for SEO and requires other functionality to be hidden in the component as a result)
  5. Involve a significant number of aspects/components/pages and dependency injection (very complicated for something that historically has been relatively easy, e.g. with web forms or MVC)

Within the ASP.NET Framework it was easy to return a 404 status. This could be intercepted by IIS or returned as a bespoke page. It didn't matter what level in the control/business logic hierarchy this occurred at:

Web forms

if (myThing == null) {
    var ctx = HttpContext.Current;
    ctx.Response.Clear();
    ctx.Response.StatusCode = System.Net.HttpStatusCode.NotFound;
    ctx.Response.StatusDescription = "No data was found against that URL.";
    ctx.Response.End();
}

ASP.Net Framework MVC

if (myThing == null) {
    ViewBag.ErrorMessage = "No data was found against that URL.";
    Response.StatusCode = (int)HttpStatusCode.NotFound;
    return View("~/Views/Error/NotFound.cshtml");
}

.Net Core Razor Pages

if (myThing == null) {
    return NotFound();
}

.Net Core Blazor Server Components (@page)?

Is there an equivalent within a Blazor Component (.razor file) when that component is configured to act as a @page (not a component placed inside a .cshtml file)?

I appreciate from the comments that the page is found and is processing the request, but to a search engine two similar URL's with different ID parameters are indexed as different pages.

A use case example would be this:

@page "/Product/{Id:int}"
@using Microsoft.EntityFrameworkCore

<h1>Product Details</h1>

@code {

    [Inject]
    IDbContextFactory<MyDbContext> _dbContextFactory { get; set; } = default!;

    [Parameter]
    public int? Id { get; set; }

    protected override async Task OnInitializedAsync()
    {

        using var context = _dbContextFactory.CreateDbContext();

        var thingToEdit = await context.MyEntities
                    .Where(e => e.EntityId == Id.Value)
                    .FirstOrDefaultAsync();

        if (thingToEdit == null)
        {
            // Go to/show a 404 page/message, but must set HTTP status code header
        }
        else
        {
            // Show product details in this component page
        }
    }
}
Melee answered 12/10, 2022 at 14:5 Comment(10)
[polite] Are you fundamentally misunderstanding the issue? In any SPA you already have a living, breathing page. It's not a 404. You can't transform it into one without reloading it. The reason you can't find an answer is because one doesn't exist without fundamental changes to the web, or divine intervention!Sisco
404 is a page not found, but You have the page. What You don't have is the object You're looking to edit.Skidproof
Hi, yes I completely understand and agree with you. I've updated the question with more clarity. If I'm still off the mark then please comment and I'll revise further or delete. @MrCakaShaunCurtis its not actually an SPA to be fair, as the components are acting as @pages.Melee
So your current problem is that different paramrter ID lead to different pages? Or is this what you expect? I'm a little confused. Could you tell me what you expect?Eklund
If a corresponding entity is found for an ID (e.g. ID==3) then the entity details should be shown in that component page. However, if an entity is not found (e.g. URL corruption, user abuse or a discontinued entity [the most important reason]) then I need a 404 status code/page. This will also serve to remove old data from search engines. The actual 404 page isn't as important as the 404 status.Melee
404 status is a response to a page not found. If just an object does not exist, it should not trigger a 404 response unless you manually add a response to it.Eklund
Yes @Eklund that is exactly what is needed. It's an SEO thing.Melee
Are we talking Server or WASM? (Or have I missed that).Sisco
Blazor Server with components as pages (not Core MVC).Melee
I think this may not be possible, the status of 404 is returned by the browser running program autonomously, isn't manual change against the HTTP?Eklund
B
2

I propose you a mixed solution, that use NavigationManager to redirect to a not found page managed by a simple handler by the app.

In the page you could have something like this:

@page "/Product/{Id:int}"
@using Microsoft.EntityFrameworkCore

<h1>Product Details</h1>

@code {

    [Inject]
    IDbContextFactory<MyDbContext> _dbContextFactory { get; set; } = default!;

    [Inject]
    public NavigationManager NavigationManager { get; set; }

    [Parameter]
    public int? Id { get; set; }

    protected override async Task OnInitializedAsync()
    {

        using var context = _dbContextFactory.CreateDbContext();

        var thingToEdit = await context.MyEntities
                    .Where(e => e.EntityId == Id.Value)
                    .FirstOrDefaultAsync();

        if (thingToEdit == null)
        {
            NavigationManager.NavigateTo($"/product-not-found/{Id}", forceLoad: true);
        }
        else
        {
            // Show product details in this component page
        }
    }
}

And than you could register an handler like this:

app.MapGet("/product-not-found/{Id:int}", (int Id) => {
    // return status code 404 and a message
    return Results.NotFound($"Product with id {Id} not found");
});

Anyway if you doesn't register the handler, the default behaviour of the Router component return a 404 http status page.

Bozen answered 21/11, 2023 at 14:51 Comment(0)
C
1

Credit: How to replace Blazor default "soft 404" with an actual 404 status code response

I just added a Razor component called PageNotFound:

@using Microsoft.AspNetCore.Http
@using Tuneality.Web.Server.Views.Layout
@inject IHttpContextAccessor HttpContextAccessor

<PageTitle>Not Found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
  @ChildContent
</LayoutView>

@code {
  [Parameter]
  public RenderFragment ChildContent { get; set; }

   protected override void OnInitialized() {
      if (HttpContextAccessor.HttpContext != null)
         HttpContextAccessor.HttpContext.Response.StatusCode = 404;
   }
}

Then I changed my <NotFound> route to point at the component:

<Router AppAssembly="@typeof(ServerApp).Assembly">
  <Found Context="routeData">
    <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
    <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
  </Found>
  <NotFound>
    <PageNotFound>
      There's nothing on this page!
    </PageNotFound>
  </NotFound>
</Router>

The result is a hard 404 status code on pages which are "not found." As was said in the comments by the OP, this is important for SEO reasons. It allows us to tell search engines to stop indexing the page if they had previously done so, so that we are able to remove content from the site when needed (among other things).

Since the new component accepts children to be rendered, you can use it as a generic component elsewhere with custom "not found" messages. For example, you could have a @page "/images/{id?}" component, and if the image were deleted, render the <PageNotFound> with a message like The image #{id} has been deleted.

Coenocyte answered 21/11, 2023 at 14:0 Comment(3)
Maybe not a good idea... learn.microsoft.com/en-us/aspnet/core/fundamentals/…. We read in multiple places that IHttpContextAccessor just isn't reliable enough. <NotFound> without the status code was our solution. We had a post on GitHub where MS dev's told us that SPA's shouldn't set status codes because of their very nature that they exist within a context that has already been parsed and set to create the SPA.Melee
@Melee your vague and unspecific fear is unwarranted. If you read the caveats listed on that page and take the time to understand them, you'd see they are not relevant to the usage in question. Because this code is for a server-side Blazor page, and executed as part of the render lifecycle, it's identical to using the accessor within a normal Controller context (which is what the accessor was designed for). I have used this exact approach for years with no problems.Coenocyte
Also, if you refer to the modern docs (the docs you sent were for version 3.1 which isn't even supported any more), you'll note that the key difference here is that I'm statically rendering the components. Microsoft explicitly endorses using the HttpContext in statically rendered Blazor apps for this purpose, stating it can be used "only in statically-rendered root components for general tasks, such as inspecting and modifying headers or other properties in the App component"Coenocyte

© 2022 - 2024 — McMap. All rights reserved.