Parcourir la source

[automated] Merge branch 'release/7.0' => 'main' (#43830)

dotnet-maestro-bot il y a 3 ans
Parent
commit
32014fb8b3
37 fichiers modifiés avec 593 ajouts et 135 suppressions
  1. 2 1
      eng/Workarounds.targets
  2. 1 0
      eng/targets/Helix.targets
  3. 3 1
      eng/targets/ResolveReferences.targets
  4. 1 1
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/GrpcServerLog.cs
  5. 1 3
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
  6. 3 2
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingServerCallContext.cs
  7. 0 26
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Protos/errors.proto
  8. 1 4
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj
  9. 7 1
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs
  10. 1 1
      src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj
  11. 50 2
      src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ServerStreamingServerCallHandlerTests.cs
  12. 71 12
      src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
  13. 3 1
      src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/GrpcSwaggerServiceExtensionsTests.cs
  14. 2 2
      src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/XmlComments/XmlDocumentationIntegrationTests.cs
  15. 6 1
      src/Http/Http.Extensions/src/RequestDelegateFactory.cs
  16. 26 0
      src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
  17. 10 4
      src/Http/Routing/src/CompositeEndpointDataSource.cs
  18. 35 1
      src/Http/Routing/test/UnitTests/CompositeEndpointDataSourceTest.cs
  19. 2 1
      src/Identity/ApiAuthorization.IdentityServer/src/Microsoft.AspNetCore.ApiAuthorization.IdentityServer.csproj
  20. 1 0
      src/Security/Authentication/Negotiate/src/Microsoft.AspNetCore.Authentication.Negotiate.csproj
  21. 8 0
      src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
  22. 39 26
      src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs
  23. 3 0
      src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs
  24. 126 4
      src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs
  25. 8 4
      src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs
  26. 16 2
      src/Servers/Kestrel/shared/test/HttpParsingData.cs
  27. 1 0
      src/Shared/BrowserTesting/src/Microsoft.AspNetCore.BrowserTesting.csproj
  28. 5 0
      src/SignalR/common/Shared/ClientResultsManager.cs
  29. 9 12
      src/SignalR/server/Specification.Tests/src/ScaleoutHubLifetimeManagerTests.cs
  30. 7 12
      src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs
  31. 21 6
      src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs
  32. 2 2
      src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommand.cs
  33. 107 0
      src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs
  34. 5 1
      src/Tools/GetDocumentInsider/src/GetDocument.Insider.csproj
  35. 8 1
      src/Tools/dotnet-getdocument/src/Commands/InvokeCommand.cs
  36. 1 1
      src/Tools/dotnet-getdocument/src/dotnet-getdocument.csproj
  37. 1 0
      src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj

+ 2 - 1
eng/Workarounds.targets

@@ -49,7 +49,8 @@
   </Target>
 
   <!-- Work around https://github.com/dotnet/aspnetcore/issues/34048 -->
-  <Target Name="_RemoveDuplicateLoggingSourceGenerator" AfterTargets="ResolvePackageAssets" Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
+  <Target Name="_RemoveDuplicateLoggingSourceGenerator" AfterTargets="ResolvePackageAssets" Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' AND
+      ('$(IsAspNetCoreApp)' == 'true' OR '$(UseAspNetCoreSharedRuntime)' == 'true')">
     <ItemGroup>
       <ResolvedAnalyzers Remove="@(ResolvedAnalyzers)" Condition="'%(ResolvedAnalyzers.NuGetPackageId)' == 'Microsoft.Extensions.Logging.Abstractions'" />
       <ResolvedAnalyzers Remove="@(ResolvedAnalyzers)" Condition="'%(ResolvedAnalyzers.NuGetPackageId)' == 'System.Text.Json'" />

+ 1 - 0
eng/targets/Helix.targets

@@ -11,6 +11,7 @@
   <PropertyGroup>
     <_TestingArchitecture>x64</_TestingArchitecture>
     <_TestingArchitecture Condition=" '$(IsArm64HelixQueue)' == 'true' ">arm64</_TestingArchitecture>
+    <TestDependsOnPlaywright Condition="'$(TestDependsOnPlaywright)' == ''">false</TestDependsOnPlaywright>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(TestDependsOnPlaywright)' == 'true'">

+ 3 - 1
eng/targets/ResolveReferences.targets

@@ -253,7 +253,9 @@
   -->
   <ItemGroup Condition=" '$(MSBuildProjectName)' != 'Microsoft.AspNetCore.App.Runtime' AND
       '$(MSBuildProjectName)' != 'RepoTasks' AND
-      ($(_CompileTfmUsingReferenceAssemblies) OR '$(MSBuildProjectName)' == 'Microsoft.AspNetCore.App.Ref') ">
+      ('$(MSBuildProjectName)' == 'Microsoft.AspNetCore.App.Ref' OR
+      (('$(IsAspNetCoreApp)' == 'true' OR '$(UseAspNetCoreSharedRuntime)' == 'true') AND
+      $(_CompileTfmUsingReferenceAssemblies))) ">
     <PackageReference Include="Microsoft.Internal.Runtime.AspNetCore.Transport"
         Version="$(MicrosoftInternalRuntimeAspNetCoreTransportVersion)"
         IsImplicitlyDefined="true"

+ 1 - 1
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/GrpcServerLog.cs

@@ -15,7 +15,7 @@ internal static partial class GrpcServerLog
     public static partial void ErrorExecutingServiceMethod(ILogger logger, string serviceMethod, Exception ex);
 
     [LoggerMessage(3, LogLevel.Information, "Error status code '{StatusCode}' with detail '{Detail}' raised.", EventName = "RpcConnectionError")]
-    public static partial void RpcConnectionError(ILogger logger, StatusCode statusCode, string detail);
+    public static partial void RpcConnectionError(ILogger logger, StatusCode statusCode, string detail, Exception? debugException);
 
     [LoggerMessage(4, LogLevel.Debug, "Reading message.", EventName = "ReadingMessage")]
     public static partial void ReadingMessage(ILogger logger);

+ 1 - 3
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs

@@ -10,7 +10,6 @@ using Google.Api;
 using Google.Protobuf;
 using Google.Protobuf.Reflection;
 using Grpc.Core;
-using Grpc.Gateway.Runtime;
 using Grpc.Shared;
 using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
 using Microsoft.AspNetCore.Http;
@@ -96,9 +95,8 @@ internal static class JsonRequestHelpers
             response.ContentType = MediaType.ReplaceEncoding("application/json", encoding);
         }
 
-        var e = new Error
+        var e = new Google.Rpc.Status
         {
-            Error_ = status.Detail,
             Message = status.Detail,
             Code = (int)status.StatusCode
         };

+ 3 - 2
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingServerCallContext.cs

@@ -104,8 +104,9 @@ internal sealed class JsonTranscodingServerCallContext : ServerCallContext, ISer
         if (ex is RpcException rpcException)
         {
             // RpcException is thrown by client code to modify the status returned from the server.
-            // Log the status and detail. Don't log the exception to reduce log verbosity.
-            GrpcServerLog.RpcConnectionError(Logger, rpcException.StatusCode, rpcException.Status.Detail);
+            // Log the status, detail and debug exception (if present).
+            // Don't log the RpcException itself to reduce log verbosity. All of its information is already captured.
+            GrpcServerLog.RpcConnectionError(Logger, rpcException.StatusCode, rpcException.Status.Detail, rpcException.Status.DebugException);
 
             status = rpcException.Status;
         }

+ 0 - 26
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Protos/errors.proto

