diff --git a/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs b/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs index b1c113beca88..8a0a00df13d1 100644 --- a/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs +++ b/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs @@ -10,10 +10,12 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Forms; internal class EndpointAntiforgeryStateProvider(IAntiforgery antiforgery) : DefaultAntiforgeryStateProvider() { private HttpContext? _context; + private bool _canGenerateToken; internal void SetRequestContext(HttpContext context) { _context = context; + _canGenerateToken = true; } public override AntiforgeryRequestToken? GetAntiforgeryToken() @@ -24,17 +26,23 @@ internal void SetRequestContext(HttpContext context) return _currentToken; } - // We already have a callback setup to generate the token when the response starts if needed. - // If we need the tokens before we start streaming the response, we'll generate and store them; - // otherwise we'll just retrieve them. - // In case there are no tokens available, we are going to return null and no-op. - var tokens = !_context.Response.HasStarted ? antiforgery.GetAndStoreTokens(_context) : antiforgery.GetTokens(_context); - if (tokens.RequestToken is null) + if (_currentToken == null && _canGenerateToken) { - return null; + // We already have a callback setup to generate the token when the response starts if needed. + // If we need the tokens before we start streaming the response, we'll generate and store them; + // otherwise we'll just retrieve them. + // In case there are no tokens available, we are going to return null and no-op. + var tokens = !_context.Response.HasStarted ? antiforgery.GetAndStoreTokens(_context) : antiforgery.GetTokens(_context); + if (tokens.RequestToken is null) + { + return null; + } + + _currentToken = new AntiforgeryRequestToken(tokens.RequestToken, tokens.FormFieldName); } - _currentToken = new AntiforgeryRequestToken(tokens.RequestToken, tokens.FormFieldName); return _currentToken; } + + internal void DisableTokenGeneration() => _canGenerateToken = false; } diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 8e5338f54788..6e9bb59ab45a 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -6,7 +6,9 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Components.Endpoints.Forms; using Microsoft.AspNetCore.Components.Endpoints.Rendering; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; @@ -76,14 +78,6 @@ private async Task RenderComponentCore(HttpContext context) return; } - context.Response.OnStarting(() => - { - // Generate the antiforgery tokens before we start streaming the response, as it needs - // to set the cookie header. - antiforgery!.GetAndStoreTokens(context); - return Task.CompletedTask; - }); - if (httpActivityContext != default) { _activityLinkStore.SetActivityContext(ComponentsActivityLinkStore.Http, httpActivityContext, null); @@ -143,8 +137,17 @@ await _renderer.InitializeStandardComponentServicesAsync( var bufferingFeature = context.Features.GetRequiredFeature(); bufferingFeature.DisableBuffering(); + // Store the tokens if not emitted already in case we stream a form in the response. + antiforgery!.GetAndStoreTokens(context); + context.Response.Headers.ContentEncoding = "identity"; } + else if (endpoint.Metadata.GetMetadata()?.ConfiguredRenderModes.Length == 0) + { + // Disable token generation on EndpointAntiforgeryStateProvider if we are not streaming. + var provider = (EndpointAntiforgeryStateProvider)context.RequestServices.GetRequiredService(); + provider.DisableTokenGeneration(); + } // Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context) // in between the first call to htmlContent.WriteTo and the point where we start listening for subsequent