diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs b/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
index 69051dd32ff0..87f7e1b68674 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
@@ -17,7 +17,7 @@
namespace Microsoft.AspNetCore.Server.HttpSys
{
[MsQuicSupported] // Required by HttpClient
- [Http3Supported]
+ [HttpSysHttp3Supported]
public class Http3Tests
{
[ConditionalFact]
diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj
index c574f2941644..43bac347fcd9 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj
+++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj
@@ -23,6 +23,7 @@
+
diff --git a/src/Servers/IIS/Directory.Build.props b/src/Servers/IIS/Directory.Build.props
new file mode 100644
index 000000000000..ba4f039671d7
--- /dev/null
+++ b/src/Servers/IIS/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+
+ $(MSBuildThisFileDirectory)..\Kestrel\shared\
+
+
diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
index 2766c803529d..34ef1725f557 100644
--- a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
+++ b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
@@ -3,6 +3,7 @@
using System;
using System.Buffers;
+using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
@@ -65,8 +66,17 @@ public override async Task ProcessRequestAsync()
if (!success && HasResponseStarted && NativeMethods.HttpHasResponse4(_requestNativeHandle))
{
// HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7
- // Otherwise the default is Cancel = 0x8.
- SetResetCode(2);
+ // Otherwise the default is Cancel = 0x8 (h2) or 0x010c (h3).
+ if (HttpVersion == System.Net.HttpVersion.Version20)
+ {
+ // HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7
+ SetResetCode(2);
+ }
+ else if (HttpVersion == System.Net.HttpVersion.Version30)
+ {
+ // HTTP/3 H3_INTERNAL_ERROR = 0x0102 https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-8.1
+ SetResetCode(0x0102);
+ }
}
if (!_requestAborted)
diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs
new file mode 100644
index 000000000000..4b889be972c8
--- /dev/null
+++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs
@@ -0,0 +1,178 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Quic;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.IIS.FunctionalTests;
+using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
+using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using Microsoft.Win32;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
+{
+ [MsQuicSupported]
+ [HttpSysHttp3Supported]
+ [Collection(IISHttpsTestSiteCollection.Name)]
+ public class Http3Tests
+ {
+ public Http3Tests(IISTestSiteFixture fixture)
+ {
+ var port = TestPortHelper.GetNextSSLPort();
+ fixture.DeploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/";
+ fixture.DeploymentParameters.AddHttpsToServerConfig();
+ fixture.DeploymentParameters.SetWindowsAuth(false);
+ Fixture = fixture;
+ }
+
+ public IISTestSiteFixture Fixture { get; }
+
+ [ConditionalFact]
+ public async Task Http3_Direct()
+ {
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version30;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ var response = await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_Direct");
+
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(HttpVersion.Version30, response.Version);
+ Assert.Equal("HTTP/3", await response.Content.ReadAsStringAsync());
+ }
+
+ [ConditionalFact]
+ public async Task Http3_AltSvcHeader_UpgradeFromHttp1()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_AltSvcHeader_UpgradeFromHttp1";
+
+ var altsvc = $@"h3="":{new Uri(address).Port}""";
+ using var client = SetUpClient();
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
+
+ // First request is HTTP/1.1, gets an alt-svc response
+ var request = new HttpRequestMessage(HttpMethod.Get, address);
+ request.Version = HttpVersion.Version11;
+ request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ var response1 = await client.SendAsync(request);
+ response1.EnsureSuccessStatusCode();
+ Assert.Equal("HTTP/1.1", await response1.Content.ReadAsStringAsync());
+ Assert.Equal(altsvc, response1.Headers.GetValues(HeaderNames.AltSvc).SingleOrDefault());
+
+ // Second request is HTTP/3
+ var response3 = await client.GetAsync(address);
+ Assert.Equal(HttpVersion.Version30, response3.Version);
+ Assert.Equal("HTTP/3", await response3.Content.ReadAsStringAsync());
+ }
+
+ [ConditionalFact]
+ public async Task Http3_AltSvcHeader_UpgradeFromHttp2()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_AltSvcHeader_UpgradeFromHttp2";
+
+ var altsvc = $@"h3="":{new Uri(address).Port}""";
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version20;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
+
+ // First request is HTTP/2, gets an alt-svc response
+ var response2 = await client.GetAsync(address);
+ response2.EnsureSuccessStatusCode();
+ Assert.Equal(altsvc, response2.Headers.GetValues(HeaderNames.AltSvc).SingleOrDefault());
+ Assert.Equal("HTTP/2", await response2.Content.ReadAsStringAsync());
+
+ // Second request is HTTP/3
+ var response3 = await client.GetStringAsync(address);
+ Assert.Equal("HTTP/3", response3);
+ }
+
+ [ConditionalFact]
+ public async Task Http3_ResponseTrailers()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResponseTrailers";
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version30;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ var response = await client.GetAsync(address);
+ response.EnsureSuccessStatusCode();
+ var result = await response.Content.ReadAsStringAsync();
+ Assert.Equal("HTTP/3", result);
+ Assert.Equal("value", response.TrailingHeaders.GetValues("custom").SingleOrDefault());
+ }
+
+ [ConditionalFact]
+ public async Task Http3_ResetBeforeHeaders()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResetBeforeHeaders";
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version30;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ var ex = await Assert.ThrowsAsync(() => client.GetAsync(address));
+ var qex = Assert.IsType(ex.InnerException);
+ Assert.Equal(0x010b, qex.ErrorCode);
+ }
+
+ [ConditionalFact]
+ public async Task Http3_ResetAfterHeaders()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResetAfterHeaders";
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version30;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
+ var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+ await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_ResetAfterHeaders_SetResult");
+ response.EnsureSuccessStatusCode();
+ var ex = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync());
+ var qex = Assert.IsType(ex.InnerException?.InnerException?.InnerException);
+ Assert.Equal(0x010c, qex.ErrorCode); // H3_REQUEST_CANCELLED
+ }
+
+ [ConditionalFact]
+ public async Task Http3_AppExceptionAfterHeaders_InternalError()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_AppExceptionAfterHeaders_InternalError";
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version30;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
+
+ var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+ await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_AppExceptionAfterHeaders_InternalError_SetResult");
+ response.EnsureSuccessStatusCode();
+ var ex = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync());
+ var qex = Assert.IsType(ex.InnerException?.InnerException?.InnerException);
+ Assert.Equal(0x0102, qex.ErrorCode); // H3_INTERNAL_ERROR
+ }
+
+ [ConditionalFact]
+ public async Task Http3_Abort_Cancel()
+ {
+ var address = Fixture.Client.BaseAddress.ToString() + "Http3_Abort_Cancel";
+ using var client = SetUpClient();
+ client.DefaultRequestVersion = HttpVersion.Version30;
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
+
+ var ex = await Assert.ThrowsAsync(() => client.GetAsync(address));
+ var qex = Assert.IsType(ex.InnerException);
+ Assert.Equal(0x010c, qex.ErrorCode); // H3_REQUEST_CANCELLED
+ }
+
+ private HttpClient SetUpClient()
+ {
+ var handler = new HttpClientHandler();
+ // Needed on CI, the IIS Express cert we use isn't trusted there.
+ handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
+ return new HttpClient(handler);
+ }
+ }
+}
diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj
index a7aab6e07a5b..5aa5b73cb057 100644
--- a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj
+++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj
@@ -11,6 +11,11 @@
+
+
+
+
+
@@ -29,6 +34,8 @@
+
+
diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs
index 4e4c0b652adf..2e3a90029527 100644
--- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs
+++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs
@@ -1550,6 +1550,111 @@ public Task OnCompletedThrows(HttpContext httpContext)
return Task.CompletedTask;
}
+ public Task Http3_Direct(HttpContext context)
+ {
+ try
+ {
+ Assert.True(context.Request.IsHttps);
+ return context.Response.WriteAsync(context.Request.Protocol);
+ }
+ catch (Exception ex)
+ {
+ return context.Response.WriteAsync(ex.ToString());
+ }
+ }
+
+ public Task Http3_AltSvcHeader_UpgradeFromHttp1(HttpContext context)
+ {
+ var altsvc = $@"h3="":{context.Connection.LocalPort}""";
+ try
+ {
+ Assert.True(context.Request.IsHttps);
+ context.Response.Headers.AltSvc = altsvc;
+ return context.Response.WriteAsync(context.Request.Protocol);
+ }
+ catch (Exception ex)
+ {
+ return context.Response.WriteAsync(ex.ToString());
+ }
+ }
+
+ public Task Http3_AltSvcHeader_UpgradeFromHttp2(HttpContext context)
+ {
+ return Http3_AltSvcHeader_UpgradeFromHttp1(context);
+ }
+
+ public async Task Http3_ResponseTrailers(HttpContext context)
+ {
+ try
+ {
+ Assert.True(context.Request.IsHttps);
+ await context.Response.WriteAsync(context.Request.Protocol);
+ context.Response.AppendTrailer("custom", "value");
+ }
+ catch (Exception ex)
+ {
+ await context.Response.WriteAsync(ex.ToString());
+ }
+ }
+
+ public Task Http3_ResetBeforeHeaders(HttpContext context)
+ {
+ try
+ {
+ Assert.True(context.Request.IsHttps);
+ context.Features.Get().Reset(0x010b); // H3_REQUEST_REJECTED
+ return Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ return context.Response.WriteAsync(ex.ToString());
+ }
+ }
+
+ private TaskCompletionSource _http3_ResetAfterHeadersCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public async Task Http3_ResetAfterHeaders(HttpContext context)
+ {
+ try
+ {
+ Assert.True(context.Request.IsHttps);
+ await context.Response.Body.FlushAsync();
+ await _http3_ResetAfterHeadersCts.Task;
+ context.Features.Get().Reset(0x010c); // H3_REQUEST_CANCELLED
+ }
+ catch (Exception ex)
+ {
+ await context.Response.WriteAsync(ex.ToString());
+ }
+ }
+
+ public Task Http3_ResetAfterHeaders_SetResult(HttpContext context)
+ {
+ _http3_ResetAfterHeadersCts.SetResult();
+ return Task.CompletedTask;
+ }
+
+ private TaskCompletionSource _http3_AppExceptionAfterHeaders_InternalErrorCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public async Task Http3_AppExceptionAfterHeaders_InternalError(HttpContext context)
+ {
+ await context.Response.Body.FlushAsync();
+ await _http3_AppExceptionAfterHeaders_InternalErrorCts.Task;
+ throw new Exception("App Exception");
+ }
+
+ public Task Http3_AppExceptionAfterHeaders_InternalError_SetResult(HttpContext context)
+ {
+ _http3_AppExceptionAfterHeaders_InternalErrorCts.SetResult();
+ return Task.CompletedTask;
+ }
+
+ public Task Http3_Abort_Cancel(HttpContext context)
+ {
+ context.Abort();
+ return Task.CompletedTask;
+ }
+
internal static readonly HashSet<(string, StringValues, StringValues)> NullTrailers = new HashSet<(string, StringValues, StringValues)>()
{
("NullString", (string)null, (string)null),
diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs
similarity index 90%
rename from src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs
rename to src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs
index e3a2bd25d7cc..4c2256a212ae 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs
+++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs
@@ -3,13 +3,12 @@
using System;
using System.Net.Quic;
-using Microsoft.AspNetCore.Testing;
using Microsoft.Win32;
-namespace Microsoft.AspNetCore.Server.HttpSys
+namespace Microsoft.AspNetCore.Testing
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
- public class Http3SupportedAttribute : Attribute, ITestCondition
+ public class HttpSysHttp3SupportedAttribute : Attribute, ITestCondition
{
// We have the same OS and TLS version requirements as MsQuic so check that first.
public bool IsMet => QuicImplementationProviders.MsQuic.IsSupported && IsRegKeySet;