@@ -1,26 +0,0 @@
-syntax = "proto3";
-package grpc.gateway.runtime;
-option go_package = "internal";
-
-import "google/protobuf/any.proto";
-
-// Error is the generic error returned from unary RPCs.
-message Error {
-	string error = 1;
-	// This is to make the error more compatible with users that expect errors to be Status objects:
-	// https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto
-	// It should be the exact same message as the Error field.
-	int32 code = 2;
-	string message = 3;
-	repeated google.protobuf.Any details = 4;
-}
-
-// StreamError is a response type which is returned when
-// streaming rpc returns an error.
-message StreamError {
-	int32 grpc_code = 1;
-	int32 http_code = 2;
-	string message = 3;
-	string http_status = 4;
-	repeated google.protobuf.Any details = 5;
-}

+ 1 - 4
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <Description>HTTP API for gRPC ASP.NET Core</Description>
     <PackageTags>gRPC RPC HTTP/2 REST</PackageTags>
@@ -27,11 +27,8 @@
     <Compile Include="..\Shared\HttpRoutePatternParser.cs" Link="Internal\Shared\HttpRoutePatternParser.cs" />
     <Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Internal\Shared" />
 
-    <Protobuf Include="Internal\Protos\errors.proto" Access="Internal" />
-
     <Reference Include="Google.Api.CommonProtos" />
     <Reference Include="Google.Protobuf" />
     <Reference Include="Grpc.AspNetCore.Server" />
-    <Reference Include="Grpc.Tools" PrivateAssets="All" />
   </ItemGroup>
 </Project>

+ 7 - 1
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs

@@ -65,7 +65,7 @@ internal sealed class GrpcJsonTranscodingDescriptionProvider : IApiDescriptionPr
             {
                 // Swagger uses this to group endpoints together.
                 // Group methods together using the service name.
-                ["controller"] = methodDescriptor.Service.FullName
+                ["controller"] = methodDescriptor.Service.Name
             },
             EndpointMetadata = routeEndpoint.Metadata.ToList()
         };
@@ -77,6 +77,12 @@ internal sealed class GrpcJsonTranscodingDescriptionProvider : IApiDescriptionPr
             ModelMetadata = new GrpcModelMetadata(ModelMetadataIdentity.ForType(methodDescriptor.OutputType.ClrType)),
             StatusCode = 200
         });
+        apiDescription.SupportedResponseTypes.Add(new ApiResponseType
+        {
+            ApiResponseFormats = { new ApiResponseFormat { MediaType = "application/json" } },
+            ModelMetadata = new GrpcModelMetadata(ModelMetadataIdentity.ForType(typeof(Google.Rpc.Status))),
+            IsDefaultResponse = true
+        });
         var explorerSettings = routeEndpoint.Metadata.GetMetadata<ApiExplorerSettingsAttribute>();
         if (explorerSettings != null)
         {

+ 1 - 1
src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <Description>Swagger for gRPC ASP.NET Core</Description>
     <PackageTags>gRPC RPC HTTP/2 REST Swagger OpenAPI</PackageTags>

+ 50 - 2
src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/ServerStreamingServerCallHandlerTests.cs

@@ -106,9 +106,54 @@ public class ServerStreamingServerCallHandlerTests : LoggedTest
         var line2 = await ReadLineAsync(pipe.Reader).DefaultTimeout();
         using var responseJson2 = JsonDocument.Parse(line2!);
         Assert.Equal("Exception was thrown by handler.", responseJson2.RootElement.GetProperty("message").GetString());
-        Assert.Equal("Exception was thrown by handler.", responseJson2.RootElement.GetProperty("error").GetString());
         Assert.Equal(2, responseJson2.RootElement.GetProperty("code").GetInt32());
 
+        var exceptionWrite = TestSink.Writes.Single(w => w.EventId.Name == "ErrorExecutingServiceMethod");
+        Assert.Equal("Error when executing service method 'TestMethodName'.", exceptionWrite.Message);
+        Assert.Equal("Exception!", exceptionWrite.Exception.Message);
+
+        await callTask.DefaultTimeout();
+    }
+
+    [Fact]
+    public async Task HandleCallAsync_MessageThenRpcException_MessageThenErrorReturned()
+    {
+        // Arrange
+        var debugException = new Exception("Error!");
+        ServerStreamingServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = async (s, r, w, c) =>
+        {
+            await w.WriteAsync(new HelloReply { Message = $"Hello {r.Name} 1" });
+            throw new RpcException(new Status(StatusCode.Aborted, "Detail!", debugException));
+        };
+
+        var pipe = new Pipe();
+
+        var routeParameterDescriptors = new Dictionary<string, List<FieldDescriptor>>
+        {
+            ["name"] = new List<FieldDescriptor>(new[] { HelloRequest.Descriptor.FindFieldByNumber(HelloRequest.NameFieldNumber) })
+        };
+        var descriptorInfo = TestHelpers.CreateDescriptorInfo(routeParameterDescriptors: routeParameterDescriptors);
+        var callHandler = CreateCallHandler(invoker, descriptorInfo: descriptorInfo);
+        var httpContext = TestHelpers.CreateHttpContext(bodyStream: pipe.Writer.AsStream());
+        httpContext.Request.RouteValues["name"] = "TestName!";
+
+        // Act
+        var callTask = callHandler.HandleCallAsync(httpContext);
+
+        // Assert
+        var line1 = await ReadLineAsync(pipe.Reader).DefaultTimeout();
+        using var responseJson1 = JsonDocument.Parse(line1!);
+        Assert.Equal("Hello TestName! 1", responseJson1.RootElement.GetProperty("message").GetString());
+
+        var line2 = await ReadLineAsync(pipe.Reader).DefaultTimeout();
+        using var responseJson2 = JsonDocument.Parse(line2!);
+        Assert.Equal("Detail!", responseJson2.RootElement.GetProperty("message").GetString());
+        Assert.Equal((int)StatusCode.Aborted, responseJson2.RootElement.GetProperty("code").GetInt32());
+
+        var exceptionWrite = TestSink.Writes.Single(w => w.EventId.Name == "RpcConnectionError");
+        Assert.Equal("Error status code 'Aborted' with detail 'Detail!' raised.", exceptionWrite.Message);
+        Assert.Equal(debugException, exceptionWrite.Exception);
+
         await callTask.DefaultTimeout();
     }
 
