Skip to content

Commit fa0aa05

Browse files
authored
HTTP/3 with IIS (dotnet#35934)
* Add failing test * Another bad test * Weird tests * Add IIS tests for HTTP/3 * Undo changes * Fixup * Move attribute * Feedback
1 parent 3dfe7bd commit fa0aa05

File tree

8 files changed

+312
-6
lines changed

8 files changed

+312
-6
lines changed

src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
namespace Microsoft.AspNetCore.Server.HttpSys
1818
{
1919
[MsQuicSupported] // Required by HttpClient
20-
[Http3Supported]
20+
[HttpSysHttp3Supported]
2121
public class Http3Tests
2222
{
2323
[ConditionalFact]

src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(KestrelSharedSourceRoot)test\TestResources.cs" LinkBase="shared" />
2424
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
2525
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\" />
26+
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\HttpSysHttp3SupportedAttribute.cs" LinkBase="shared\" />
2627
</ItemGroup>
2728

2829
<ItemGroup>

src/Servers/IIS/Directory.Build.props

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Project>
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
3+
<PropertyGroup>
4+
<KestrelSharedSourceRoot>$(MSBuildThisFileDirectory)..\Kestrel\shared\</KestrelSharedSourceRoot>
5+
</PropertyGroup>
6+
</Project>

src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Net;
67
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Builder;
89
using Microsoft.AspNetCore.Hosting.Server;
@@ -65,8 +66,17 @@ public override async Task<bool> ProcessRequestAsync()
6566
if (!success && HasResponseStarted && NativeMethods.HttpHasResponse4(_requestNativeHandle))
6667
{
6768
// HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7
68-
// Otherwise the default is Cancel = 0x8.
69-
SetResetCode(2);
69+
// Otherwise the default is Cancel = 0x8 (h2) or 0x010c (h3).
70+
if (HttpVersion == System.Net.HttpVersion.Version20)
71+
{
72+
// HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7
73+
SetResetCode(2);
74+
}
75+
else if (HttpVersion == System.Net.HttpVersion.Version30)
76+
{
77+
// HTTP/3 H3_INTERNAL_ERROR = 0x0102 https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-8.1
78+
SetResetCode(0x0102);
79+
}
7080
}
7181

7282
if (!_requestAborted)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using System.Net;
9+
using System.Net.Http;
10+
using System.Net.Quic;
11+
using System.Text;
12+
using System.Threading.Tasks;
13+
using Microsoft.AspNetCore.Http;
14+
using Microsoft.AspNetCore.Server.IIS.FunctionalTests;
15+
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
16+
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
17+
using Microsoft.AspNetCore.Testing;
18+
using Microsoft.Extensions.Hosting;
19+
using Microsoft.Extensions.Logging;
20+
using Microsoft.Net.Http.Headers;
21+
using Microsoft.Win32;
22+
using Xunit;
23+
24+
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
25+
{
26+
[MsQuicSupported]
27+
[HttpSysHttp3Supported]
28+
[Collection(IISHttpsTestSiteCollection.Name)]
29+
public class Http3Tests
30+
{
31+
public Http3Tests(IISTestSiteFixture fixture)
32+
{
33+
var port = TestPortHelper.GetNextSSLPort();
34+
fixture.DeploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/";
35+
fixture.DeploymentParameters.AddHttpsToServerConfig();
36+
fixture.DeploymentParameters.SetWindowsAuth(false);
37+
Fixture = fixture;
38+
}
39+
40+
public IISTestSiteFixture Fixture { get; }
41+
42+
[ConditionalFact]
43+
public async Task Http3_Direct()
44+
{
45+
using var client = SetUpClient();
46+
client.DefaultRequestVersion = HttpVersion.Version30;
47+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
48+
var response = await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_Direct");
49+
50+
response.EnsureSuccessStatusCode();
51+
Assert.Equal(HttpVersion.Version30, response.Version);
52+
Assert.Equal("HTTP/3", await response.Content.ReadAsStringAsync());
53+
}
54+
55+
[ConditionalFact]
56+
public async Task Http3_AltSvcHeader_UpgradeFromHttp1()
57+
{
58+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_AltSvcHeader_UpgradeFromHttp1";
59+
60+
var altsvc = $@"h3="":{new Uri(address).Port}""";
61+
using var client = SetUpClient();
62+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
63+
64+
// First request is HTTP/1.1, gets an alt-svc response
65+
var request = new HttpRequestMessage(HttpMethod.Get, address);
66+
request.Version = HttpVersion.Version11;
67+
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
68+
var response1 = await client.SendAsync(request);
69+
response1.EnsureSuccessStatusCode();
70+
Assert.Equal("HTTP/1.1", await response1.Content.ReadAsStringAsync());
71+
Assert.Equal(altsvc, response1.Headers.GetValues(HeaderNames.AltSvc).SingleOrDefault());
72+
73+
// Second request is HTTP/3
74+
var response3 = await client.GetAsync(address);
75+
Assert.Equal(HttpVersion.Version30, response3.Version);
76+
Assert.Equal("HTTP/3", await response3.Content.ReadAsStringAsync());
77+
}
78+
79+
[ConditionalFact]
80+
public async Task Http3_AltSvcHeader_UpgradeFromHttp2()
81+
{
82+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_AltSvcHeader_UpgradeFromHttp2";
83+
84+
var altsvc = $@"h3="":{new Uri(address).Port}""";
85+
using var client = SetUpClient();
86+
client.DefaultRequestVersion = HttpVersion.Version20;
87+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
88+
89+
// First request is HTTP/2, gets an alt-svc response
90+
var response2 = await client.GetAsync(address);
91+
response2.EnsureSuccessStatusCode();
92+
Assert.Equal(altsvc, response2.Headers.GetValues(HeaderNames.AltSvc).SingleOrDefault());
93+
Assert.Equal("HTTP/2", await response2.Content.ReadAsStringAsync());
94+
95+
// Second request is HTTP/3
96+
var response3 = await client.GetStringAsync(address);
97+
Assert.Equal("HTTP/3", response3);
98+
}
99+
100+
[ConditionalFact]
101+
public async Task Http3_ResponseTrailers()
102+
{
103+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResponseTrailers";
104+
using var client = SetUpClient();
105+
client.DefaultRequestVersion = HttpVersion.Version30;
106+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
107+
var response = await client.GetAsync(address);
108+
response.EnsureSuccessStatusCode();
109+
var result = await response.Content.ReadAsStringAsync();
110+
Assert.Equal("HTTP/3", result);
111+
Assert.Equal("value", response.TrailingHeaders.GetValues("custom").SingleOrDefault());
112+
}
113+
114+
[ConditionalFact]
115+
public async Task Http3_ResetBeforeHeaders()
116+
{
117+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResetBeforeHeaders";
118+
using var client = SetUpClient();
119+
client.DefaultRequestVersion = HttpVersion.Version30;
120+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
121+
var ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(address));
122+
var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException);
123+
Assert.Equal(0x010b, qex.ErrorCode);
124+
}
125+
126+
[ConditionalFact]
127+
public async Task Http3_ResetAfterHeaders()
128+
{
129+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResetAfterHeaders";
130+
using var client = SetUpClient();
131+
client.DefaultRequestVersion = HttpVersion.Version30;
132+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
133+
var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
134+
await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_ResetAfterHeaders_SetResult");
135+
response.EnsureSuccessStatusCode();
136+
var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync());
137+
var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException?.InnerException?.InnerException);
138+
Assert.Equal(0x010c, qex.ErrorCode); // H3_REQUEST_CANCELLED
139+
}
140+
141+
[ConditionalFact]
142+
public async Task Http3_AppExceptionAfterHeaders_InternalError()
143+
{
144+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_AppExceptionAfterHeaders_InternalError";
145+
using var client = SetUpClient();
146+
client.DefaultRequestVersion = HttpVersion.Version30;
147+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
148+
149+
var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
150+
await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_AppExceptionAfterHeaders_InternalError_SetResult");
151+
response.EnsureSuccessStatusCode();
152+
var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync());
153+
var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException?.InnerException?.InnerException);
154+
Assert.Equal(0x0102, qex.ErrorCode); // H3_INTERNAL_ERROR
155+
}
156+
157+
[ConditionalFact]
158+
public async Task Http3_Abort_Cancel()
159+
{
160+
var address = Fixture.Client.BaseAddress.ToString() + "Http3_Abort_Cancel";
161+
using var client = SetUpClient();
162+
client.DefaultRequestVersion = HttpVersion.Version30;
163+
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
164+
165+
var ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(address));
166+
var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException);
167+
Assert.Equal(0x010c, qex.ErrorCode); // H3_REQUEST_CANCELLED
168+
}
169+
170+
private HttpClient SetUpClient()
171+
{
172+
var handler = new HttpClientHandler();
173+
// Needed on CI, the IIS Express cert we use isn't trusted there.
174+
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
175+
return new HttpClient(handler);
176+
}
177+
}
178+
}

