From f4ee6c150844ed73a0aa99e46f358664c8cb42d4 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:06:12 +0000 Subject: [PATCH 1/6] Update dependencies from https://github.com/dotnet/arcade build 20240916.2 (#57931) [release/9.0-rc2] Update dependencies from dotnet/arcade --- eng/Version.Details.xml | 24 ++++++++++++------------ eng/Versions.props | 8 ++++---- global.json | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index cd58caf6e98f..22516dfbaa82 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -388,31 +388,31 @@ https://github.com/dotnet/winforms b1fd89453ed5e3ad91e4f18c9386cac8dade6e36 - + https://github.com/dotnet/arcade - bcba6939456aa552554eae9ea9538a039cc98d96 + 04b9022eba9c184a8036328af513c22e6949e8b6 - + https://github.com/dotnet/arcade - bcba6939456aa552554eae9ea9538a039cc98d96 + 04b9022eba9c184a8036328af513c22e6949e8b6 - + https://github.com/dotnet/arcade - bcba6939456aa552554eae9ea9538a039cc98d96 + 04b9022eba9c184a8036328af513c22e6949e8b6 - + https://github.com/dotnet/arcade - bcba6939456aa552554eae9ea9538a039cc98d96 + 04b9022eba9c184a8036328af513c22e6949e8b6 - + https://github.com/dotnet/arcade - bcba6939456aa552554eae9ea9538a039cc98d96 + 04b9022eba9c184a8036328af513c22e6949e8b6 - + https://github.com/dotnet/arcade - bcba6939456aa552554eae9ea9538a039cc98d96 + 04b9022eba9c184a8036328af513c22e6949e8b6 https://github.com/dotnet/extensions diff --git a/eng/Versions.props b/eng/Versions.props index da0888cc291e..bf582fec104f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -165,10 +165,10 @@ 6.2.4 6.2.4 - 9.0.0-beta.24463.2 - 9.0.0-beta.24463.2 - 9.0.0-beta.24463.2 - 9.0.0-beta.24463.2 + 9.0.0-beta.24466.2 + 9.0.0-beta.24466.2 + 9.0.0-beta.24466.2 + 9.0.0-beta.24466.2 9.0.0-alpha.1.24452.1 diff --git a/global.json b/global.json index 44c47e0578f5..d4c978d226d2 100644 --- a/global.json +++ b/global.json @@ -27,7 +27,7 @@ "jdk": "11" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.24463.2", - "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.24463.2" + "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.24466.2", + "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.24466.2" } } From 74bdb8bd375944d951db0ecdc2e49e0a1cfc091e Mon Sep 17 00:00:00 2001 From: William Godbe Date: Thu, 19 Sep 2024 15:18:10 -0700 Subject: [PATCH 2/6] Add registry search for upgrade policy keys (#57952) * Add registry search for upgrade policy keys * SharedFx bundle too * Add util extension * Another fix --- src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs | 7 ++++++- .../SharedFrameworkBundle/SharedFrameworkBundle.wixproj | 5 +++++ src/Installers/Windows/WindowsHostingBundle/Bundle.wxs | 5 +++++ .../WindowsHostingBundle/WindowsHostingBundle.wixproj | 1 + src/Installers/Windows/Wix.targets | 2 +- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs b/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs index f0508bc3b611..52e85b9a96ce 100644 --- a/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs +++ b/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs @@ -1,5 +1,6 @@ - + + + + + diff --git a/src/Installers/Windows/SharedFrameworkBundle/SharedFrameworkBundle.wixproj b/src/Installers/Windows/SharedFrameworkBundle/SharedFrameworkBundle.wixproj index e7c93b5ecba8..5f47072f1fee 100644 --- a/src/Installers/Windows/SharedFrameworkBundle/SharedFrameworkBundle.wixproj +++ b/src/Installers/Windows/SharedFrameworkBundle/SharedFrameworkBundle.wixproj @@ -13,6 +13,10 @@ + + $(WixExtDir)\WixUtilExtension.dll + WixUtilExtension + $(WixExtDir)\WixDependencyExtension.dll WixDependencyExtension @@ -25,6 +29,7 @@ + diff --git a/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs b/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs index 429c66c241a3..6131a49404f4 100644 --- a/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs +++ b/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs @@ -13,6 +13,11 @@ + + + + + diff --git a/src/Installers/Windows/WindowsHostingBundle/WindowsHostingBundle.wixproj b/src/Installers/Windows/WindowsHostingBundle/WindowsHostingBundle.wixproj index cfb32de2dc5d..2e5c010ed0e7 100644 --- a/src/Installers/Windows/WindowsHostingBundle/WindowsHostingBundle.wixproj +++ b/src/Installers/Windows/WindowsHostingBundle/WindowsHostingBundle.wixproj @@ -36,6 +36,7 @@ + diff --git a/src/Installers/Windows/Wix.targets b/src/Installers/Windows/Wix.targets index 4fa41e244bd9..7b0a4577492b 100644 --- a/src/Installers/Windows/Wix.targets +++ b/src/Installers/Windows/Wix.targets @@ -110,7 +110,7 @@ NoLogo="true" Cultures="en-us" InstallerFile="%(WixInstallerFilesToProcess.Identity)" - AdditionalBasePaths="$(MSBuildProjectDirectory)" + AdditionalBasePaths="$(MSBuildProjectDirectory);$(PkgMicrosoft_DotNet_Build_Tasks_Installers)\build\wix\bundle" WixExtensions="@(WixExtension)" Loc="@(EmbeddedResource)" Sice="$(SuppressIces)" From a717e7fd80e639f4603c126a8e772e541d58a43e Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Fri, 20 Sep 2024 11:59:15 -0700 Subject: [PATCH 3/6] Check for sentinel value when setting HTTP/3 error code (#57976) If no error code has been set, `IProtocolErrorFeature.Error` will be `-1`. If we pass that through verbatim, it will be caught by validation in the setter (ironically, of the same property on the same feature object), resulting in an exception and a Critical (but apparently benign) log message. Fixes #57933 --- .../src/Internal/Http3/Http3Connection.cs | 11 ++- .../shared/test/Http3/Http3InMemory.cs | 2 +- .../Http3/Http3ConnectionTests.cs | 78 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index e0f300839104..92777ea1d79c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -101,6 +101,9 @@ private void UpdateHighestOpenedRequestStreamId(long streamId) public string ConnectionId => _context.ConnectionId; public ITimeoutControl TimeoutControl => _context.TimeoutControl; + // The default error value is -1. If it hasn't been changed before abort is called then default to HTTP/3's NoError value. + private Http3ErrorCode Http3ErrorCodeOrNoError => _errorCodeFeature.Error == -1 ? Http3ErrorCode.NoError : (Http3ErrorCode)_errorCodeFeature.Error; + public void StopProcessingNextRequest(ConnectionEndReason reason) => StopProcessingNextRequest(serverInitiated: true, reason); @@ -505,12 +508,14 @@ public async Task ProcessRequestsAsync(IHttpApplication appl } } + var errorCode = Http3ErrorCodeOrNoError; + // Abort active request streams. lock (_streams) { foreach (var stream in _streams.Values) { - stream.Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error); + stream.Abort(CreateConnectionAbortError(error, clientAbort), errorCode); } } @@ -546,7 +551,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl } // Complete - Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error, reason); + Abort(CreateConnectionAbortError(error, clientAbort), errorCode, reason); // Wait for active requests to complete. while (_activeRequestCount > 0) @@ -905,7 +910,7 @@ public void OnInputOrOutputCompleted() TryStopAcceptingStreams(); // Abort the connection using the error code the client used. For a graceful close, this should be H3_NO_ERROR. - Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), (Http3ErrorCode)_errorCodeFeature.Error, ConnectionEndReason.TransportCompleted); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), Http3ErrorCodeOrNoError, ConnectionEndReason.TransportCompleted); } internal WebTransportSession OpenNewWebTransportSession(Http3Stream http3Stream) diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 730e80862de8..64ebcdb07b41 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -225,7 +225,7 @@ public void TriggerTick(TimeSpan timeSpan = default) public async Task InitializeConnectionAsync(RequestDelegate application) { - MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this) + MultiplexedConnectionContext ??= new TestMultiplexedConnectionContext(this) { ConnectionId = "TEST" }; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 4471e7db42f3..06d96adc238f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -701,6 +701,84 @@ await Http3Api.InitializeConnectionAsync(async c => await tcs.Task; } + [Fact] + public async Task ErrorCodeIsValidOnConnectionTimeout() + { + // This test loosely repros the scenario in https://github.com/dotnet/aspnetcore/issues/57933. + // In particular, there's a request from the server and, once a response has been sent, + // the (simulated) transport throws a QuicException that surfaces through AcceptAsync. + // This test confirms that Http3Connection.ProcessRequestsAsync doesn't (indirectly) cause + // IProtocolErrorCodeFeature.Error to be set to (or left at) -1, which System.Net.Quic will + // not accept. + + // Used to signal that a request has been sent and a response has been received + var requestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // Used to signal that the connection context has been aborted + var abortTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // InitializeConnectionAsync consumes the connection context, so set it first + Http3Api.MultiplexedConnectionContext = new ThrowingMultiplexedConnectionContext(Http3Api, skipCount: 2, requestTcs, abortTcs); + await Http3Api.InitializeConnectionAsync(_echoApplication); + + await Http3Api.CreateControlStream(); + await Http3Api.GetInboundControlStream(); + var requestStream = await Http3Api.CreateRequestStream(Headers, endStream: true); + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + await requestStream.ExpectReceiveEndOfStream(); + await requestStream.OnDisposedTask.DefaultTimeout(); + + requestTcs.SetResult(); + + // By the time the connection context is aborted, the error code feature has been updated + await abortTcs.Task.DefaultTimeout(); + + Http3Api.CloseServerGracefully(); + + var errorCodeFeature = Http3Api.MultiplexedConnectionContext.Features.Get(); + Assert.InRange(errorCodeFeature.Error, 0, (1L << 62) - 1); // Valid range for HTTP/3 error codes + } + + private sealed class ThrowingMultiplexedConnectionContext : TestMultiplexedConnectionContext + { + private int _skipCount; + private readonly TaskCompletionSource _requestTcs; + private readonly TaskCompletionSource _abortTcs; + + /// + /// After calls to , the next call will throw a + /// (after waiting for to be set). + /// + /// lets this type signal that has been called. + /// + public ThrowingMultiplexedConnectionContext(Http3InMemory testBase, int skipCount, TaskCompletionSource requestTcs, TaskCompletionSource abortTcs) + : base(testBase) + { + _skipCount = skipCount; + _requestTcs = requestTcs; + _abortTcs = abortTcs; + } + + public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + if (_skipCount-- <= 0) + { + await _requestTcs.Task.DefaultTimeout(); + throw new System.Net.Quic.QuicException( + System.Net.Quic.QuicError.ConnectionTimeout, + applicationErrorCode: null, + "Connection timed out waiting for a response from the peer."); + } + return await base.AcceptAsync(cancellationToken); + } + + public override void Abort(ConnectionAbortedException abortReason) + { + _abortTcs.SetResult(); + base.Abort(abortReason); + } + } + private async Task MakeRequestAsync(int index, KeyValuePair[] headers, bool sendData, bool waitForServerDispose) { var requestStream = await Http3Api.CreateRequestStream(headers, endStream: !sendData); From 8371724b2f349b5817558c005e831d0d00ca66ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:14:26 -0700 Subject: [PATCH 4/6] [release/9.0-rc2] [Blazor] Update `WebAssembly.DevServer` to serve the `Blazor-Environment` header (#57974) * Serve 'Blazor-Environment' header * PR feedback * Update src/Components/WebAssembly/DevServer/src/Server/Startup.cs --------- Co-authored-by: Mackinnon Buck --- .../DevServer/src/Server/Startup.cs | 30 ++++++++++--------- .../Tests/WebAssemblyConfigurationTest.cs | 6 ++++ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Components/WebAssembly/DevServer/src/Server/Startup.cs b/src/Components/WebAssembly/DevServer/src/Server/Startup.cs index 342370171656..046031a29f79 100644 --- a/src/Components/WebAssembly/DevServer/src/Server/Startup.cs +++ b/src/Components/WebAssembly/DevServer/src/Server/Startup.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -29,27 +30,28 @@ public static void Configure(IApplicationBuilder app, IConfiguration configurati app.UseWebAssemblyDebugging(); - bool applyCopHeaders = configuration.GetValue("ApplyCopHeaders"); + var webHostEnvironment = app.ApplicationServices.GetRequiredService(); + var applyCopHeaders = configuration.GetValue("ApplyCopHeaders"); - if (applyCopHeaders) + app.Use(async (ctx, next) => { - app.Use(async (ctx, next) => + if (ctx.Request.Path.StartsWithSegments("/_framework/blazor.boot.json")) { - if (ctx.Request.Path.StartsWithSegments("/_framework") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.server.js") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.web.js")) + ctx.Response.Headers.Append("Blazor-Environment", webHostEnvironment.EnvironmentName); + } + else if (applyCopHeaders && ctx.Request.Path.StartsWithSegments("/_framework") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.server.js") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.web.js")) + { + var fileExtension = Path.GetExtension(ctx.Request.Path); + if (string.Equals(fileExtension, ".js", StringComparison.OrdinalIgnoreCase)) { - string fileExtension = Path.GetExtension(ctx.Request.Path); - if (string.Equals(fileExtension, ".js")) - { - // Browser multi-threaded runtime requires cross-origin policy headers to enable SharedArrayBuffer. - ApplyCrossOriginPolicyHeaders(ctx); - } + // Browser multi-threaded runtime requires cross-origin policy headers to enable SharedArrayBuffer. + ApplyCrossOriginPolicyHeaders(ctx); } + } - await next(ctx); - }); - } + await next(ctx); + }); - //app.UseBlazorFrameworkFiles(); app.UseRouting(); app.UseStaticFiles(new StaticFileOptions diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs index aee9648a4109..b231545db2aa 100644 --- a/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs +++ b/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs @@ -39,6 +39,9 @@ public void WebAssemblyConfiguration_Works() if (_serverFixture.TestTrimmedOrMultithreadingApps) { + // Verify that the environment gets detected as 'Production'. + Browser.Equal("Production", () => _appElement.FindElement(By.Id("environment")).Text); + // Verify values overriden by an environment specific 'appsettings.$(Environment).json are read Assert.Equal("Prod key2-value", _appElement.FindElement(By.Id("key2")).Text); @@ -47,6 +50,9 @@ public void WebAssemblyConfiguration_Works() } else { + // Verify that the dev server always correctly serves the 'Blazor-Environment: Development' header. + Browser.Equal("Development", () => _appElement.FindElement(By.Id("environment")).Text); + // Verify values overriden by an environment specific 'appsettings.$(Environment).json are read Assert.Equal("Development key2-value", _appElement.FindElement(By.Id("key2")).Text); From 92376ce36468ed605a66f09cfe02d4c436ea3a2b Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 20 Sep 2024 16:42:17 -0700 Subject: [PATCH 5/6] Fix IAsyncEnumerable controller methods to allow setting headers (#57924) * Fix IAsyncEnumerable controller methods to allow setting headers * name * httpjson extensions too * revert --- .../src/HttpResponseJsonExtensions.cs | 102 ++++-------------- .../test/HttpResponseJsonExtensionsTests.cs | 70 +++++++++++- ...ft.AspNetCore.Http.Extensions.Tests.csproj | 1 + .../SystemTextJsonOutputFormatter.cs | 5 - .../SystemTextJsonResultExecutor.cs | 6 -- .../SystemTextJsonOutputFormatterTest.cs | 17 +++ ...SystemTextJsonOutputFormatterController.cs | 8 ++ 7 files changed, 117 insertions(+), 92 deletions(-) diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index c7d003e6bb0b..84e09c1a3581 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -90,22 +90,12 @@ public static Task WriteAsJsonAsync( response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset; - var startTask = Task.CompletedTask; - if (!response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - startTask = response.StartAsync(cancellationToken); - } - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled) + if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, options, - ignoreOCE: !cancellationToken.CanBeCanceled, - cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted); + return WriteAsJsonAsyncSlow(response.BodyWriter, value, options, response.HttpContext.RequestAborted); } - startTask.GetAwaiter().GetResult(); return JsonSerializer.SerializeAsync(response.BodyWriter, value, options, cancellationToken); } @@ -131,33 +121,22 @@ public static Task WriteAsJsonAsync( response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset; - var startTask = Task.CompletedTask; - if (!response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - startTask = response.StartAsync(cancellationToken); - } - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled) + if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(startTask, response, value, jsonTypeInfo, - ignoreOCE: !cancellationToken.CanBeCanceled, - cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted); + return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo, response.HttpContext.RequestAborted); } - startTask.GetAwaiter().GetResult(); return JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken); - static async Task WriteAsJsonAsyncSlow(Task startTask, HttpResponse response, TValue value, JsonTypeInfo jsonTypeInfo, - bool ignoreOCE, CancellationToken cancellationToken) + static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, JsonTypeInfo jsonTypeInfo, + CancellationToken cancellationToken) { try { - await startTask; await JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken); } - catch (OperationCanceledException) when (ignoreOCE) { } + catch (OperationCanceledException) { } } } @@ -184,52 +163,38 @@ public static Task WriteAsJsonAsync( response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset; - var startTask = Task.CompletedTask; - if (!response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - startTask = response.StartAsync(cancellationToken); - } - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled) + if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(startTask, response, value, jsonTypeInfo, - ignoreOCE: !cancellationToken.CanBeCanceled, - cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted); + return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo, response.HttpContext.RequestAborted); } - startTask.GetAwaiter().GetResult(); return JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken); - static async Task WriteAsJsonAsyncSlow(Task startTask, HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo, - bool ignoreOCE, CancellationToken cancellationToken) + static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo, + CancellationToken cancellationToken) { try { - await startTask; await JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken); } - catch (OperationCanceledException) when (ignoreOCE) { } + catch (OperationCanceledException) { } } } [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(RequiresDynamicCodeMessage)] private static async Task WriteAsJsonAsyncSlow( - Task startTask, PipeWriter body, TValue value, JsonSerializerOptions? options, - bool ignoreOCE, CancellationToken cancellationToken) { try { - await startTask; await JsonSerializer.SerializeAsync(body, value, options, cancellationToken); } - catch (OperationCanceledException) when (ignoreOCE) { } + catch (OperationCanceledException) { } } /// @@ -304,42 +269,30 @@ public static Task WriteAsJsonAsync( response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset; - var startTask = Task.CompletedTask; - if (!response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - startTask = response.StartAsync(cancellationToken); - } - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled) + if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, type, options, - ignoreOCE: !cancellationToken.CanBeCanceled, - cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted); + return WriteAsJsonAsyncSlow(response.BodyWriter, value, type, options, + response.HttpContext.RequestAborted); } - startTask.GetAwaiter().GetResult(); return JsonSerializer.SerializeAsync(response.BodyWriter, value, type, options, cancellationToken); } [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] [RequiresDynamicCode(RequiresDynamicCodeMessage)] private static async Task WriteAsJsonAsyncSlow( - Task startTask, PipeWriter body, object? value, Type type, JsonSerializerOptions? options, - bool ignoreOCE, CancellationToken cancellationToken) { try { - await startTask; await JsonSerializer.SerializeAsync(body, value, type, options, cancellationToken); } - catch (OperationCanceledException) when (ignoreOCE) { } + catch (OperationCanceledException) { } } /// @@ -367,33 +320,22 @@ public static Task WriteAsJsonAsync( response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset; - var startTask = Task.CompletedTask; - if (!response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - startTask = response.StartAsync(cancellationToken); - } - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled) + if (!cancellationToken.CanBeCanceled) { - return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, type, context, - ignoreOCE: !cancellationToken.CanBeCanceled, - cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted); + return WriteAsJsonAsyncSlow(response.BodyWriter, value, type, context, response.HttpContext.RequestAborted); } - startTask.GetAwaiter().GetResult(); return JsonSerializer.SerializeAsync(response.BodyWriter, value, type, context, cancellationToken); - static async Task WriteAsJsonAsyncSlow(Task startTask, PipeWriter body, object? value, Type type, JsonSerializerContext context, - bool ignoreOCE, CancellationToken cancellationToken) + static async Task WriteAsJsonAsyncSlow(PipeWriter body, object? value, Type type, JsonSerializerContext context, + CancellationToken cancellationToken) { try { - await startTask; await JsonSerializer.SerializeAsync(body, value, type, context, cancellationToken); } - catch (OperationCanceledException) when (ignoreOCE) { } + catch (OperationCanceledException) { } } } diff --git a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs index 35cfa265d7f1..3d5007e73d26 100644 --- a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs @@ -1,12 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO.Pipelines; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; #nullable enable @@ -481,6 +484,71 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse() Assert.Equal("null", data); } + // Regression test: https://github.com/dotnet/aspnetcore/issues/57895 + [Fact] + public async Task AsyncEnumerableCanSetHeader() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + await using var app = builder.Build(); + + app.MapGet("/", IAsyncEnumerable (HttpContext httpContext) => + { + return AsyncEnum(); + + async IAsyncEnumerable AsyncEnum() + { + await Task.Yield(); + httpContext.Response.Headers["Test"] = "t"; + yield return 1; + } + }); + + await app.StartAsync(); + + var client = app.GetTestClient(); + + var result = await client.GetAsync("/"); + result.EnsureSuccessStatusCode(); + var headerValue = Assert.Single(result.Headers.GetValues("Test")); + Assert.Equal("t", headerValue); + + await app.StopAsync(); + } + + // Regression test: https://github.com/dotnet/aspnetcore/issues/57895 + [Fact] + public async Task EnumerableCanSetHeader() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + await using var app = builder.Build(); + + app.MapGet("/", IEnumerable (HttpContext httpContext) => + { + return Enum(); + + IEnumerable Enum() + { + httpContext.Response.Headers["Test"] = "t"; + yield return 1; + } + }); + + await app.StartAsync(); + + var client = app.GetTestClient(); + + var result = await client.GetAsync("/"); + result.EnsureSuccessStatusCode(); + var headerValue = Assert.Single(result.Headers.GetValues("Test")); + Assert.Equal("t", headerValue); + + await app.StopAsync(); + } + public class TestObject { public string? StringProperty { get; set; } diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 686ab34dd28a..4a35778afa55 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index c51ca745d8e7..f4e82f6857f7 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -88,11 +88,6 @@ public sealed override async Task WriteResponseBodyAsync(OutputFormatterWriteCon try { var responseWriter = httpContext.Response.BodyWriter; - if (!httpContext.Response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - await httpContext.Response.StartAsync(); - } if (jsonTypeInfo is not null) { diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs index cfce28c8dc64..167d4f71bec0 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs @@ -66,12 +66,6 @@ public async Task ExecuteAsync(ActionContext context, JsonResult result) try { var responseWriter = response.BodyWriter; - if (!response.HasStarted) - { - // Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush. - await response.StartAsync(); - } - await JsonSerializer.SerializeAsync(responseWriter, value, objectType, jsonSerializerOptions, context.HttpContext.RequestAborted); } catch (OperationCanceledException) when (context.HttpContext.RequestAborted.IsCancellationRequested) { } diff --git a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs index df54ab0d8cd9..d4906ab320f6 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs @@ -65,4 +65,21 @@ public async Task Formatting_PolymorphicModel_WithJsonPolymorphism() await response.AssertStatusCodeAsync(HttpStatusCode.OK); Assert.Equal(expected, await response.Content.ReadAsStringAsync()); } + + // Regression test: https://github.com/dotnet/aspnetcore/issues/57895 + [Fact] + public async Task CanSetHeaderWithAsyncEnumerable() + { + // Arrange + var expected = "[1]"; + + // Act + var response = await Client.GetAsync($"/SystemTextJsonOutputFormatter/{nameof(SystemTextJsonOutputFormatterController.AsyncEnumerable)}"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + var headerValue = Assert.Single(response.Headers.GetValues("Test")); + Assert.Equal("t", headerValue); + } } diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/SystemTextJsonOutputFormatterController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/SystemTextJsonOutputFormatterController.cs index 287ffa90fd91..dcbd10cb1171 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/SystemTextJsonOutputFormatterController.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/SystemTextJsonOutputFormatterController.cs @@ -19,6 +19,14 @@ public class SystemTextJsonOutputFormatterController : ControllerBase Address = "Some address", }; + [HttpGet] + public async IAsyncEnumerable AsyncEnumerable() + { + await Task.Yield(); + HttpContext.Response.Headers["Test"] = "t"; + yield return 1; + } + [JsonPolymorphic] [JsonDerivedType(typeof(DerivedModel), nameof(DerivedModel))] public class SimpleModel From b77c79e89d9aec0055f6c1851040cc4425a20563 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 20 Sep 2024 16:43:03 -0700 Subject: [PATCH 6/6] Add partitioned to cookie for SignalR browser testing (#57997) * Add partitioned to cookie for SignalR browser testing Looks like Chromium (not chrome) headless now requires 'partitioned' on the cookie when using `Secure` and `Same-Site=None` * Apply suggestions from code review Co-authored-by: Andrew Casey --------- Co-authored-by: Andrew Casey --- src/SignalR/clients/ts/FunctionalTests/Startup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs index 45d3f8546a71..dd3ee9378458 100644 --- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs @@ -184,9 +184,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger< { cookieOptions.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; cookieOptions.Secure = true; + cookieOptions.Extensions.Add("partitioned"); // Required by Chromium expiredCookieOptions.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None; expiredCookieOptions.Secure = true; + expiredCookieOptions.Extensions.Add("partitioned"); // Required by Chromium } context.Response.Cookies.Append("testCookie", "testValue", cookieOptions); context.Response.Cookies.Append("testCookie2", "testValue2", cookieOptions);