@@ -140,9 +185,12 @@ public class ServerStreamingServerCallHandlerTests : LoggedTest
         var line = await ReadLineAsync(pipe.Reader).DefaultTimeout();
         using var responseJson = JsonDocument.Parse(line!);
         Assert.Equal("Exception was thrown by handler. Exception: Exception!", responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal("Exception was thrown by handler. Exception: Exception!", responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal(2, responseJson.RootElement.GetProperty("code").GetInt32());
 
+        var exceptionWrite = TestSink.Writes.Single(w => w.EventId.Name == "ErrorExecutingServiceMethod");
+        Assert.Equal("Error when executing service method 'TestMethodName'.", exceptionWrite.Message);
+        Assert.Equal("Exception!", exceptionWrite.Exception.Message);
+
         await callTask.DefaultTimeout();
     }
 

+ 71 - 12
src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs

@@ -445,7 +445,6 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal(expectedError, responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal(expectedError, responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.InvalidArgument, responseJson.RootElement.GetProperty("code").GetInt32());
     }
 
@@ -484,7 +483,6 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal(expectedError, responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal(expectedError, responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.InvalidArgument, responseJson.RootElement.GetProperty("code").GetInt32());
     }
 
@@ -515,7 +513,6 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal(expectedError, responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal(expectedError, responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.InvalidArgument, responseJson.RootElement.GetProperty("code").GetInt32());
     }
 
@@ -540,7 +537,6 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal("Detail!", responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal("Detail!", responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.Unauthenticated, responseJson.RootElement.GetProperty("code").GetInt32());
     }
 
@@ -548,9 +544,10 @@ public class UnaryServerCallHandlerTests : LoggedTest
     public async Task HandleCallAsync_RpcExceptionThrown_StatusReturned()
     {
         // Arrange
+        var debugException = new Exception("Error!");
         UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
         {
-            throw new RpcException(new Status(StatusCode.Unauthenticated, "Detail!"), "Message!");
+            throw new RpcException(new Status(StatusCode.Unauthenticated, "Detail!", debugException), "Message!");
         };
 
         var unaryServerCallHandler = CreateCallHandler(invoker);
@@ -565,8 +562,69 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal("Detail!", responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal("Detail!", responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.Unauthenticated, responseJson.RootElement.GetProperty("code").GetInt32());
+
+        var exceptionWrite = TestSink.Writes.Single(w => w.EventId.Name == "RpcConnectionError");
+        Assert.Equal("Error status code 'Unauthenticated' with detail 'Detail!' raised.", exceptionWrite.Message);
+        Assert.Equal(debugException, exceptionWrite.Exception);
+    }
+
+    [Fact]
+    public async Task HandleCallAsync_OtherExceptionThrown_StatusReturned()
+    {
+        // Arrange
+        UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
+        {
+            throw new InvalidOperationException("Error!");
+        };
+
+        var unaryServerCallHandler = CreateCallHandler(invoker);
+        var httpContext = TestHelpers.CreateHttpContext();
+
+        // Act
+        await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+        // Assert
+        Assert.Equal(500, httpContext.Response.StatusCode);
+
+        httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
+        using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
+        Assert.Equal("Exception was thrown by handler.", responseJson.RootElement.GetProperty("message").GetString());
+        Assert.Equal((int)StatusCode.Unknown, responseJson.RootElement.GetProperty("code").GetInt32());
+
+        var exceptionWrite = TestSink.Writes.Single(w => w.EventId.Name == "ErrorExecutingServiceMethod");
+        Assert.Equal("Error when executing service method 'TestMethodName'.", exceptionWrite.Message);
+        Assert.Equal("Error!", exceptionWrite.Exception.Message);
+    }
+
+    [Fact]
+    public async Task HandleCallAsync_EnableDetailedErrors_OtherExceptionThrown_StatusReturned()
+    {
+        // Arrange
+        UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
+        {
+            throw new InvalidOperationException("Error!");
+        };
+
+        var unaryServerCallHandler = CreateCallHandler(
+            invoker,
+            serviceOptions: new GrpcServiceOptions { EnableDetailedErrors = true });
+        var httpContext = TestHelpers.CreateHttpContext();
+
+        // Act
+        await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+        // Assert
+        Assert.Equal(500, httpContext.Response.StatusCode);
+
+        httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
+        using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
+        Assert.Equal("Exception was thrown by handler. InvalidOperationException: Error!", responseJson.RootElement.GetProperty("message").GetString());
+        Assert.Equal((int)StatusCode.Unknown, responseJson.RootElement.GetProperty("code").GetInt32());
+
+        var exceptionWrite = TestSink.Writes.Single(w => w.EventId.Name == "ErrorExecutingServiceMethod");
+        Assert.Equal("Error when executing service method 'TestMethodName'.", exceptionWrite.Message);
+        Assert.Equal("Error!", exceptionWrite.Exception.Message);
     }
 
     [Fact]
@@ -591,7 +649,6 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal(@"Detail!", responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal(@"Detail!", responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.Unauthenticated, responseJson.RootElement.GetProperty("code").GetInt32());
     }
 
@@ -1000,7 +1057,6 @@ public class UnaryServerCallHandlerTests : LoggedTest
         httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
         using var responseJson = JsonDocument.Parse(httpContext.Response.Body);
         Assert.Equal("Exception was thrown by handler.", responseJson.RootElement.GetProperty("message").GetString());
-        Assert.Equal("Exception was thrown by handler.", responseJson.RootElement.GetProperty("error").GetString());
         Assert.Equal((int)StatusCode.Unknown, responseJson.RootElement.GetProperty("code").GetInt32());
     }
 
@@ -1271,14 +1327,16 @@ public class UnaryServerCallHandlerTests : LoggedTest
         UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker,
         CallHandlerDescriptorInfo? descriptorInfo = null,
         List<(Type Type, object[] Args)>? interceptors = null,
-        GrpcJsonTranscodingOptions? jsonTranscodingOptions = null)
+        GrpcJsonTranscodingOptions? jsonTranscodingOptions = null,
+        GrpcServiceOptions? serviceOptions = null)
     {
         return CreateCallHandler(
             invoker,
             CreateServiceMethod("TestMethodName", HelloRequest.Parser, HelloReply.Parser),
             descriptorInfo,
             interceptors,
-            jsonTranscodingOptions);
+            jsonTranscodingOptions,
+            serviceOptions);
     }
 
     private UnaryServerCallHandler<JsonTranscodingGreeterService, TRequest, TResponse> CreateCallHandler<TRequest, TResponse>(
@@ -1286,11 +1344,12 @@ public class UnaryServerCallHandlerTests : LoggedTest
         Method<TRequest, TResponse> method,
         CallHandlerDescriptorInfo? descriptorInfo = null,
         List<(Type Type, object[] Args)>? interceptors = null,
-        GrpcJsonTranscodingOptions? jsonTranscodingOptions = null)
+        GrpcJsonTranscodingOptions? jsonTranscodingOptions = null,
+        GrpcServiceOptions? serviceOptions = null)
         where TRequest : class, IMessage<TRequest>
         where TResponse : class, IMessage<TResponse>
     {
-        var serviceOptions = new GrpcServiceOptions();
+        serviceOptions ??= new GrpcServiceOptions();
         if (interceptors != null)
         {
             foreach (var interceptor in interceptors)

+ 3 - 1
src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/GrpcSwaggerServiceExtensionsTests.cs

@@ -45,7 +45,9 @@ public class GrpcSwaggerServiceExtensionsTests
         Assert.Single(swagger.Paths);
 
         var path = swagger.Paths["/v1/greeter/{name}"];
-        Assert.True(path.Operations.ContainsKey(OperationType.Get));
+        Assert.True(path.Operations.TryGetValue(OperationType.Get, out var operation));
+        Assert.Equal("Success", operation.Responses["200"].Description);
+        Assert.Equal("Error", operation.Responses["default"].Description);
     }
 
     [Fact]

+ 2 - 2
src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/XmlComments/XmlDocumentationIntegrationTests.cs

@@ -30,7 +30,7 @@ public class XmlDocumentationIntegrationTests
         var swagger = GetOpenApiDocument<XmlDocServiceWithComments>();
 
         // Assert
-        Assert.Equal("xmldoc.XmlDoc", swagger.Tags[0].Name);
+        Assert.Equal("XmlDoc", swagger.Tags[0].Name);
         Assert.Equal("XmlDocServiceWithComments XML comment!", swagger.Tags[0].Description);
     }
 
@@ -41,7 +41,7 @@ public class XmlDocumentationIntegrationTests
         var swagger = GetOpenApiDocument<XmlDocService>();
 
         // Assert
-        Assert.Equal("xmldoc.XmlDoc", swagger.Tags[0].Name);
+        Assert.Equal("XmlDoc", swagger.Tags[0].Name);
         Assert.Equal("XmlDoc!", swagger.Tags[0].Description);
     }
 