src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111

1212
<Import Project="../FunctionalTest.props" />
1313

14+
<ItemGroup>
15+
<!-- Required for QUIC & HTTP/3 in .NET 6 - https://github.com/dotnet/runtime/pull/55332 -->
16+
<RuntimeHostConfigurationOption Include="System.Net.SocketsHttpHandler.Http3Support" Value="true" />
17+
</ItemGroup>
18+
1419
<ItemGroup>
1520
<ProjectReference Include="..\testassets\IIS.Common.TestLib\IIS.Common.TestLib.csproj" />
1621
<ProjectReference Include="..\testassets\InProcessWebSite\InProcessWebSite.csproj">
@@ -29,6 +34,8 @@
2934
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared\" />
3035
<Compile Remove="$(SharedSourceRoot)ServerInfrastructure\DuplexPipe.cs" />
3136
<Compile Include="$(SharedSourceRoot)TaskToApm.cs" Link="Shared\TaskToApm.cs" />
37+
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="Shared\" />
38+
<Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\HttpSysHttp3SupportedAttribute.cs" LinkBase="shared\" />
3239
</ItemGroup>
3340

3441
<ItemGroup>

src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs

+105
Original file line numberDiff line numberDiff line change
@@ -1550,6 +1550,111 @@ public Task OnCompletedThrows(HttpContext httpContext)
15501550
return Task.CompletedTask;
15511551
}
15521552

