dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.57k stars 10.05k forks source link

Support using Blazor pages with status code pages middleware #51203

Open danroth27 opened 1 year ago

danroth27 commented 1 year ago

If you try to use Blazor pages with the status code pages middleware you get unexpected behavior.

Repro steps:

@page "/StatusCode/{status:int}"

<PageTitle>@Status</PageTitle>

<h1>@Status</h1>

@code {
    [Parameter]
    public int Status { get; set; }
}

Expected result: Blazor status page renders for the 404 response. Actual result: Blazor status page shows up briefly and then is replaced with a plain text "Not found" page.

We special case re-executed requests from the error handling middleware. We might need to do this for the status code page middleware as well.

Related issue for handling not found requests: #45654

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 11 months ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

likquietlyy commented 9 months ago

Page with plain text "Not Found" is the default implementation of the Router's NotFound razor component. That means that <NotFound>...</NotFound> component is used to provide custom content when content isn't found at Blazor Web App with global interactive server rendering. But documentation tells that this section only applies to Blazor WebAssembly apps. Is this a documentation error or we should not use the NotFound component in the Blazor Server App?

getWombats commented 9 months ago

Page with plain text "Not Found" is the default implementation of the Router's NotFound razor component. That means that <NotFound>...</NotFound> component is used to provide custom content when content isn't found at Blazor Web App with global interactive server rendering. But documentation tells that this section only applies to Blazor WebAssembly apps. Is this a documentation error or we should not use the NotFound component in the Blazor Server App?

I have just encountered the same problem. Trying to get some kind of workaround but adding app.UseStatusCodePagesWithRedirects("/somePageToRedirect"); to Program.cs isnt really what i am looking for. Any other suggestions?

janrhansen commented 8 months ago

I see two possible workarounds:

1) add @rendermode InteractiveServer to each and every page, instead of applying it globally in (which I guess is what we mean by "Create a Blazor Web App with global interactive server rendering") like <Routes @rendermode="InteractiveServer" /> I don't know if this approach will prevent something else from working. It is cumbersome

2) Add it globally <Routes @rendermode="InteractiveServer" /> and then in <Router...> under <NotFound> (which is now suddenly relevant again) handle the NotFound-situation directly or by including a component

<Router AppAssembly="typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
                <Authorizing>
                    Authorizing...
                </Authorizing>
                <NotAuthorized>
                    <RedirectToLogin></RedirectToLogin>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="typeof(Layout.MainLayout)">
                <p>Route was not found</p>
            </LayoutView>
        </NotFound>
    </Router>

You would then think that the app.UseStatusCodePagesWithReExecute("/StatusCode/{0}"); was no longer needed, since that page is now never invoked (I only had it to catch the 404) - but NO. If you remove that line, the NotFound approach doesn't work.

janrhansen commented 8 months ago

@mkArtakMSFT How can this not be a bug? Is it intentional that the <NotFound> part of <Router> is active/ignored depending on whether @rendermode="InteractiveServer" is set on the parent level or not? It feels like a bug, and unless this is intentional behaviour, I would ask that this issue is reclassified as a bug again, not an enhancement.

purplepiranha commented 6 months ago

@janrhansen @mkArtakMSFT I agree that this must be a bug!

The documentation states that 'Blazor Web Apps don't use the NotFound parameter...', however, when setting app.UseStatusCodePagesWithReExecute("/error/{0}");, it gets enabled. This means that my error route gets displayed momentarily and then 'Not found' is displayed instead.

Trying to get a genuine 404 page to render with Blazor is a total nightmare, and the documented approach of using UseStatusCodePagesWithRedirects is not user friendly and definitely not SEO friendly.

Whilst, there is never going to be a way to get a 404 status when working interactively, it is imperative that we can generate a proper status code response from the initial server render and that we can do this without a change in URL. A 302 redirect when it should be a 404 is simply unacceptable.

Take my scenario, a blog. The URL structure is simple: https://mydomain.xyz/blog/{categoryUrlStub} https://mydomain.xyz/blog/{categoryUrlStub}/{postUrlStub}

In order to get a 404 during the server render, I have some middleware that checks to see if the category and post exists. If not, it sets the status code on the response to 404. The problem then is that it still matches a route (I haven't found a way to stop this), so once the error page is rendered it then re-renders the matching route. By ensuring that a similar check is made within my page model, the client then renders the same.

Now, say I have a situation where I need an '410 - Gone' status page, or would like to use '418 - I'm a Tea Pot' status for some reason... I'm now in the situation where the server does it's job and I can see the page (briefly), but the Router still decides to carry on and as it can't find that route, and it generates a 404 page but the status has been reported as 410 or 418. This is less of a problem, because I can catch these and they'll be re-rendered, but it means that every-time something like this is done it's run the code on the server, and then re-run the code on the server (as I'm using InteractiveServer).

My feeling is that once the status code has been set to an error response, then the Router shouldn't be dealing with the route. It should be handed off to the error page(s) to then render and deal with any interaction.

DM-98 commented 1 month ago

Hello @danroth27

A fix for this (specifically 404 not found) is by adding it to Routes.razor like this;

@using BlazorApp21.Components.Layout
@using BlazorApp21.Components.Pages

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>

    <NotFound>
        <MainLayout>
            <Body>
                <StatusCode Status="404" />
            </Body>
        </MainLayout>
    </NotFound>
</Router>

Another option which might be even more robust when it comes to all status codes, is by Excluding it from interactivity: StatusCode.razor:

@page "/StatusCode/{status:int}"

@attribute [ExcludeFromInteractiveRouting]

<PageTitle>@Status</PageTitle>

<h1>@Status</h1>

@code {
    [Parameter]
    public int Status { get; set; }
}

App.razor:

...
<HeadOutlet @rendermode="PageRenderMode" />
...
<Routes @rendermode="PageRenderMode" />
...
@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}