+ 6 - 1
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -1005,7 +1005,7 @@ public static partial class RequestDelegateFactory
             else
             {
                 // TODO: Handle custom awaitables
-                throw new NotSupportedException($"Unsupported return type: {returnType}");
+                throw new NotSupportedException($"Unsupported return type: {TypeNameHelper.GetTypeDisplayName(returnType)}");
             }
         }
         else if (typeof(IResult).IsAssignableFrom(returnType))
@@ -1021,6 +1021,11 @@ public static partial class RequestDelegateFactory
         {
             return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall);
         }
+        else if (returnType.IsByRefLike)
+        {
+            // Unsupported
+            throw new NotSupportedException($"Unsupported return type: {TypeNameHelper.GetTypeDisplayName(returnType)}");
+        }
         else if (returnType.IsValueType)
         {
             var box = Expression.TypeAs(methodCall, typeof(object));

+ 26 - 0
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

@@ -2182,6 +2182,32 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(default, structToBeZeroed);
     }
 
+    [Fact]
+    public void RequestDelegateFactoryThrowsForByRefReturnTypes()
+    {
+        ReadOnlySpan<byte> Method1() => "hello world"u8;
+        Span<byte> Method2() => "hello world"u8.ToArray();
+        RefStruct Method3() => new("hello world"u8);
+
+        var ex1 = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(Method1));
+        var ex2 = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(Method2));
+        var ex3 = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(Method3));
+
+        Assert.Equal("Unsupported return type: System.ReadOnlySpan<byte>", ex1.Message);
+        Assert.Equal("Unsupported return type: System.Span<byte>", ex2.Message);
+        Assert.Equal($"Unsupported return type: {typeof(RefStruct).FullName}", ex3.Message);
+    }
+
+    ref struct RefStruct
+    {
+        public ReadOnlySpan<byte> Buffer { get; }
+
+        public RefStruct(ReadOnlySpan<byte> buffer)
+        {
+            Buffer = buffer;
+        }
+    }
+
     [Theory]
     [InlineData(true)]
     [InlineData(false)]

+ 10 - 4
src/Http/Routing/src/CompositeEndpointDataSource.cs

@@ -242,8 +242,7 @@ public sealed class CompositeEndpointDataSource : EndpointDataSource, IDisposabl
     [MemberNotNull(nameof(_consumerChangeToken))]
     private void CreateChangeTokenUnsynchronized(bool collectionChanged)
     {
-        _cts = new CancellationTokenSource();
-        _consumerChangeToken = new CancellationChangeToken(_cts.Token);
+        var cts = new CancellationTokenSource();
 
         if (collectionChanged)
         {
@@ -255,17 +254,24 @@ public sealed class CompositeEndpointDataSource : EndpointDataSource, IDisposabl
                     () => HandleChange(collectionChanged: false)));
             }
         }
+
+        _cts = cts;
+        _consumerChangeToken = new CancellationChangeToken(cts.Token);
     }
 
     [MemberNotNull(nameof(_endpoints))]
     private void CreateEndpointsUnsynchronized()
     {
-        _endpoints = new List<Endpoint>();
+        var endpoints = new List<Endpoint>();
 
         foreach (var dataSource in _dataSources)
         {
-            _endpoints.AddRange(dataSource.Endpoints);
+            endpoints.AddRange(dataSource.Endpoints);
         }
+
+        // Only cache _endpoints after everything succeeds without throwing.
+        // We don't want to create a negative cache which would cause 404s when there should be 500s.
+        _endpoints = endpoints;
     }
 
     // Use private variable '_endpoints' to avoid initialization

+ 35 - 1
src/Http/Routing/test/UnitTests/CompositeEndpointDataSourceTest.cs

@@ -2,7 +2,6 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.ObjectModel;
-using System.Linq;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Routing.Patterns;
@@ -58,6 +57,28 @@ public class CompositeEndpointDataSourceTest
         Assert.Equal(groupedEndpoints, resolvedGroupEndpoints);
     }
 
+    [Fact]
+    public void RepeatedlyThrows_WhenChildDataSourcesThrow()
+    {
+        var ex = new Exception();
+        var compositeDataSource = new CompositeEndpointDataSource(new[]
+        {
+            new EndpointThrowingDataSource(ex),
+        });
+        var groupContext = new RouteGroupContext
+        {
+            Prefix = RoutePatternFactory.Parse(""),
+            Conventions = Array.Empty<Action<EndpointBuilder>>(),
+            FinallyConventions = Array.Empty<Action<EndpointBuilder>>(),
+            ApplicationServices = new ServiceCollection().BuildServiceProvider(),
+        };
+
+        Assert.Same(ex, Assert.Throws<Exception>(() => compositeDataSource.Endpoints));
+        Assert.Same(ex, Assert.Throws<Exception>(() => compositeDataSource.Endpoints));
+        Assert.Same(ex, Assert.Throws<Exception>(() => compositeDataSource.GetGroupedEndpoints(groupContext)));
+        Assert.Same(ex, Assert.Throws<Exception>(() => compositeDataSource.GetGroupedEndpoints(groupContext)));
+    }
+
     [Fact]
     public void Endpoints_ReturnsAllEndpoints_FromMultipleDataSources()
     {
@@ -502,4 +523,17 @@ public class CompositeEndpointDataSourceTest
 
         public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
     }
+
+    private class EndpointThrowingDataSource : EndpointDataSource
+    {
+        private readonly Exception _ex;
+
+        public EndpointThrowingDataSource(Exception ex)
+        {
+            _ex = ex;
+        }
+
+        public override IReadOnlyList<Endpoint> Endpoints => throw _ex;
+        public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;
+    }
 }

+ 2 - 1
src/Identity/ApiAuthorization.IdentityServer/src/Microsoft.AspNetCore.ApiAuthorization.IdentityServer.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -29,6 +29,7 @@
     <Reference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
     <Reference Include="Microsoft.AspNetCore.Mvc" />
     <Reference Include="Microsoft.Extensions.Http" />
+    <Reference Include="Microsoft.Extensions.Configuration.Binder" />
   </ItemGroup>
 
 </Project>

+ 1 - 0
src/Security/Authentication/Negotiate/src/Microsoft.AspNetCore.Authentication.Negotiate.csproj

@@ -13,6 +13,7 @@
     <Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
     <Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
     <Reference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" />
+    <Reference Include="Microsoft.Extensions.Caching.Memory" />
     <Reference Include="System.DirectoryServices.Protocols" />
   </ItemGroup>
 

+ 8 - 0
src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs

@@ -139,6 +139,14 @@ public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TR
         // Consume space
         offset++;
 
+        while ((uint)offset < (uint)requestLine.Length
+            && requestLine[offset] == ByteSpace)
+        {
+            // It's invalid to have multiple spaces between the url resource and version
+            // but some clients do it. Skip them.
+            offset++;
+        }
+
         // Version + CR is 9 bytes which should take us to .Length
         // LF should have been dropped prior to method call
         if ((uint)offset + 9 != (uint)requestLine.Length || requestLine[offset + 8] != ByteCR)

+ 39 - 26
src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs

@@ -75,10 +75,7 @@ internal sealed class QuicConnectionListener : IMultiplexedConnectionListener, I
                 var serverAuthenticationOptions = await _tlsConnectionCallbackOptions.OnConnection(context, cancellationToken);
 
                 // If the callback didn't set protocols then use the listener's list of protocols.