1553+
public Task Http3_Direct(HttpContext context)
1554+
{
1555+
try
1556+
{
1557+
Assert.True(context.Request.IsHttps);
1558+
return context.Response.WriteAsync(context.Request.Protocol);
1559+
}
1560+
catch (Exception ex)
1561+
{
1562+
return context.Response.WriteAsync(ex.ToString());
1563+
}
1564+
}
1565+
1566+
public Task Http3_AltSvcHeader_UpgradeFromHttp1(HttpContext context)
1567+
{
1568+
var altsvc = $@"h3="":{context.Connection.LocalPort}""";
1569+
try
1570+
{
1571+
Assert.True(context.Request.IsHttps);
1572+
context.Response.Headers.AltSvc = altsvc;
1573+
return context.Response.WriteAsync(context.Request.Protocol);
1574+
}
1575+
catch (Exception ex)
1576+
{
1577+
return context.Response.WriteAsync(ex.ToString());
1578+
}
1579+
}
1580+
1581+
public Task Http3_AltSvcHeader_UpgradeFromHttp2(HttpContext context)
1582+
{
1583+
return Http3_AltSvcHeader_UpgradeFromHttp1(context);
1584+
}
1585+
1586+
public async Task Http3_ResponseTrailers(HttpContext context)
1587+
{
1588+
try
1589+
{
1590+
Assert.True(context.Request.IsHttps);
1591+
await context.Response.WriteAsync(context.Request.Protocol);
1592+
context.Response.AppendTrailer("custom", "value");
1593+
}
1594+
catch (Exception ex)
1595+
{
1596+
await context.Response.WriteAsync(ex.ToString());
1597+
}
1598+
}
1599+
1600+
public Task Http3_ResetBeforeHeaders(HttpContext context)
1601+
{
1602+
try
1603+
{
1604+
Assert.True(context.Request.IsHttps);
1605+
context.Features.Get<IHttpResetFeature>().Reset(0x010b); // H3_REQUEST_REJECTED
1606+
return Task.CompletedTask;
1607+
}
1608+
catch (Exception ex)
1609+
{
1610+
return context.Response.WriteAsync(ex.ToString());
1611+
}
1612+
}
1613+
1614+
private TaskCompletionSource _http3_ResetAfterHeadersCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
1615+
1616+
public async Task Http3_ResetAfterHeaders(HttpContext context)
1617+
{
1618+
try
1619+
{
1620+
Assert.True(context.Request.IsHttps);
1621+
await context.Response.Body.FlushAsync();
1622+
await _http3_ResetAfterHeadersCts.Task;
1623+
context.Features.Get<IHttpResetFeature>().Reset(0x010c); // H3_REQUEST_CANCELLED
1624+
}
1625+
catch (Exception ex)
1626+
{
1627+
await context.Response.WriteAsync(ex.ToString());
1628+
}
1629+
}
1630+
1631+
public Task Http3_ResetAfterHeaders_SetResult(HttpContext context)
1632+
{
1633+
_http3_ResetAfterHeadersCts.SetResult();
1634+
return Task.CompletedTask;
1635+
}
1636+
1637+
private TaskCompletionSource _http3_AppExceptionAfterHeaders_InternalErrorCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
1638+
1639+
public async Task Http3_AppExceptionAfterHeaders_InternalError(HttpContext context)
1640+
{
1641+
await context.Response.Body.FlushAsync();
1642+
await _http3_AppExceptionAfterHeaders_InternalErrorCts.Task;
1643+
throw new Exception("App Exception");
1644+
}
1645+
1646+
public Task Http3_AppExceptionAfterHeaders_InternalError_SetResult(HttpContext context)
1647+
{
1648+
_http3_AppExceptionAfterHeaders_InternalErrorCts.SetResult();
1649+
return Task.CompletedTask;
1650+
}
1651+
1652+
public Task Http3_Abort_Cancel(HttpContext context)
1653+
{
1654+
context.Abort();
1655+
return Task.CompletedTask;
1656+
}
1657+
15531658
internal static readonly HashSet<(string, StringValues, StringValues)> NullTrailers = new HashSet<(string, StringValues, StringValues)>()
15541659
{
15551660
("NullString", (string)null, (string)null),

src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs renamed to src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33

44
using System;
55
using System.Net.Quic;
6-
using Microsoft.AspNetCore.Testing;
76
using Microsoft.Win32;
87

9-
namespace Microsoft.AspNetCore.Server.HttpSys
8+
namespace Microsoft.AspNetCore.Testing
109
{
1110
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
12-
public class Http3SupportedAttribute : Attribute, ITestCondition
11+
public class HttpSysHttp3SupportedAttribute : Attribute, ITestCondition
1312
{
1413
// We have the same OS and TLS version requirements as MsQuic so check that first.
1514
public bool IsMet => QuicImplementationProviders.MsQuic.IsSupported && IsRegKeySet;

0 commit comments

Comments
 (0)