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

[Blazor] Stream response ends before components finish rendering #54157

Closed
1 task done
sbwalker opened this issue Feb 21, 2024 · 21 comments
Closed
1 task done

[Blazor] Stream response ends before components finish rendering #54157

sbwalker opened this issue Feb 21, 2024 · 21 comments
Labels
area-blazor Includes: Blazor, Razor Components investigate

Comments

@sbwalker
Copy link

sbwalker commented Feb 21, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

For the past 3 weeks I have been migrating a traditional Blazor application (https://github.com/oqtane/oqtane.framework) to the new Blazor approach in .NET 8. The process has not been smooth. The main problems are not related to the refactoring of the code - they are related to the run-time behavior of Blazor and the fact that it is a "black box".

More specifically, I am referring to scenarios where Blazor refuses to render a component... where it executes component logic such as StateHasChanged() and then does nothing - ie. it does not refresh the UI, it does not throw an exception, and it does not log anything in the browser console. Many days can be spent trying to track down these types of issues and in the end there is no resolution because it is not possible to identify the root cause (because the problem is occurring deep within the Blazor framework itself and there is no developer feedback loop).

Example # 1

An application which runs fine on global interactive rendering, attempts to run on static rendering. Within Visual Studio all of the logic is executing fine - no exceptions, no browser console messages - however the browser simply displays a blank page. If you view the source in the browser, you only see content from the App component - nothing else has been rendered. This resulted in 3-4 days of trying to identify the problem.

image

Example # 2

A stattically rendered component which indicates that a variable contains content within Visual Studio and should be rendered in the UI, however the UI is never updated with the content. This provoked another 3-4 days of investigation.

image

In both of these examples above (and a number of others) it turns out that there is a simple solution...

@attribute [StreamRendering]

Including this attribute within a component definition appears to be a "magic pill" to cure all sorts of strange, unexplainable rendering problems. Basically, rather than spending days trying to diagnose the Blazor "black box" I now sprinkle a component with some [StreamRendering] pixie dust, and it usually fixes the problem. From a developer productivity perspective alone, this attribute has already saved me from many days of spinning my wheels.

For Example # 1 including [StreamRendering] on the Routes component allowed the entire application to render as expected. For Example # 2 including [StreamRendering] on a custom Head component allowed page titles to be rendered as expected.

My concern is that most "magic pills" have side effects... they may solve the original problem but cause other issues in the process. In the case of [StreamRendering] there is very little documentation about what it is actually doing under the covers. Most of the information simply describes its purpose and benefits at a very high level. The fact that the Blazor Web template only includes it on the Weather component would seem to indicate that it should be used sparingly. So what are the implications from a performance, scalability, security perspective? Before I go ahead and sprinkle this attribute throughout my Blazor applications I would like to better understand the long term consequences.

Expected Behavior

Better guidance around the usage of [StreamRendering] in terms of when it should or should not be used, and the impacts of using it within an application.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

8.0

Anything else?

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Feb 21, 2024
@mkArtakMSFT
Copy link
Member

Thanks for sharing your concerns, @sbwalker.
Could you please share a minimal repro for us to investigate these further. Errors during development should be discoverable and if there is a case where they are not, we will try to improve it.

@sbwalker
Copy link
Author

sbwalker commented Feb 21, 2024

@mkArtakMSFT the following repo contains a minimal repro:

https://github.com/oqtane/OqtaneSSR

Run the app and the Home page displays:

image

The red square indicates where content is supposed to be rendered by the OqtaneSSR.Client\Components\Modules\Home.razor component

<p>This component demonstrates retrieving data using the ITextService</p>

@if (_text != null)
{
    <strong>@_text</strong>
}

Now modify the OqtaneSSR.Client\Components\Root\Routes.razor component and include the StreamRendering attribute:

@attribute [StreamRendering]

and the content will be displayed:

image

no changes to the app other than adding StreamRendering... the "magic pill".

@MackinnonBuck MackinnonBuck added this to the .NET 9 Planning milestone Feb 22, 2024
@sbwalker sbwalker changed the title .NET 8 Blazor - StreamRendering - Magic Pill or Major Headache? .NET 8 Blazor - StreamRendering - Magic Pill or Future Headache? Feb 23, 2024
@sbwalker
Copy link
Author

After further testing, the answer to the title of this issue is that StreamRendering is NOT a magic pill - it IS a Future Headache. When used in a root component of an app, StreamRendering produces a "flash" effect as it progressively pushes content to the browser which can result in a poor user experience.

@sbwalker
Copy link
Author

sbwalker commented Apr 1, 2024

@MackinnonBuck please let me know if you would like to discuss this issue further.

@MackinnonBuck
Copy link
Member

@sbwalker Do any of these problems go away after updating to the latest .NET 8 patch release? There were some servicing fixes related to stream rendering that may have resolved some of these (e.g., #52823).

@sbwalker
Copy link
Author

sbwalker commented Apr 2, 2024

@MackinnonBuck by "latest .NET 8 patch release" are you referring to 8.0.3?

@sbwalker
Copy link
Author

sbwalker commented Apr 3, 2024

@MackinnonBuck if you are referring to 8.0.3, Oqtane is already running on 8.0.3 and the problem still exists. To be clear, this issue is NOT about StreamRendering... it is that Blazor refuses to render content in SSR for some unexplainable reason, and the only way to force content to be rendered is by adding the StreamRendering attribute:

image

But by including StreamRendering on the Routes component it results in a UI "flash" for some pages, which is not desirable.

@MackinnonBuck
Copy link
Member

MackinnonBuck commented Apr 4, 2024

@sbwalker, the fix I was referring to was introduced in 8.0.2, so it seems like that bug isn't the cause of what you're seeing.

I briefly looked into the repro and on my machine, the "Hello StreamRendering" text displays even without stream rendering enabled. But I do see that the problematic code is called from an async void event handler, which stood out as a red flag. Blazor has no way of tracking async void invocations, so it doesn't know when the work there has completed. The renderer likely thinks that all components have finished rendering so it writes out the response without knowing that another task will later trigger a re-render.

It's possible that adding [StreamRendering] makes the text appear might be because there's an extra await call somewhere in Blazor's internals that happens to yield execution to code in the async void handler. But that would not indicate a bug in stream rendering.

Ultimately, I haven't been able to repro the exact behavior you're seeing so this is all a bit of a guess. I noticed that the repro uses configuration to decide render modes to use, so maybe there's a difference in local configuration that's resulting in the repro being different on my end. Could you provide a more minimal repro without all that machinery that demonstrates the issue you're seeing? Thanks.

Edit: It would be great if the repro also didn't use async void or unawaited tasks, so we could rule that out as being the issue

@sbwalker
Copy link
Author

sbwalker commented Apr 4, 2024

@MackinnonBuck I am not sure how the code in the https://github.com/oqtane/OqtaneSSR repo could work for you? I just updated the references to 8.0.3 and tried running it locally:

image

Then I uncomment the StreamRendering attribute in Routes.razor:

image

And run the application and it displays the content:

image

Your comment about the async void event handler is interesting. Specifically you are referring to this code in the custom SiteRouter:

    private async void LocationChanged(object sender, LocationChangedEventArgs args)
    {
        _absoluteUri = args.Location;
        await Refresh();
    }

This SiteRouter code was based on the default Blazor Router and that specific method in Oqtane has not changed since it is was introduced in 2018... but it could certainly still be a problem which has been exposed by the different run-time behavior in Static Blazor.

In the original default Blazor Router it was simply a void method - but in Oqtane it was modified to be async because the Refresh method contains HttpClient calls (all of the routing configuration in Oqtane is stored in a database which is loaded dynamically at runtime - note that Oqtane was originally an Interactive Blazor application and still supports this render mode).

In the latest Blazor Router the method has changed slightly to be "internal virtual" (with support for navigation interception):

internal virtual void Refresh(bool isNavigationIntercepted)

    internal virtual void Refresh(bool isNavigationIntercepted)
    {
        // If an `OnNavigateAsync` task is currently in progress, then wait
        // for it to complete before rendering. Note: because _previousOnNavigateTask
        // is initialized to a CompletedTask on initialization, this will still
        // allow first-render to complete successfully.

So I think what you are suggesting is to try to find a way to avoid the async void method. Unfortunately async Task does not work because the NavigationManager.LocationChanged -= LocationChanged event handler does not like it. Which I believe is why it ended being specified as async void in the first place. Do you have a suggestion on how to declare the LocationChanged event handler in such a way that it would permit asynchronous calls within the implementation? The reason why the default Blazor Router does not suffer from this is because it relies on reflection and purely static configuration... but as soon as you try to embrace a more dynamic approach to routing, the asynchronous requirement becomes very important.

I really appreciate your assistance.

@MackinnonBuck
Copy link
Member

@sbwalker,

I did a fresh clean of the repo, pulled the latest commit, confirmed that stream rendering was disabled in Routes.razor, and ran it, and got this:

image

Not sure what to say except the problem doesn't repro for me. The site uses static routing when I run it, so LocationChanged isn't even getting invoked, which makes me think there's some local configuration difference causing me not to see the problem.

Unfortunately async Task does not work because the NavigationManager.LocationChanged -= LocationChanged event handler does not like it

You can use NavigationManager.RegisterLocationChangingHandler() to register an async callback that blocks the navigation until the task completes. If you switch to use that, does the problem go away?

@sbwalker
Copy link
Author

sbwalker commented Apr 5, 2024

@MackinnonBuck I was very confused why this would not repro for you... and then I noticed that there was an appsettings.Development.json file in the repo - perhaps this config was being used when you were running the solution? I have removed the file so that it now uses the appsettings.json which contains the expected configuration. When running the app it should look like:

image

And you are correct that when running on Static Rendering (the default for the app) the LocationChanged() method is not even called because this is only relevant in Interactive Rendering. So I don't think this is the source of the problem.

However, I do think you are correct that the rendering problem is related to async calls. If you comment out the code in SiteRouter.razor in the Refresh method which is performing an HttpClient call (to simulate retrieving data from a backend API):

image

The content will be displayed as expected (even with StreamRendering commented out in Routes.razor). Note that the Refresh method is actually running synchronously in this scenario because the Refresh method no longer contains an await call.

image

Refresh is an async Task (not async void) and if f I now add an await Task.Yield() so that the method is asynchronous:

image

The content is no longer displayed:

image

But moving the await Task.Yield() to the end of the method:

image

Allows the content to be displayed once again:

image

(however in practice this would not work because in a real app the HttpClient service call is necessary to retrieve the values for setting the PageState object).

To add a bit more context, the Oqtane SSR application is a highly simplified POC to demonstrate the core concepts of the Oqtane Framework (https://github.com/oqtane/oqtane.framework). The Oqtane Framework supports ALL Blazor render modes. It is multi-tenant and supports multiple "sites" from the same installation. Each "site' specifies its preferred Render Mode and Interactivity:

OqtaneDevelopers20240306-1

This is all working... however for Static render mode it only works because there is a StreamRendering attribute set in Routes.razor. If the attribute is removed, a blank browser screen is rendered:

image

However if I View Page Source in the browser, there is content:

image

And in browser dev tools:

image

I demonstrated this to Dan Roth at the MVP Summit. I even tried disabling Enhanced Navigation in the App.razor and it does not resolve the problem either… no content is displayed unless I include the [StreamRendering] attribute.

So ultimately the purpose of the Oqtane SSR simplified POC repro project is to try to figure out why Blazor Static Server Rendering does not work - which will hopefully reveal the solution for the Oqtane Framework.

@MackinnonBuck
Copy link
Member

Thanks for the additional details, @sbwalker.

I cloned the latest version of your repro and ran it, and it still worked fine for me. Is the repro set up in a way where it can just be cloned and run without additional configuration? AFAICT, the conditions are set up correctly to reproduce the bug (stream rendering disabled, an async Refresh() method), but I still see the "Hello StreamRendering!!" text when visiting the site (and when navigating to another page and back, etc.).

I even tried it on a fresh machine... still worked.

Are you able to simulate a clean environment and run the project? If so, does the issue still happen after doing that?

What I did was:

@sbwalker
Copy link
Author

sbwalker commented Apr 5, 2024

@MackinnonBuck I can’t say I have had a “it doesn’t work on my machine” moment for a long time, as .NET generally creates repeatable solutions. I will install Windows Sandbox and give it a try. I will also request the Oqtane community try to run the solution in the repo to see if they experience issues or not. At the end of the day it is not the issue in this repo, it is the Oqtane Framework rendering problem which I want to resolve… so I am now wondering if the Oqtane Framework can be run in the Windows Sandbox so that the real problem can be reproduced (blank browser screen). I will let you know.

@sbwalker
Copy link
Author

sbwalker commented Apr 9, 2024

@MackinnonBuck the fact that you were not able to reproduce the problem prompted me to do some further investigation...

I started the Oqtane migration to use the new .NET 8 Blazor approach (ie. blazor.web.js) in January. I was still running the original .NET 8.0.0 SDK at that time... and it exhibited the problems outlined in the earlier posts in this thread (ie. rendering a blank screen unless I added StreamRendering). Other Oqtane developers also experienced the same behavior - which is why I logged the issue.

Later, the Oqtane solution was updated to newer versions of .NET 8 - ie. the project files were modified to reference 8.0.1, 8.0.2, 8.0.3. However, I just realized that I did not actually install the SDK for each of those versions on my local machine. So although I thought that I was running on the latest SDK, I actually was not. This is why I continued to see the rendering problems described in this issue.

After you mentioned that you could not reproduce the problem, I installed the 8.0.3 SDK and the problem no longer exists. So it appears that one of the SDK patch releases resolved the problem. However, I could not be certain which specific SDK patch release contained the fix - which made me a bit uncomfortable as it is not always possible to control the SDK version which is available in a run-time environment.

Case in point is Azure. I generally deploy Oqtane on Azure App Services, and the only configuration option you have is the ability to specify the major .NET version - so I have mine set to .NET 8. However, it is not clear what SDK version is installed so I used Kudu and ran dotnet --info. The results are:

C:\home>dotnet --info
.NET SDK:
Version: 8.0.101
Commit: 6eceda187b
Workload version: 8.0.100-manifests.69afb982

So it appears Azure is running 8.0.1 (2 versions behind the latest 8.0.3). This indicated that Oqtane may or may not function properly in that environment - as I was only able to confirm that it works on 8.0.3. So I created a new custom deployment and tested it out, and was able to confirm that it works fine on 8.0.1. So this means that whatever problem which was causing the rendering issue was fixed in the 8.0.1 SDK patch release.

Long story short... this issue can probably be closed, as the rendering problem is now resolved (the only remaining question which has not been addressed is if there is any official guidance which could be provided to developers in regards to when StreamRendering should be used - as without this guidance it will be used arbitrarily without understanding the consequences).

I very much appreciated your assistance in resolving this issue.

@MackinnonBuck
Copy link
Member

I'm glad you were able to find the cause of the issue, @sbwalker!

the only remaining question which has not been addressed is if there is any official guidance which could be provided to developers in regards to when StreamRendering should be used - as without this guidance it will be used arbitrarily without understanding the consequences

Is there something that our current guidance is lacking that you think could use additional clarification? I'm not aware of any scenarios where toggling stream rendering breaks the app in unintuitive ways (on the latest 8.0.3 SDK, that is).

@sbwalker
Copy link
Author

@MackinnonBuck the current guidance you linked to is quite thorough. The only scenario which is not mentioned is that enabling StreamRendering can cause unexpected "The renderer does not have a component with ID ##" in some scenarios where you are dynamically creating components. This seems to be caused by the differences in the initial content rendered in versus the final content which Blazor has a difficult time reconciling. I will need to create a simple repro for this and log it as a new issue.

@sbwalker
Copy link
Author

Spoke too soon... the Azure site where I deployed the bits with StreamRendering removed is still not rendering content in some scenarios:

image

So I am guessing that SDK 8.0.1 did not fix this issue... perhaps it was fixed in SDK 8.0.2 or 8.0.3. Now I need to figure out how to upgrade the SDK in my Azure App Service.

@sbwalker sbwalker reopened this Apr 10, 2024
@sbwalker
Copy link
Author

Upgraded my Azure App Service to .NET SDK 8.0.4 (using Development Tools / Extensions) and the rendering problem is resolved.

@garrettlondon1
Copy link

Thank you so much @sbwalker , I was struggling to figure out, all day, why StreamRendering was working locally, but not on App Service.

Added 8.0.4 to Extensions in App Service and now everything is working smoothly. Thanks for updating.

@MackinnonBuck
Copy link
Member

Glad to hear that the upgrade fixed it!

"The renderer does not have a component with ID ##" in some scenarios where you are dynamically creating components.

A new issue with a repro would be great if you'd be willing to provide one!

@MackinnonBuck MackinnonBuck changed the title .NET 8 Blazor - StreamRendering - Magic Pill or Future Headache? [Blazor] Stream response ends before components finish rendering Apr 15, 2024
@MackinnonBuck
Copy link
Member

Updated the title so that others experiencing a similar issue can find this one more easily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components investigate
Projects
None yet
Development

No branches or pull requests

4 participants