-                if (serverAuthenticationOptions.ApplicationProtocols == null)
-                {
-                    serverAuthenticationOptions.ApplicationProtocols = _tlsConnectionCallbackOptions.ApplicationProtocols;
-                }
+                serverAuthenticationOptions.ApplicationProtocols ??= _tlsConnectionCallbackOptions.ApplicationProtocols;
 
                 // If the SslServerAuthenticationOptions doesn't have a cert or protocols then the
                 // QUIC connection will fail and the client receives an unhelpful message.
@@ -145,38 +142,54 @@ internal sealed class QuicConnectionListener : IMultiplexedConnectionListener, I
             throw new InvalidOperationException($"The listener needs to be initialized by calling {nameof(CreateListenerAsync)}.");
         }
 
-        try
+        while (!cancellationToken.IsCancellationRequested)
         {
-            var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
+            try
+            {
+                var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
+
+                if (!_pendingConnections.TryGetValue(quicConnection, out var connectionContext))
+                {
+                    throw new InvalidOperationException("Couldn't find ConnectionContext for QuicConnection.");
+                }
+                else
+                {
+                    _pendingConnections.Remove(quicConnection);
+                }
+
+                // Verify the connection context was created and set correctly.
+                Debug.Assert(connectionContext != null);
+                Debug.Assert(connectionContext.GetInnerConnection() == quicConnection);
+
+                QuicLog.AcceptedConnection(_log, connectionContext);
 
-            if (!_pendingConnections.TryGetValue(quicConnection, out var connectionContext))
+                return connectionContext;
+            }
+            catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted)
             {
-                throw new InvalidOperationException("Couldn't find ConnectionContext for QuicConnection.");
+                // OperationAborted is reported when an accept is in-progress and the listener is unbind/disposed.
+                QuicLog.ConnectionListenerAborted(_log, ex);
+                return null;
             }
-            else
+            catch (ObjectDisposedException ex)
             {
-                _pendingConnections.Remove(quicConnection);
+                // ObjectDisposedException is reported when an accept is started after the listener is unbind/disposed.
+                QuicLog.ConnectionListenerAborted(_log, ex);
+                return null;
+            }
+            catch (Exception ex)
+            {
+                // If the client rejects the connection because of an invalid cert then AcceptConnectionAsync throws.
+                // An error thrown inside ConnectionOptionsCallback can also throw from AcceptConnectionAsync.
+                // These are recoverable errors and we don't want to stop accepting connections.
+                QuicLog.ConnectionListenerAcceptConnectionFailed(_log, ex);
             }
-
-            // Verify the connection context was created and set correctly.
-            Debug.Assert(connectionContext != null);
-            Debug.Assert(connectionContext.GetInnerConnection() == quicConnection);
-
-            QuicLog.AcceptedConnection(_log, connectionContext);
-
-            return connectionContext;
-        }
-        catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted)
-        {
-            QuicLog.ConnectionListenerAborted(_log, ex);
         }
+
         return null;
     }
 
-    public async ValueTask UnbindAsync(CancellationToken cancellationToken = default)
-    {
-        await DisposeAsync();
-    }
+    public ValueTask UnbindAsync(CancellationToken cancellationToken = default) => DisposeAsync();
 
     public async ValueTask DisposeAsync()
     {

+ 3 - 0
src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs

@@ -232,6 +232,9 @@ internal static partial class QuicLog
         }
     }
 
+    [LoggerMessage(24, LogLevel.Debug, "QUIC listener connection failed.", EventName = "ConnectionListenerAcceptConnectionFailed")]
+    public static partial void ConnectionListenerAcceptConnectionFailed(ILogger logger, Exception exception);
+
     private static StreamType GetStreamType(QuicStreamContext streamContext) =>
         streamContext.CanRead && streamContext.CanWrite
             ? StreamType.Bidirectional

+ 126 - 4
src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs

@@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Logging;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests;
@@ -24,7 +25,7 @@ public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
 
     [ConditionalFact]
     [MsQuicSupported]
-    public async Task AcceptAsync_AfterUnbind_Error()
+    public async Task AcceptAsync_AfterUnbind_ReturnNull()
     {
         // Arrange
         await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
@@ -33,7 +34,7 @@ public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
         await connectionListener.UnbindAsync().DefaultTimeout();
 
         // Assert
-        await Assert.ThrowsAsync<ObjectDisposedException>(() => connectionListener.AcceptAndAddFeatureAsync().AsTask()).DefaultTimeout();
+        Assert.Null(await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout());
     }
 
     [ConditionalFact]
@@ -51,7 +52,7 @@ public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
         await using var clientConnection = await QuicConnection.ConnectAsync(options);
 
         // Assert
-        await using var serverConnection = await acceptTask.DefaultTimeout();
+        var serverConnection = await acceptTask.DefaultTimeout();
         Assert.False(serverConnection.ConnectionClosed.IsCancellationRequested);
 
         await serverConnection.DisposeAsync().AsTask().DefaultTimeout();
@@ -60,6 +61,48 @@ public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
         Assert.False(serverConnection.ConnectionClosed.IsCancellationRequested);
     }
 
+    [ConditionalFact]
+    [MsQuicSupported]
+    public async Task AcceptAsync_ClientCreatesInvalidConnection_ServerContinuesToAccept()
+    {
+        await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
+
+        // Act & Assert 1
+        Logger.LogInformation("Client creating successful connection 1");
+        var acceptTask1 = connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
+        await using var clientConnection1 = await QuicConnection.ConnectAsync(
+            QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint));
+
+        var serverConnection1 = await acceptTask1.DefaultTimeout();
+        Assert.False(serverConnection1.ConnectionClosed.IsCancellationRequested);
+        await serverConnection1.DisposeAsync().AsTask().DefaultTimeout();
+
+        // Act & Assert 2
+        var serverFailureLogTask = WaitForLogMessage(m => m.EventId.Name == "ConnectionListenerAcceptConnectionFailed");
+
+        Logger.LogInformation("Client creating unsuccessful connection 2");
+        var acceptTask2 = connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
+        var ex = await Assert.ThrowsAsync<AuthenticationException>(async () =>
+        {
+            await QuicConnection.ConnectAsync(
+                QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint, ignoreInvalidCertificate: false));
+        });
+        Assert.Contains("RemoteCertificateChainErrors", ex.Message);
+
+        Assert.False(acceptTask2.IsCompleted, "Accept doesn't return for failed client connection.");
+        var serverFailureLog = await serverFailureLogTask.DefaultTimeout();
+        Assert.NotNull(serverFailureLog.Exception);
+
+        // Act & Assert 3
+        Logger.LogInformation("Client creating successful connection 3");
+        await using var clientConnection2 = await QuicConnection.ConnectAsync(
+            QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint));
+
+        var serverConnection2 = await acceptTask2.DefaultTimeout();
+        Assert.False(serverConnection2.ConnectionClosed.IsCancellationRequested);
+        await serverConnection2.DisposeAsync().AsTask().DefaultTimeout();
+    }
+
     [ConditionalFact]
     [MsQuicSupported]
     [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
@@ -144,6 +187,85 @@ public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
         Assert.Contains(LogMessages, m => m.EventId.Name == "ConnectionListenerApplicationProtocolsNotSpecified");
     }
 
+    [ConditionalFact]
+    [MsQuicSupported]
+    public async Task AcceptAsync_UnbindAfterCall_CleanExitAndLog()
+    {
+        // Arrange
+        await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
+
+        // Act
+        var acceptTask = connectionListener.AcceptAndAddFeatureAsync();
+
+        await connectionListener.UnbindAsync().DefaultTimeout();
+
+        // Assert
+        Assert.Null(await acceptTask.AsTask().DefaultTimeout());
+
+        Assert.Contains(LogMessages, m => m.EventId.Name == "ConnectionListenerAborted");
+    }
+
+    [ConditionalFact]
+    [MsQuicSupported]
+    public async Task AcceptAsync_DisposeAfterCall_CleanExitAndLog()
+    {
+        // Arrange
+        await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
+
+        // Act
+        var acceptTask = connectionListener.AcceptAndAddFeatureAsync();
+
+        await connectionListener.DisposeAsync().DefaultTimeout();
+
+        // Assert
+        Assert.Null(await acceptTask.AsTask().DefaultTimeout());
+
+        Assert.Contains(LogMessages, m => m.EventId.Name == "ConnectionListenerAborted");
+    }
+
+    [ConditionalFact]
+    [MsQuicSupported]
+    public async Task AcceptAsync_ErrorFromServerCallback_CleanExitAndLog()
+    {
+        // Arrange
+        var throwErrorInCallback = true;
+        await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(
+            new TlsConnectionCallbackOptions
+            {
+                ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
+                OnConnection = (context, cancellationToken) =>
+                {
+                    if (throwErrorInCallback)
+                    {
+                        throwErrorInCallback = false;
+                        throw new Exception("An error!");
+                    }
+
+                    var options = new SslServerAuthenticationOptions();
+                    options.ServerCertificate = TestResources.GetTestCertificate();
+                    return ValueTask.FromResult(options);
+                }
+            },
+            LoggerFactory);
+
+        // Act
+        var acceptTask = connectionListener.AcceptAndAddFeatureAsync();
+
+        var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
+
+        var ex = await Assert.ThrowsAsync<AuthenticationException>(() => QuicConnection.ConnectAsync(options).AsTask()).DefaultTimeout();
+        Assert.Equal("Authentication failed because the remote party sent a TLS alert: 'UserCanceled'.", ex.Message);
+
+        // Assert
+        Assert.False(acceptTask.IsCompleted, "Still waiting for non-errored connection.");
+
+        await using var clientConnection = await QuicConnection.ConnectAsync(options).DefaultTimeout();
+        await using var serverConnection = await acceptTask.DefaultTimeout();
+
+        Assert.NotNull(serverConnection);
+        Assert.NotNull(clientConnection);
+    }
+
     [ConditionalFact]
     [MsQuicSupported]
     public async Task BindAsync_ListenersSharePort_ThrowAddressInUse()
@@ -265,8 +387,8 @@ public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
 
         syncPoint.Continue();
 
-        await Assert.ThrowsAsync<ArgumentException>(() => acceptTask.AsTask()).DefaultTimeout();
         await Assert.ThrowsAsync<AuthenticationException>(() => clientConnectionTask.AsTask()).DefaultTimeout();
+        Assert.False(acceptTask.IsCompleted);
 
         // Assert
         for (var i = 0; i < 20; i++)

+ 8 - 4
src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs

@@ -116,9 +116,9 @@ internal static class QuicTestHelpers
         return true;
     }
 
-    public static QuicClientConnectionOptions CreateClientConnectionOptions(EndPoint remoteEndPoint)
+    public static QuicClientConnectionOptions CreateClientConnectionOptions(EndPoint remoteEndPoint, bool? ignoreInvalidCertificate = null)
     {
-        return new QuicClientConnectionOptions
+        var options = new QuicClientConnectionOptions
         {
             MaxInboundBidirectionalStreams = 200,
             MaxInboundUnidirectionalStreams = 200,
@@ -128,12 +128,16 @@ internal static class QuicTestHelpers
                 ApplicationProtocols = new List<SslApplicationProtocol>
                 {
                     SslApplicationProtocol.Http3
-                },
-                RemoteCertificateValidationCallback = RemoteCertificateValidationCallback
+                }
             },
             DefaultStreamErrorCode = 0,
             DefaultCloseErrorCode = 0,
         };
+        if (ignoreInvalidCertificate ?? true)
+        {
+            options.ClientAuthenticationOptions.RemoteCertificateValidationCallback = RemoteCertificateValidationCallback;
+        }
+        return options;
     }
 
     public static async Task<QuicStreamContext> CreateAndCompleteBidirectionalStreamGracefully(QuicConnection clientConnection, MultiplexedConnectionContext serverConnection, ILogger logger)

+ 16 - 2
src/Servers/Kestrel/shared/test/HttpParsingData.cs

@@ -76,7 +76,9 @@ public class HttpParsingData
             var httpVersions = new[]
             {
                     "HTTP/1.0",
-                    "HTTP/1.1"
+                    "HTTP/1.1",
+                    " HTTP/1.1",
+                    "   HTTP/1.1"
                 };
 
             return from method in methods
@@ -91,7 +93,7 @@ public class HttpParsingData
                            $"{path.Item1}",
                            $"{path.Item2}",
                            queryString,
-                           httpVersion
+                           httpVersion.Trim()
                        };
         }
     }
@@ -164,6 +166,12 @@ public class HttpParsingData
                     "GET / HTTP/1.1\n",
                     "GET / HTTP/1.0\rA\n",
                     "GET / HTTP/1.1\ra\n",
+                    "GET  / HTTP/1.1\r\n",
+                    "GET   / HTTP/1.1\r\n",
+                    "GET  /  HTTP/1.1\r\n",
+                    "GET   /   HTTP/1.1\r\n",
+                    "GET / HTTP/1.1 \r\n",
+                    "GET / HTTP/1.1  \r\n",
                     "GET / H\r\n",
                     "GET / HT\r\n",
                     "GET / HTT\r\n",
@@ -195,6 +203,12 @@ public class HttpParsingData
                     "CUSTOM / HTTP/1.1\n",
                     "CUSTOM / HTTP/1.0\rA\n",
                     "CUSTOM / HTTP/1.1\ra\n",
+                    "CUSTOM  / HTTP/1.1\r\n",
+                    "CUSTOM   / HTTP/1.1\r\n",
+                    "CUSTOM  /  HTTP/1.1\r\n",
+                    "CUSTOM   /   HTTP/1.1\r\n",
+                    "CUSTOM / HTTP/1.1 \r\n",
+                    "CUSTOM / HTTP/1.1  \r\n",
                     "CUSTOM / H\r\n",
                     "CUSTOM / HT\r\n",
                     "CUSTOM / HTT\r\n",

+ 1 - 0
src/Shared/BrowserTesting/src/Microsoft.AspNetCore.BrowserTesting.csproj

@@ -12,6 +12,7 @@
     <Reference Include="Microsoft.Playwright" Condition="'$(IsPlaywrightAvailable)' == 'true'" />
     <Reference Include="Microsoft.Playwright" ExcludeAssets="build" Condition="'$(IsPlaywrightAvailable)' != 'true'" />
     <Reference Include="Microsoft.AspNetCore.Testing" />
+    <Reference Include="Microsoft.Extensions.Configuration.Json" />
   </ItemGroup>
 
 </Project>

+ 5 - 0
src/SignalR/common/Shared/ClientResultsManager.cs

@@ -42,6 +42,11 @@ internal sealed class ClientResultsManager : IInvocationBinder
     {
         var result = _pendingInvocations.TryAdd(invocationId, invocationInfo);
         Debug.Assert(result);
+        // Should have a 50% chance of happening once every 2.71 quintillion invocations (see UUID in Wikipedia)
+        if (!result)
+        {
+            invocationInfo.Complete(invocationInfo.Tcs, CompletionMessage.WithError(invocationId, "ID collision occurred when using client results. This is likely a bug in SignalR."));
+        }
     }
 
     public void TryCompleteResult(string connectionId, CompletionMessage message)

+ 9 - 12
src/SignalR/server/Specification.Tests/src/ScaleoutHubLifetimeManagerTests.cs

@@ -588,25 +588,22 @@ public abstract class ScaleoutHubLifetimeManagerTests<TBackplane> : HubLifetimeM
         var manager1 = CreateNewHubLifetimeManager(backplane);
         var manager2 = CreateNewHubLifetimeManager(backplane);
 
-        using (var client1 = new TestClient())
-        using (var client2 = new TestClient())
+        using (var client = new TestClient())
         {
-            var connection1 = HubConnectionContextUtils.Create(client1.Connection);
-            var connection2 = HubConnectionContextUtils.Create(client2.Connection);
+            var connection = HubConnectionContextUtils.Create(client.Connection);
 
-            await manager1.OnConnectedAsync(connection1).DefaultTimeout();
-            await manager2.OnConnectedAsync(connection2).DefaultTimeout();
+            await manager1.OnConnectedAsync(connection).DefaultTimeout();
 
-            var invoke1 = manager1.InvokeConnectionAsync<int>(connection2.ConnectionId, "Result", new object[] { "test" }, cancellationToken: default);
-            var invocation2 = Assert.IsType<InvocationMessage>(await client2.ReadAsync().DefaultTimeout());
+            var invoke1 = manager1.InvokeConnectionAsync<int>(connection.ConnectionId, "Result", new object[] { "test" }, cancellationToken: default);
+            var invocation2 = Assert.IsType<InvocationMessage>(await client.ReadAsync().DefaultTimeout());
 
-            var invoke2 = manager2.InvokeConnectionAsync<int>(connection1.ConnectionId, "Result", new object[] { "test" }, cancellationToken: default);
-            var invocation1 = Assert.IsType<InvocationMessage>(await client1.ReadAsync().DefaultTimeout());
+            var invoke2 = manager2.InvokeConnectionAsync<int>(connection.ConnectionId, "Result", new object[] { "test" }, cancellationToken: default);
+            var invocation1 = Assert.IsType<InvocationMessage>(await client.ReadAsync().DefaultTimeout());
 
             Assert.NotEqual(invocation1.InvocationId, invocation2.InvocationId);
 
-            await manager1.SetConnectionResultAsync(connection2.ConnectionId, CompletionMessage.WithResult(invocation2.InvocationId, 2)).DefaultTimeout();
-            await manager2.SetConnectionResultAsync(connection1.ConnectionId, CompletionMessage.WithResult(invocation1.InvocationId, 5)).DefaultTimeout();
+            await manager1.SetConnectionResultAsync(connection.ConnectionId, CompletionMessage.WithResult(invocation2.InvocationId, 2)).DefaultTimeout();
+            await manager2.SetConnectionResultAsync(connection.ConnectionId, CompletionMessage.WithResult(invocation1.InvocationId, 5)).DefaultTimeout();
 
             var res = await invoke1.DefaultTimeout();
             Assert.Equal(2, res);

+ 7 - 12
src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs

@@ -23,12 +23,18 @@ internal sealed class RedisChannels
     /// </summary>
     public string GroupManagement { get; }
 
-    public RedisChannels(string prefix)
+    /// <summary>
+    /// Gets the name of the internal channel for receiving client results.
+    /// </summary>
+    public string ReturnResults { get; }
+
+    public RedisChannels(string prefix, string serverName)
     {
         _prefix = prefix;
 
         All = prefix + ":all";
         GroupManagement = prefix + ":internal:groups";
+        ReturnResults = _prefix + ":internal:return:" + serverName;
     }
 
     /// <summary>
@@ -71,15 +77,4 @@ internal sealed class RedisChannels
     {
         return _prefix + ":internal:ack:" + serverName;
     }
-
-    /// <summary>
-    /// Gets the name of the client return results channel for the specified server.
-    /// </summary>
-    /// <param name="serverName">The name of the server to get the client return results channel for.</param>
-    /// <returns></returns>
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    public string ReturnResults(string serverName)
-    {
-        return _prefix + ":internal:return:" + serverName;
-    }
 }

+ 21 - 6
src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs

@@ -39,7 +39,6 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
 
     private readonly AckHandler _ackHandler;
     private int _internalAckId;
-    private ulong _lastInvocationId;
 
     /// <summary>
     /// Constructs the <see cref="RedisHubLifetimeManager{THub}"/> with types from Dependency Injection.
@@ -72,7 +71,7 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
         _logger = logger;
         _options = options.Value;
         _ackHandler = new AckHandler();
