System.InvalidOperationException: Headers are read-only, response has already
started. on a statically rendered Blazor page means you set a cookie or another
header after the page had already begun sending HTML. With [StreamRendering]
that happens at the first await, so set the cookie before it.
Move Response.Cookies.Append above the first await in
OnInitializedAsync, or remove [StreamRendering] if the page does not
need to show a loading state.
The error
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_SetCookie(StringValues value)
at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value)
at FixLab.Components.Pages.Cookie.OnInitializedAsync() in ...\FixLab\Components\Pages\Cookie.razor:line 18
The path was shortened; nothing else changed. A few lines further the log adds
The response has already started, the error page middleware will not be executed.
The browser got a 200, a page stuck on "Loading..." and no cookie.
Why it happens
This was the page, a statically rendered component with no render mode:
@page "/cookie"
@attribute [StreamRendering]
<p>@status</p>
@code {
[CascadingParameter] public HttpContext? HttpContext { get; set; }
private string status = "Loading...";
protected override async Task OnInitializedAsync()
{
await Task.Delay(500);
HttpContext!.Response.Cookies.Append("theme", "dark");
status = "Saved.";
}
}
[StreamRendering] tells Blazor not to wait for slow initialisation. As soon as
OnInitializedAsync reaches an await that has not finished, Blazor
writes the page as it is now ("Loading...") to the response and keeps the connection open
to stream the updated markup later. Writing the first bytes of an HTTP response sends the
status line and every header with it, so from that moment the header collection is
read-only. A cookie is a Set-Cookie header, and Kestrel refuses to add it.
Anything that writes a header after that point fails the same way, a cookie sign-in
included.
The fix
Set the cookie before the first await:
protected override async Task OnInitializedAsync()
{
HttpContext!.Response.Cookies.Append("theme", "dark");
await Task.Delay(500);
status = "Saved.";
}
The same request now carries the cookie, and the streamed update arrives:
HTTP/1.1 200 OK
Set-Cookie: theme=dark; path=/
The second option is to drop [StreamRendering]. A copy of the failing page
without the attribute, cookie still after the await, returned 200 with
Set-Cookie: theme=dark and "Saved.", because without streaming Blazor waits
for OnInitializedAsync to finish before it writes anything. The price is that
the visitor sees nothing until the slow work is done, which is what streaming was for.
How it was reproduced
A Blazor Web App from dotnet new blazor -n FixLab -int Server on .NET SDK
10.0.401 with the ASP.NET Core 10.0.12 runtime. The page above was added without a render
mode, so it rendered statically, and Task.Delay(500) stood in for a slow
database call. Running dotnet run --urls http://localhost:15480 and requesting
/cookie with curl.exe -s -D - produced the log above and a
response without the cookie. Moving the line above the await produced the
Set-Cookie header and no errors.
Frequently asked
- Why do I get headers are read-only, response has already started in Blazor?
- The response had already started sending when your code tried to add a header such as a cookie. On a static page with StreamRendering, the first await in OnInitializedAsync sends the initial HTML and its headers, so set cookies before that await.
- How do I set a cookie in a Blazor static SSR page?
- Use the cascading HttpContext and call HttpContext.Response.Cookies.Append before anything is written to the response, which on a page with StreamRendering means before the first await in OnInitializedAsync.
- Does StreamRendering stop me from setting cookies in Blazor?
- Only after the first await. Streaming sends the initial render and the headers early, so cookies set before that point still go out. If the cookie depends on slow work, remove StreamRendering from that page.
More decoded errors in the Fixes category. For what runs when in a component, see The Component Lifecycle: What Runs When (and Why).