diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs b/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
index 69051dd32ff07cb13096be432b98166766d051a3..87f7e1b68674b5a125732d1a2746288268f44262 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
@@ -17,7 +17,7 @@ using Xunit;
 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 c574f2941644248bb5948fe6728df76713ff2713..43bac347fcd92ee3e118d785af08e5b0ea58324a 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 @@
     <Compile Include="$(KestrelSharedSourceRoot)test\TestResources.cs" LinkBase="shared" />
     <Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
     <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\" />
+    <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\HttpSysHttp3SupportedAttribute.cs" LinkBase="shared\" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/Servers/IIS/Directory.Build.props b/src/Servers/IIS/Directory.Build.props
new file mode 100644
index 0000000000000000000000000000000000000000..ba4f039671d75c90ab456ff9f005205ded17795f
--- /dev/null
+++ b/src/Servers/IIS/Directory.Build.props
@@ -0,0 +1,6 @@
+<Project>
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
+  <PropertyGroup>
+    <KestrelSharedSourceRoot>$(MSBuildThisFileDirectory)..\Kestrel\shared\</KestrelSharedSourceRoot>
+  </PropertyGroup>
+</Project>
diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
index 2766c803529dcdcb2c4de196ea8993fe74787aff..34ef1725f557a7f3a1c896b0d206dddc8f01cdca 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 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
                 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 0000000000000000000000000000000000000000..4b889be972c8c4996af7d6241f608ee0baa9ba13
--- /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<HttpRequestException>(() => client.GetAsync(address));
+            var qex = Assert.IsType<QuicStreamAbortedException>(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<HttpRequestException>(() => response.Content.ReadAsStringAsync());
+            var qex = Assert.IsType<QuicStreamAbortedException>(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<HttpRequestException>(() => response.Content.ReadAsStringAsync());
+            var qex = Assert.IsType<QuicStreamAbortedException>(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<HttpRequestException>(() => client.GetAsync(address));
+            var qex = Assert.IsType<QuicStreamAbortedException>(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 a7aab6e07a5b08c81ba3070a039cdcde6274fd6f..5aa5b73cb0579f3a7ac93e2db829a97272ea2caa 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 @@
 
   <Import Project="../FunctionalTest.props" />
 
+  <ItemGroup>
+    <!-- Required for QUIC & HTTP/3 in .NET 6 - https://github.com/dotnet/runtime/pull/55332 -->
+    <RuntimeHostConfigurationOption Include="System.Net.SocketsHttpHandler.Http3Support" Value="true" />
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\testassets\IIS.Common.TestLib\IIS.Common.TestLib.csproj" />
     <ProjectReference Include="..\testassets\InProcessWebSite\InProcessWebSite.csproj">
@@ -29,6 +34,8 @@
     <Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared\" />
     <Compile Remove="$(SharedSourceRoot)ServerInfrastructure\DuplexPipe.cs" />
     <Compile Include="$(SharedSourceRoot)TaskToApm.cs" Link="Shared\TaskToApm.cs" />
+    <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="Shared\" />
+    <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\HttpSysHttp3SupportedAttribute.cs" LinkBase="shared\" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs
index 4e4c0b652adf7e007574f2d66f17cdcb5649b6a9..2e3a900295274a0fe8402f7571b923933841f517 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 @@ namespace TestSite
             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<IHttpResetFeature>().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<IHttpResetFeature>().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 e3a2bd25d7cc394f9ada5b5170c74ad90940ab49..4c2256a212ae581d52e2e29ae1e9a20e21d78b35 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;