-        _channels = new RedisChannels(typeof(THub).FullName!);
+        _channels = new RedisChannels(typeof(THub).FullName!, _serverName);
         if (globalHubOptions != null && hubOptions != null)
         {
             _protocol = new RedisProtocol(new DefaultHubMessageSerializer(hubProtocolResolver, globalHubOptions.Value.SupportedProtocols, hubOptions.Value.SupportedProtocols));
@@ -416,8 +415,8 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
 
         var connection = _connections[connectionId];
 
-        // Needs to be unique across servers, easiest way to do that is prefix with connection ID.
-        var invocationId = $"{connectionId}{Interlocked.Increment(ref _lastInvocationId)}";
+        // ID needs to be unique for each invocation and across servers, we generate a GUID every time, that should provide enough uniqueness guarantees.
+        var invocationId = GenerateInvocationId();
 
         using var _ = CancellationTokenUtils.CreateLinkedToken(cancellationToken,
             connection?.ConnectionAborted ?? default, out var linkedToken);
@@ -428,7 +427,7 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
             if (connection == null)
             {
                 // TODO: Need to handle other server going away while waiting for connection result
-                var messageBytes = _protocol.WriteInvocation(methodName, args, invocationId, returnChannel: _channels.ReturnResults(_serverName));
+                var messageBytes = _protocol.WriteInvocation(methodName, args, invocationId, returnChannel: _channels.ReturnResults);
                 var received = await PublishAsync(_channels.Connection(connectionId), messageBytes);
                 if (received < 1)
                 {
@@ -674,7 +673,7 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
 
     private async Task SubscribeToReturnResultsAsync()
     {
-        var channel = await _bus!.SubscribeAsync(_channels.ReturnResults(_serverName));
+        var channel = await _bus!.SubscribeAsync(_channels.ReturnResults);
         channel.OnMessage((channelMessage) =>
         {
             var completion = RedisProtocol.ReadCompletion(channelMessage.Message);
@@ -700,6 +699,7 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
             Debug.Assert(parseSuccess);
 
             var invocationInfo = _clientResultsManager.RemoveInvocation(((CompletionMessage)hubMessage!).InvocationId!);
+
             invocationInfo?.Completion(invocationInfo?.Tcs!, (CompletionMessage)hubMessage!);
         });
     }
@@ -784,6 +784,21 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
         return $"{Environment.MachineName}_{Guid.NewGuid():N}";
     }
 
+    private static string GenerateInvocationId()
+    {
+        Span<byte> buffer = stackalloc byte[16];
+        var success = Guid.NewGuid().TryWriteBytes(buffer);
+        Debug.Assert(success);
+        // 16 * 4/3 = 21.333 which means base64 encoding will use 22 characters of actual data and 2 characters of padding ('=')
+        Span<char> base64 = stackalloc char[24];
+        success = Convert.TryToBase64Chars(buffer, base64, out var written);
+        Debug.Assert(success);
+        Debug.Assert(written == 24);
+        // Trim the two '=='
+        Debug.Assert(base64.EndsWith("=="));
+        return new string(base64[..^2]);
+    }
+
     private sealed class LoggerTextWriter : TextWriter
     {
         private readonly ILogger _logger;

+ 2 - 2
src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommand.cs

@@ -5,7 +5,7 @@ using System;
 using System.IO;
 using System.Linq;
 using System.Reflection;
-#if NETCOREAPP2_1
+#if NETCOREAPP
 using System.Runtime.Loader;
 #endif
 using Microsoft.Extensions.CommandLineUtils;
@@ -69,7 +69,7 @@ internal sealed class GetDocumentCommand : ProjectCommandBase
             }
         }
 
-#if NETCOREAPP2_1
+#if NETCOREAPP
         AssemblyLoadContext.Default.Resolving += (loadContext, assemblyName) =>
         {
             var name = assemblyName.Name;

+ 107 - 0
src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs

@@ -2,13 +2,20 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.IO;
 using System.Reflection;
 using System.Text;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Tools.Internal;
+#if NET7_0_OR_GREATER
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Http.Features;
+#endif
 
 namespace Microsoft.Extensions.ApiDescription.Tool.Commands;
 
@@ -53,6 +60,88 @@ internal sealed class GetDocumentCommandWorker
             return 3;
         }
 
+#if NET7_0_OR_GREATER
+        // Register no-op implementations of IServer and IHostLifetime
+        // to prevent the application server from actually launching after build.
+        void ConfigureHostBuilder(object hostBuilder)
+        {
+            ((IHostBuilder)hostBuilder).ConfigureServices((context, services) =>
+            {
+                services.AddSingleton<IServer, NoopServer>();
+                services.AddSingleton<IHostLifetime, NoopHostLifetime>();
+            });
+        }
+
+        // Register a TCS to be invoked when the entrypoint (aka Program.Main)
+        // has finished running. For minimal APIs, this means that all app.X
+        // calls about the host has been built have been executed.
+        var waitForStartTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
+        void OnEntryPointExit(Exception exception)
+        {
+            // If the entry point exited, we'll try to complete the wait
+            if (exception != null)
+            {
+                waitForStartTcs.TrySetException(exception);
+            }
+            else
+            {
+                waitForStartTcs.TrySetResult(null);
+            }
+        }
+
+        // Resolve the host factory, ensuring that we don't stop the
+        // application after the host has been built.
+        var factory = HostFactoryResolver.ResolveHostFactory(assembly,
+            stopApplication: false,
+            configureHostBuilder: ConfigureHostBuilder,
+            entrypointCompleted: OnEntryPointExit);
+
+        if (factory == null)
+        {
+            _reporter.WriteError(Resources.FormatMethodsNotFound(
+                HostFactoryResolver.BuildWebHost,
+                HostFactoryResolver.CreateHostBuilder,
+                HostFactoryResolver.CreateWebHostBuilder,
+                entryPointType));
+
+            return 8;
+        }
+
+        try
+        {
+            // Retrieve the service provider from the target host.
+            var services = ((IHost)factory(new[] { $"--{HostDefaults.ApplicationKey}={assemblyName}" })).Services;
+            if (services == null)
+            {
+                _reporter.WriteError(Resources.FormatServiceProviderNotFound(
+                    typeof(IServiceProvider),
+                    HostFactoryResolver.BuildWebHost,
+                    HostFactoryResolver.CreateHostBuilder,
+                    HostFactoryResolver.CreateWebHostBuilder,
+                    entryPointType));
+
+                return 9;
+            }
+
+            // Wait for the application to start to ensure that all configurations
+            // on the WebApplicationBuilder have been processed.
+            var applicationLifetime = services.GetRequiredService<IHostApplicationLifetime>();
+            using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)))
+            {
+                waitForStartTcs.Task.Wait();
+                var success = GetDocuments(services);
+                if (!success)
+                {
+                    return 10;
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            _reporter.WriteError(ex.ToString());
+            return 11;
+        }
+#else
         try
         {
             var serviceFactory = HostFactoryResolver.ResolveServiceProviderFactory(assembly);
@@ -91,6 +180,7 @@ internal sealed class GetDocumentCommandWorker
             _reporter.WriteError(ex.ToString());
             return 7;
         }
+#endif
 
         return 0;
     }
@@ -303,4 +393,21 @@ internal sealed class GetDocumentCommandWorker
 
         return result;
     }
+
+#if NET7_0_OR_GREATER
+        private sealed class NoopHostLifetime : IHostLifetime
+        {
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+        }
+
+        private sealed class NoopServer : IServer
+        {
+            public IFeatureCollection Features { get; } = new FeatureCollection();
+            public void Dispose() { }
+            public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) => Task.CompletedTask;
+            public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+        }
+#endif
 }

+ 5 - 1
src/Tools/GetDocumentInsider/src/GetDocument.Insider.csproj

@@ -5,7 +5,7 @@
     <IsPackable>false</IsPackable>
     <OutputType>Exe</OutputType>
     <RootNamespace>Microsoft.Extensions.ApiDescription.Tool</RootNamespace>
-    <TargetFrameworks>netcoreapp2.1;$(DefaultNetFxTargetFramework)</TargetFrameworks>
+    <TargetFrameworks>netcoreapp2.1;$(DefaultNetCoreTargetFramework);$(DefaultNetFxTargetFramework)</TargetFrameworks>
     <IsShippingPackage>false</IsShippingPackage>
     <Nullable>disable</Nullable>
     <NoWarn>$(NoWarn);nullable</NoWarn>
@@ -15,6 +15,10 @@
     <Reference Include="Microsoft.CSharp" />
   </ItemGroup>
 
+  <ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
+    <Reference Include="Microsoft.AspNetCore" />
+  </ItemGroup>
+
   <ItemGroup>
     <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.6.0">
       <AllowExplicitReference>true</AllowExplicitReference>

+ 8 - 1
src/Tools/dotnet-getdocument/src/Commands/InvokeCommand.cs

@@ -79,9 +79,16 @@ internal sealed class InvokeCommand : HelpCommandBase
                             projectName,
                             targetFramework.Version));
                     }
+                    else if (targetFramework.Version >= new Version(7, 0))
+                    {
+                        toolsDirectory = Path.Combine(thisPath, $"net{targetFramework.Version}");
+                    }
+                    else
+                    {
+                        toolsDirectory = Path.Combine(thisPath, "netcoreapp2.1");
+                    }
 
                     executable = DotNetMuxer.MuxerPathOrDefault();
-                    toolsDirectory = Path.Combine(thisPath, "netcoreapp2.1");
 
                     args.Add("exec");
                     args.Add("--depsFile");

+ 1 - 1
src/Tools/dotnet-getdocument/src/dotnet-getdocument.csproj

@@ -5,7 +5,7 @@
     <IsPackable>false</IsPackable>
     <OutputType>Exe</OutputType>
     <RootNamespace>Microsoft.Extensions.ApiDescription.Tool</RootNamespace>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFrameworks>netcoreapp2.1;$(DefaultNetCoreTargetFramework)</TargetFrameworks>
     <UseAppHost>false</UseAppHost>
     <IsShippingPackage>false</IsShippingPackage>
   </PropertyGroup>

+ 1 - 0
src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj

@@ -30,6 +30,7 @@
     <Reference Include="System.IdentityModel.Tokens.Jwt" />
     <Reference Include="Microsoft.Extensions.Configuration.Abstractions" />
     <Reference Include="Microsoft.Extensions.Configuration" />
+    <Reference Include="Microsoft.Extensions.Configuration.Binder" />
     <Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
   </ItemGroup>