简体   繁体   中英

Session.SetString() in Server Side .NET Core Produces error "The session cannot be established after the response has started"

I have a server size Blazor app that needs to write something into a session when page is first loaded, and then read it from a session in that page and other pages.

Right now, that Session.String() is called in OnInitializedAsync() . However, I'm getting an exception "The session cannot be established after the response has started". From scant documentation that I found, this usually happens when SignalR is used with the app.

1) I don't think I'm using SignalR, unless it's configured by default to be used in server-side .net core code (In which case, how do I find out?) 2) I also tried putting the call in OnInitialized() and onAfterRender() (synchronous methods), which didn't help. 3) I think my HTTPContextAccessor and ISession are configured correctly, because I'm able to use Session.GetString() anytime, including right before the call to Session.SetString(). 4) I cannot switch to a client-size Blazor app for a variety of reasons. 5) I'm using app.UseEndpoints(), so app.useMvc() is commented out because they cannot be used at the same time.

Does anyone have any idea off the top of their head what could be wrong before I paste very large chunks of code here? The snippets of what I have so far are below

//Startup.cs
public IHttpContextAccessor HtppContextAccessor { get; }
// ...

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();

    services.AddHttpContextAccessor();

    //services.AddMvc(); //TODO: DO I NEED IT?
    services.AddDistributedMemoryCache();  //TODO : DO I NEED IT? // Adds a default in-memory implementation of IDistributedCache
    services.AddSession();

    //services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();


}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSession();


    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();


    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });

}

//**************************************************************************
//myfile.razor

protected override async Task OnInitializedAsync()
{
    var sampleValue = Session.GetString("testName1"); //this call is ok
    Session.SetString("testName1", "testValue2"); //this is where exception occurs
}

Thank you

Answer

I think this Session.SetString() error is a bug, since Session.GetString() works just fine even after the response has started, but Session.SetString() doesn't. Regardless, the workaround (or "hack" if you will) includes making a throwaway call to Session.SetString() to "prime" the session for future writing. Just find a line of code in your application where you KNOW the response hasn't sent, and insert a throwaway call to Session.SetString() there. Then you will be able to make subsequent calls to Session.SetString() with no error, including ones after the response has started, inside your OnInitializedAsync() method. You can check if the response is started by checking the property HttpContext.Response.HasStarted .

Try adding this app.Use() snippet into your Startup.cs Configure() method:

...
...
app.UseHttpsRedirection();
app.UseStaticFiles();

//begin SetString() hack
app.Use(async delegate (HttpContext Context, Func<Task> Next)
{
    //this throwaway session variable will "prime" the SetString() method
    //to allow it to be called after the response has started
    var TempKey = Guid.NewGuid().ToString(); //create a random key
    Context.Session.Set(TempKey, Array.Empty<byte>()); //set the throwaway session variable
    Context.Session.Remove(TempKey); //remove the throwaway session variable
    await Next(); //continue on with the request
});
//end SetString() hack

app.UseRouting();


app.UseEndpoints(endpoints =>
{
    endpoints.MapBlazorHub();
    endpoints.MapFallbackToPage("/_Host");
});
...
...

Background Info

The info I can share here is not Blazor specific, but will help you pinpoint what's happening in your setup, as I've come across the same error myself.

From scant documentation that I found, this usually happens when SignalR is used with the app.

I know you said "usually happens" but just to clarify, this error can still occur without SignalR being installed; it is not exclusive to SignalR. The reason SignalR is mentioned with this error, is because SignalR uses WebSockets. A WebSocket can easily trigger the error since WebSocket logic most often occurs within a WebSocket send/receive (ping/pong) loop, where the "response" is perpetually being "sent". Also, doesn't Blazor use WebSockets as well?

Anyway, the error occurs when BOTH of the following criteria are met simultaneously:

Criteria 1. A request is sent to the server with no session cookie, or the included session cookie is invalid/expired.

Criteria 2. The request in Criteria 1 makes a call to Session.SetString() after the response has started. In other words, if the property HttpContext.Response.HasStarted is true , and Session.SetString() is called, the exception will be thrown.

Important: If Criteria 1 is not met, then calling Session.SetString() after the response has started will NOT cause the error.

...needs to write something into a session when page is first loaded...

That is why the error only seems to happen upon first load of a page--it's because often in first loads, there is no session cookie that the server can use (or the one that was provided is invalid or too old), and the server has to spin up a new session data store (I don't know why it has to spin up a new one for SetString(), that's why I say I think this is a bug). If the server has to spin up a new session data store, it does so upon the first call to Session.SetString() , and new session data stores cannot be spun up after the response has started. On the other hand, if the session cookie provided was a valid one, then no new data store needs to be spun up, and thus you can call Session.SetString() anytime you want, including after the response has started.

I'm willing to bet that in your case, by the time OnInitializedAsync() is called, the property HttpContext.Response.HasStarted is true , which is why the error is being thrown. What you need to do, is make a preliminary call to Session.SetString() before the response gets started, so that the session data store gets spun up, and then your call to Session.SetString() inside the OnInitializedAsync() method won't cause the error.

That is probably because Asp.Net Core will not write any cookies (including the session cookie) to the response unless the end-user gives their consent according to EU General Data Protection Regulation (GDPR) which is supported in ASP.NET Core and defaults to get the consent from the user.

https://docs.microsoft.com/en-us/aspnet/core/security/gdpr?view=aspnetcore-2.2

You can try to set the Session Cookie as essential for your application to overcome this:

(From the documentation:)

Session state cookies are not essential. Session state isn't functional when tracking is disabled. The following code makes session cookies essential:

services.AddSession(options =>
{
    options.Cookie.IsEssential = true;
});

Better yet, you can follow the guidance in the same article to have user consent feature.

The other question you asked;

services.AddDistributedMemoryCache();  //TODO : DO I NEED IT? // Adds a default in-memory implementation of IDistributedCache
services.AddSession();

Yes; You need to set a Distributed cache, which is documented here:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-3.1

To enable the session middleware, Startup must contain:

Any of the IDistributedCache memory caches. The IDistributedCache implementation is used as a backing store for session. For more information, see Distributed caching in ASP.NET Core.

A call to AddSession in ConfigureServices.

A call to UseSession in Configure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM