diff --git a/NuGet.config b/NuGet.config
index 1c2f27eb90ce..a972af091630 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -6,8 +6,10 @@
+
+
@@ -28,8 +30,10 @@
+
+
diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props
index af5cc98a0d47..f1b0e807fea1 100644
--- a/eng/Baseline.Designer.props
+++ b/eng/Baseline.Designer.props
@@ -2,117 +2,117 @@
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
-
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
@@ -120,138 +120,138 @@
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
-
-
-
+
+
+
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
-
+
+
@@ -259,7 +259,7 @@
- 8.0.13
+ 8.0.15
@@ -268,51 +268,51 @@
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
-
+
+
@@ -322,8 +322,8 @@
-
-
+
+
@@ -331,8 +331,8 @@
-
-
+
+
@@ -343,58 +343,58 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
@@ -403,7 +403,7 @@
- 8.0.13
+ 8.0.15
@@ -411,71 +411,71 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
-
-
+
+
- 8.0.13
+ 8.0.15
@@ -491,27 +491,27 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
@@ -520,23 +520,23 @@
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
@@ -545,54 +545,54 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
-
+
+
-
-
+
+
-
-
+
+
- 8.0.13
+ 8.0.15
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
@@ -600,83 +600,83 @@
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
-
-
-
+
+
+
+
- 8.0.13
+ 8.0.15
@@ -685,64 +685,64 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
@@ -764,7 +764,7 @@
- 8.0.13
+ 8.0.15
@@ -786,7 +786,7 @@
- 8.0.13
+ 8.0.15
@@ -802,23 +802,23 @@
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
@@ -826,24 +826,24 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
-
-
+
+
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
@@ -853,7 +853,7 @@
- 8.0.13
+ 8.0.15
@@ -862,73 +862,73 @@
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
-
+
-
+
-
+
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
@@ -957,11 +957,11 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
@@ -979,18 +979,18 @@
- 8.0.13
+ 8.0.15
- 8.0.13
+ 8.0.15
-
+
- 8.0.13
+ 8.0.15
diff --git a/eng/Baseline.xml b/eng/Baseline.xml
index 9efbb290ef60..9746528d4365 100644
--- a/eng/Baseline.xml
+++ b/eng/Baseline.xml
@@ -4,110 +4,110 @@ This file contains a list of all the packages and their versions which were rele
Update this list when preparing for a new patch.
-->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index f67573e8e603..5a8d0360c7e2 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -9,37 +9,37 @@
-->
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- d00955545e8afc997726aead9b0e6103b1ceade6
+ 0118cb6810a48869bf7494aabd86ef44da5940a3
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
@@ -121,9 +121,9 @@
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
5535e31a712343a63f5d7d796cd874e563e5ac14
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
@@ -139,7 +139,7 @@
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
@@ -185,9 +185,9 @@
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
5535e31a712343a63f5d7d796cd874e563e5ac14
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://github.com/dotnet/source-build-externals
@@ -207,13 +207,13 @@
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
2d7eea252964e69be94cb9c847b371b23e4dd470
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 81cabf2857a01351e5ab578947c7403a5b128ad1
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 5535e31a712343a63f5d7d796cd874e563e5ac14
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
@@ -275,17 +275,17 @@
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
81cabf2857a01351e5ab578947c7403a5b128ad1
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
@@ -316,22 +316,22 @@
Win-x64 is used here because we have picked an arbitrary runtime identifier to flow the version of the latest NETCore.App runtime.
All Runtime.$rid packages should have the same version.
-->
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://github.com/dotnet/xdt
@@ -368,9 +368,9 @@
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 1584e493603cfc4e9b36b77d6d4afe97de6363f9
+ 50c4cb9fc31c47f03eac865d7bc518af173b74b7
https://github.com/dotnet/winforms
diff --git a/eng/Versions.props b/eng/Versions.props
index 400fb877670e..5f7b7b1220b7 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -11,7 +11,7 @@
16
- false
+ true
7.1.2
7.*
8.0.2
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14-servicing.25111.18
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15-servicing.25164.13
8.0.0
8.0.1
8.0.0
@@ -93,7 +93,7 @@
8.0.0
8.0.0
8.0.0
- 8.0.14-servicing.25111.18
+ 8.0.15-servicing.25164.13
8.0.1
8.0.1
8.0.1
@@ -109,11 +109,11 @@
8.0.0
8.0.2
8.0.0
- 8.0.14-servicing.25111.18
+ 8.0.15-servicing.25164.13
8.0.1
8.0.1
- 8.0.1
- 8.0.0
+ 8.0.2
+ 8.0.1
8.0.0-rtm.23520.14
8.0.0
8.0.1
@@ -129,9 +129,9 @@
8.0.0
8.0.0
8.0.0
- 8.0.14-servicing.25111.18
+ 8.0.15-servicing.25164.13
- 8.0.14-servicing.25111.18
+ 8.0.15-servicing.25164.13
8.0.0
8.0.1
@@ -143,14 +143,14 @@
8.1.0-preview.23604.1
8.1.0-preview.23604.1
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
- 8.0.14
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
+ 8.0.15
4.8.0-7.24574.2
4.8.0-7.24574.2
diff --git a/global.json b/global.json
index 1ea9bd10bd84..dfea7ae6dee6 100644
--- a/global.json
+++ b/global.json
@@ -1,9 +1,9 @@
{
"sdk": {
- "version": "8.0.113"
+ "version": "8.0.114"
},
"tools": {
- "dotnet": "8.0.113",
+ "dotnet": "8.0.114",
"runtimes": {
"dotnet/x86": [
"$(MicrosoftNETCoreBrowserDebugHostTransportVersion)"
diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 2c7c3e8ef18d..68908731bf54 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -734,4 +734,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
Call UseKestrelHttpsConfiguration() on IWebHostBuilder to automatically enable HTTPS when an https:// address is used.
-
+
+ The client sent a {frameType} frame to a control stream that was too large.
+
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/Http3Limits.cs b/src/Servers/Kestrel/Core/src/Http3Limits.cs
index 0d7801e48bf8..b6556557a340 100644
--- a/src/Servers/Kestrel/Core/src/Http3Limits.cs
+++ b/src/Servers/Kestrel/Core/src/Http3Limits.cs
@@ -37,7 +37,7 @@ internal int HeaderTableSize
///
/// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations.
///
- /// Value must be greater than 0, defaults to 2^14 (16,384).
+ /// Value must be greater than 0, defaults to 2^15 (32,768).
///
///
public int MaxRequestHeaderFieldSize
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs
index 95dbbcb8e4d5..ce1e9b0db815 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs
@@ -7,7 +7,7 @@ internal partial class Http3RawFrame
{
public void PrepareData()
{
- Length = 0;
+ RemainingLength = 0;
Type = Http3FrameType.Data;
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs
index fe2eb3a6e42e..de1a73cb830e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs
@@ -7,7 +7,7 @@ internal partial class Http3RawFrame
{
public void PrepareGoAway()
{
- Length = 0;
+ RemainingLength = 0;
Type = Http3FrameType.GoAway;
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs
index bcf65929694d..11e8c971ff21 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs
@@ -7,7 +7,7 @@ internal partial class Http3RawFrame
{
public void PrepareHeaders()
{
- Length = 0;
+ RemainingLength = 0;
Type = Http3FrameType.Headers;
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs
index 9e74e07db5b8..03ed2a670250 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs
@@ -7,7 +7,7 @@ internal partial class Http3RawFrame
{
public void PrepareSettings()
{
- Length = 0;
+ RemainingLength = 0;
Type = Http3FrameType.Settings;
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs
index 076b9640d0bb..5839d515524c 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs
@@ -9,7 +9,7 @@ namespace System.Net.Http;
internal partial class Http3RawFrame
#pragma warning restore CA1852 // Seal internal types
{
- public long Length { get; set; }
+ public long RemainingLength { get; set; }
public Http3FrameType Type { get; internal set; }
@@ -17,6 +17,6 @@ internal partial class Http3RawFrame
public override string ToString()
{
- return $"{FormattedType} Length: {Length}";
+ return $"{FormattedType} Length: {RemainingLength}";
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
index 599a55f50212..5bb9452a5da1 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
+using System.Diagnostics;
using System.Globalization;
using System.IO.Pipelines;
using System.Net.Http;
@@ -19,13 +20,18 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
private const int EncoderStreamTypeId = 2;
private const int DecoderStreamTypeId = 3;
+ // Arbitrarily chosen max frame length
+ // ControlStream frames currently are very small, either a single variable length integer (max 8 bytes), two variable length integers,
+ // or in the case of SETTINGS a small collection of two variable length integers
+ // We'll use a generous value of 10k in case new optional frame(s) are added that might be a little larger than the current frames.
+ private const int MaxFrameSize = 10_000;
+
private readonly Http3FrameWriter _frameWriter;
private readonly Http3StreamContext _context;
private readonly Http3PeerSettings _serverPeerSettings;
private readonly IStreamIdFeature _streamIdFeature;
private readonly IStreamClosedFeature _streamClosedFeature;
private readonly IProtocolErrorCodeFeature _errorCodeFeature;
- private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
private volatile int _isClosed;
private long _headerType;
private readonly object _completionLock = new();
@@ -159,9 +165,9 @@ private async ValueTask TryReadStreamHeaderAsync()
{
if (!readableBuffer.IsEmpty)
{
- var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined);
- if (id != -1)
+ if (VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var id))
{
+ examined = consumed;
return id;
}
}
@@ -240,6 +246,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli
}
finally
{
+ await _context.StreamContext.DisposeAsync();
+
ApplyCompletionFlag(StreamCompletionFlags.Completed);
_context.StreamLifetimeHandler.OnStreamCompleted(this);
}
@@ -247,6 +255,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli
private async Task HandleControlStream()
{
+ var incomingFrame = new Http3RawFrame();
+ var isContinuedFrame = false;
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
@@ -259,12 +269,33 @@ private async Task HandleControlStream()
if (!readableBuffer.IsEmpty)
{
// need to kick off httpprotocol process request async here.
- while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, out var framePayload))
+ while (Http3FrameReader.TryReadFrame(ref readableBuffer, incomingFrame, isContinuedFrame, out var framePayload))
{
- Log.Http3FrameReceived(_context.ConnectionId, _streamIdFeature.StreamId, _incomingFrame);
-
- consumed = examined = framePayload.End;
- await ProcessHttp3ControlStream(framePayload);
+ Debug.Assert(incomingFrame.RemainingLength >= framePayload.Length);
+
+ // Only log when parsing the beginning of the frame
+ if (!isContinuedFrame)
+ {
+ Log.Http3FrameReceived(_context.ConnectionId, _streamIdFeature.StreamId, incomingFrame);
+ }
+
+ examined = framePayload.End;
+ await ProcessHttp3ControlStream(incomingFrame, isContinuedFrame, framePayload, out consumed);
+
+ if (incomingFrame.RemainingLength == framePayload.Length)
+ {
+ Debug.Assert(framePayload.Slice(0, consumed).Length == framePayload.Length);
+
+ incomingFrame.RemainingLength = 0;
+ isContinuedFrame = false;
+ }
+ else
+ {
+ incomingFrame.RemainingLength -= framePayload.Slice(0, consumed).Length;
+ isContinuedFrame = true;
+
+ Debug.Assert(incomingFrame.RemainingLength > 0);
+ }
}
}
@@ -294,56 +325,71 @@ private async ValueTask HandleEncodingDecodingTask()
}
}
- private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence payload)
+ private ValueTask ProcessHttp3ControlStream(Http3RawFrame incomingFrame, bool isContinuedFrame, in ReadOnlySequence payload, out SequencePosition consumed)
{
- switch (_incomingFrame.Type)
+ // default to consuming the entire payload, this is so that we don't need to set consumed from all the frame types that aren't implemented yet.
+ // individual frame types can set consumed if they're implemented and want to be able to partially consume the payload.
+ consumed = payload.End;
+ switch (incomingFrame.Type)
{
case Http3FrameType.Data:
case Http3FrameType.Headers:
case Http3FrameType.PushPromise:
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2
- throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame);
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-8.1-2.12.1
+ throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame);
case Http3FrameType.Settings:
- return ProcessSettingsFrameAsync(payload);
+ CheckMaxFrameSize(incomingFrame);
+ return ProcessSettingsFrameAsync(isContinuedFrame, payload, out consumed);
case Http3FrameType.GoAway:
- return ProcessGoAwayFrameAsync();
+ return ProcessGoAwayFrameAsync(isContinuedFrame, incomingFrame, payload, out consumed);
case Http3FrameType.CancelPush:
- return ProcessCancelPushFrameAsync();
+ return ProcessCancelPushFrameAsync(incomingFrame, payload, out consumed);
case Http3FrameType.MaxPushId:
- return ProcessMaxPushIdFrameAsync();
+ return ProcessMaxPushIdFrameAsync(incomingFrame, payload, out consumed);
default:
- return ProcessUnknownFrameAsync(_incomingFrame.Type);
+ CheckMaxFrameSize(incomingFrame);
+ return ProcessUnknownFrameAsync(incomingFrame.Type);
}
- }
- private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence payload)
- {
- if (_haveReceivedSettingsFrame)
+ static void CheckMaxFrameSize(Http3RawFrame http3RawFrame)
{
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings
- throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame);
+ // Not part of the RFC, but it's a good idea to limit the size of frames when we know they're supposed to be small.
+ if (http3RawFrame.RemainingLength >= MaxFrameSize)
+ {
+ throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamFrameTooLarge(http3RawFrame.FormattedType), Http3ErrorCode.FrameError);
+ }
}
+ }
- _haveReceivedSettingsFrame = true;
- _streamClosedFeature.OnClosed(static state =>
+ private ValueTask ProcessSettingsFrameAsync(bool isContinuedFrame, ReadOnlySequence payload, out SequencePosition consumed)
+ {
+ if (!isContinuedFrame)
{
- var stream = (Http3ControlStream)state!;
- stream.OnStreamClosed();
- }, this);
+ if (_haveReceivedSettingsFrame)
+ {
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4
+ throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame);
+ }
+
+ _haveReceivedSettingsFrame = true;
+ _streamClosedFeature.OnClosed(static state =>
+ {
+ var stream = (Http3ControlStream)state!;
+ stream.OnStreamClosed();
+ }, this);
+ }
while (true)
{
- var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _);
- if (id == -1)
+ if (!VariableLengthIntegerHelper.TryGetInteger(payload, out consumed, out var id))
{
break;
}
- payload = payload.Slice(consumed);
-
- var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _);
- if (value == -1)
+ if (!VariableLengthIntegerHelper.TryGetInteger(payload.Slice(consumed), out consumed, out var value))
{
+ // Reset consumed to very start even though we successfully read 1 varint. It's because we want to keep the id for when we have the value as well.
+ consumed = payload.Start;
break;
}
@@ -382,37 +428,48 @@ private void ProcessSetting(long id, long value)
}
}
- private ValueTask ProcessGoAwayFrameAsync()
+ private ValueTask ProcessGoAwayFrameAsync(bool isContinuedFrame, Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed)
{
- EnsureSettingsFrame(Http3FrameType.GoAway);
+ // https://www.rfc-editor.org/rfc/rfc9114.html#name-goaway
+
+ // We've already triggered RequestClose since isContinuedFrame is only true
+ // after we've already parsed the frame type and called the processing function at least once.
+ if (!isContinuedFrame)
+ {
+ EnsureSettingsFrame(Http3FrameType.GoAway);
- // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated.
- _context.Connection.StopProcessingNextRequest(serverInitiated: false);
- _context.ConnectionContext.Features.Get()?.RequestClose();
+ // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated.
+ _context.Connection.StopProcessingNextRequest(serverInitiated: false);
+ _context.ConnectionContext.Features.Get()?.RequestClose();
+ }
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-goaway
- // PUSH is not implemented so nothing to do.
+ // PUSH is not implemented but we still want to parse the frame to do error checking
+ ParseVarIntWithFrameLengthValidation(incomingFrame, payload, out consumed);
// TODO: Double check the connection remains open.
return default;
}
- private ValueTask ProcessCancelPushFrameAsync()
+ private ValueTask ProcessCancelPushFrameAsync(Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed)
{
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3
+
EnsureSettingsFrame(Http3FrameType.CancelPush);
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push
- // PUSH is not implemented so nothing to do.
+ // PUSH is not implemented but we still want to parse the frame to do error checking
+ ParseVarIntWithFrameLengthValidation(incomingFrame, payload, out consumed);
return default;
}
- private ValueTask ProcessMaxPushIdFrameAsync()
+ private ValueTask ProcessMaxPushIdFrameAsync(Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed)
{
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.7
+
EnsureSettingsFrame(Http3FrameType.MaxPushId);
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push
- // PUSH is not implemented so nothing to do.
+ // PUSH is not implemented but we still want to parse the frame to do error checking
+ ParseVarIntWithFrameLengthValidation(incomingFrame, payload, out consumed);
return default;
}
@@ -426,6 +483,23 @@ private ValueTask ProcessUnknownFrameAsync(Http3FrameType frameType)
return default;
}
+ // Used for frame types that aren't (fully) implemented yet and contain a single var int as part of their framing. (CancelPush, MaxPushId, GoAway)
+ // We want to throw an error if the length field of the frame is larger than the spec defined format of the frame.
+ private static void ParseVarIntWithFrameLengthValidation(Http3RawFrame incomingFrame, ReadOnlySequence payload, out SequencePosition consumed)
+ {
+ if (!VariableLengthIntegerHelper.TryGetInteger(payload, out consumed, out _))
+ {
+ return;
+ }
+
+ if (incomingFrame.RemainingLength > payload.Slice(0, consumed).Length)
+ {
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-10.8
+ // An implementation MUST ensure that the length of a frame exactly matches the length of the fields it contains.
+ throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamFrameTooLarge(Http3Formatting.ToFormattedType(incomingFrame.Type)), Http3ErrorCode.FrameError);
+ }
+ }
+
private void EnsureSettingsFrame(Http3FrameType frameType)
{
if (!_haveReceivedSettingsFrame)
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs
index 66740c710f10..2de0472483a1 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs
@@ -19,36 +19,44 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
| Frame Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
- internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3RawFrame frame, out ReadOnlySequence framePayload)
+ // Reads and returns partial frames, don't rely on the frame being complete when using this method
+ // Set isContinuedFrame to true when expecting to read more of the previous frame
+ internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3RawFrame frame, bool isContinuedFrame, out ReadOnlySequence framePayload)
{
framePayload = ReadOnlySequence.Empty;
- SequencePosition consumed;
+ SequencePosition consumed = readableBuffer.Start;
+ var length = frame.RemainingLength;
- var type = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out _);
- if (type == -1)
+ if (!isContinuedFrame)
{
- return false;
- }
+ if (!VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var type))
+ {
+ return false;
+ }
- var firstLengthBuffer = readableBuffer.Slice(consumed);
+ var firstLengthBuffer = readableBuffer.Slice(consumed);
- var length = VariableLengthIntegerHelper.GetInteger(firstLengthBuffer, out consumed, out _);
+ if (!VariableLengthIntegerHelper.TryGetInteger(firstLengthBuffer, out consumed, out length))
+ {
+ return false;
+ }
- // Make sure the whole frame is buffered
- if (length == -1)
- {
- return false;
+ frame.RemainingLength = length;
+ frame.Type = (Http3FrameType)type;
}
var startOfFramePayload = readableBuffer.Slice(consumed);
- if (startOfFramePayload.Length < length)
+
+ // Get all the available bytes or the rest of the frame whichever is less
+ length = Math.Min(startOfFramePayload.Length, length);
+
+ // If we were expecting a non-empty payload, but haven't received any of it yet,
+ // there is nothing to process until we wait for more data.
+ if (length == 0 && frame.RemainingLength != 0)
{
return false;
}
- frame.Length = length;
- frame.Type = (Http3FrameType)type;
-
// The remaining payload minus the extra fields
framePayload = startOfFramePayload.Slice(0, length);
readableBuffer = readableBuffer.Slice(framePayload.End);
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs
index dbcd774af4d6..44034fa365f2 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs
@@ -121,7 +121,7 @@ internal Task WriteSettingsAsync(List settings)
WriteSettings(settings, buffer);
// Advance pipe writer and flush
- _outgoingFrame.Length = totalLength;
+ _outgoingFrame.RemainingLength = totalLength;
_outputWriter.Advance(totalLength);
return _outputWriter.FlushAsync().GetAsTask();
@@ -186,7 +186,7 @@ private void WriteDataUnsynchronized(in ReadOnlySequence data, long dataLe
return;
}
- _outgoingFrame.Length = (int)dataLength;
+ _outgoingFrame.RemainingLength = (int)dataLength;
WriteHeaderUnsynchronized();
@@ -209,7 +209,7 @@ void SplitAndWriteDataUnsynchronized(in ReadOnlySequence data, long dataLe
do
{
var currentData = remainingData.Slice(0, dataPayloadLength);
- _outgoingFrame.Length = dataPayloadLength;
+ _outgoingFrame.RemainingLength = dataPayloadLength;
WriteHeaderUnsynchronized();
@@ -223,7 +223,7 @@ void SplitAndWriteDataUnsynchronized(in ReadOnlySequence data, long dataLe
} while (dataLength > dataPayloadLength);
- _outgoingFrame.Length = (int)dataLength;
+ _outgoingFrame.RemainingLength = (int)dataLength;
WriteHeaderUnsynchronized();
@@ -240,7 +240,7 @@ internal ValueTask WriteGoAway(long id)
var length = VariableLengthIntegerHelper.GetByteCount(id);
- _outgoingFrame.Length = length;
+ _outgoingFrame.RemainingLength = length;
WriteHeaderUnsynchronized();
@@ -253,10 +253,10 @@ internal ValueTask WriteGoAway(long id)
private void WriteHeaderUnsynchronized()
{
_log.Http3FrameSending(_connectionId, _streamIdFeature.StreamId, _outgoingFrame);
- var headerLength = WriteHeader(_outgoingFrame.Type, _outgoingFrame.Length, _outputWriter);
+ var headerLength = WriteHeader(_outgoingFrame.Type, _outgoingFrame.RemainingLength, _outputWriter);
// We assume the payload will be written prior to the next flush.
- _unflushedBytes += headerLength + _outgoingFrame.Length;
+ _unflushedBytes += headerLength + _outgoingFrame.RemainingLength;
}
public ValueTask Write100ContinueAsync()
@@ -269,7 +269,7 @@ public ValueTask Write100ContinueAsync()
}
_outgoingFrame.PrepareHeaders();
- _outgoingFrame.Length = ContinueBytes.Length;
+ _outgoingFrame.RemainingLength = ContinueBytes.Length;
WriteHeaderUnsynchronized();
_outputWriter.Write(ContinueBytes);
return TimeFlushUnsynchronizedAsync();
@@ -394,7 +394,7 @@ private void FinishWritingHeaders(int payloadLength, bool done)
ValidateHeadersTotalSize();
- _outgoingFrame.Length = _headerEncodingBuffer.WrittenCount;
+ _outgoingFrame.RemainingLength = _headerEncodingBuffer.WrittenCount;
WriteHeaderUnsynchronized();
_outputWriter.Write(_headerEncodingBuffer.WrittenSpan);
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs
index b6db0bb810db..7dabb9654c56 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs
@@ -60,8 +60,7 @@ public async ValueTask ReadNextStreamHeaderAsync(Http3StreamContext contex
if (!readableBuffer.IsEmpty)
{
- var value = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out _);
- if (value != -1)
+ if (VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var value))
{
if (!advanceOn.HasValue || value == (long)advanceOn)
{
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 61625f180cd2..17178ffbf133 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -59,7 +59,6 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
private readonly object _completionLock = new();
protected RequestHeaderParsingState _requestHeaderParsingState;
- protected readonly Http3RawFrame _incomingFrame = new();
public bool EndStreamReceived => (_completionState & StreamCompletionFlags.EndStreamReceived) == StreamCompletionFlags.EndStreamReceived;
public bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted;
@@ -607,6 +606,8 @@ public async Task ProcessRequestAsync(IHttpApplication appli
try
{
+ var incomingFrame = new Http3RawFrame();
+ var isContinuedFrame = false;
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
@@ -618,12 +619,19 @@ public async Task ProcessRequestAsync(IHttpApplication appli
{
if (!readableBuffer.IsEmpty)
{
- while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, out var framePayload))
+ while (Http3FrameReader.TryReadFrame(ref readableBuffer, incomingFrame, isContinuedFrame, out var framePayload))
{
- Log.Http3FrameReceived(ConnectionId, _streamIdFeature.StreamId, _incomingFrame);
+ // Only log when parsing the beginning of the frame
+ if (!isContinuedFrame)
+ {
+ Log.Http3FrameReceived(ConnectionId, _streamIdFeature.StreamId, incomingFrame);
+ }
consumed = examined = framePayload.End;
- await ProcessHttp3Stream(application, framePayload, result.IsCompleted && readableBuffer.IsEmpty);
+ await ProcessHttp3Stream(application, incomingFrame, isContinuedFrame, framePayload, result.IsCompleted && readableBuffer.IsEmpty);
+
+ incomingFrame.RemainingLength -= framePayload.Length;
+ isContinuedFrame = incomingFrame.RemainingLength > 0 ? true : false;
}
}
@@ -746,22 +754,23 @@ private ValueTask OnEndStreamReceived()
return RequestBodyPipe.Writer.CompleteAsync();
}
- private Task ProcessHttp3Stream(IHttpApplication application, in ReadOnlySequence payload, bool isCompleted) where TContext : notnull
+ private Task ProcessHttp3Stream(IHttpApplication application, Http3RawFrame incomingFrame, bool isContinuedFrame,
+ in ReadOnlySequence payload, bool isCompleted) where TContext : notnull
{
- return _incomingFrame.Type switch
+ return incomingFrame.Type switch
{
Http3FrameType.Data => ProcessDataFrameAsync(payload),
- Http3FrameType.Headers => ProcessHeadersFrameAsync(application, payload, isCompleted),
+ Http3FrameType.Headers => ProcessHeadersFrameAsync(application, incomingFrame, isContinuedFrame, payload, isCompleted),
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4
// These frames need to be on a control stream
Http3FrameType.Settings or
Http3FrameType.CancelPush or
Http3FrameType.GoAway or
Http3FrameType.MaxPushId => throw new Http3ConnectionErrorException(
- CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame),
+ CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame),
// The server should never receive push promise
Http3FrameType.PushPromise => throw new Http3ConnectionErrorException(
- CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame),
+ CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame),
_ => ProcessUnknownFrameAsync(),
};
}
@@ -773,11 +782,13 @@ private static Task ProcessUnknownFrameAsync()
return Task.CompletedTask;
}
- private async Task ProcessHeadersFrameAsync(IHttpApplication application, ReadOnlySequence payload, bool isCompleted) where TContext : notnull
+ private async Task ProcessHeadersFrameAsync(IHttpApplication application, Http3RawFrame incomingFrame, bool isContinuedFrame,
+ ReadOnlySequence payload, bool isCompleted) where TContext : notnull
{
// HEADERS frame after trailing headers is invalid.
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1
- if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
+ // Since we parse data as we get it, we can receive partial frames which means we need to check that we're in the middle of handling the trailers header frame
+ if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers && !isContinuedFrame)
{
throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Headers)), Http3ErrorCode.UnexpectedFrame);
}
@@ -789,8 +800,17 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication
try
{
- QPackDecoder.Decode(payload, endHeaders: true, handler: this);
- QPackDecoder.Reset();
+ var endHeaders = payload.Length == incomingFrame.RemainingLength;
+ QPackDecoder.Decode(payload, endHeaders, handler: this);
+ if (endHeaders)
+ {
+ QPackDecoder.Reset();
+ }
+ else
+ {
+ // Headers frame isn't complete, return to read more of the frame
+ return;
+ }
}
catch (QPackDecodingException ex)
{
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs
index 4159c927e531..54e32f258f00 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs
@@ -37,7 +37,7 @@ public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame
{
if (_http3Logger.IsEnabled(LogLevel.Trace))
{
- Http3Log.Http3FrameReceived(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length);
+ Http3Log.Http3FrameReceived(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.RemainingLength);
}
}
@@ -45,7 +45,7 @@ public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame
{
if (_http3Logger.IsEnabled(LogLevel.Trace))
{
- Http3Log.Http3FrameSending(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length);
+ Http3Log.Http3FrameSending(_http3Logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.RemainingLength);
}
}
diff --git a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs
index 8b73bd0e2c48..f8fa53170829 100644
--- a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs
+++ b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs
@@ -14,7 +14,8 @@ public class VariableIntHelperTests
[MemberData(nameof(IntegerData))]
public void CheckDecoding(long expected, byte[] input)
{
- var decoded = VariableLengthIntegerHelper.GetInteger(new ReadOnlySequence(input), out _, out _);
+ var result = VariableLengthIntegerHelper.TryGetInteger(new ReadOnlySequence(input), out _, out var decoded);
+ Assert.True(result);
Assert.Equal(expected, decoded);
}
diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
index e02573b7b225..544b338ee38f 100644
--- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
+++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
@@ -388,15 +388,15 @@ private static long GetOutputResponseBufferSize(ServiceContext serviceContext)
return bufferSize ?? 0;
}
- internal ValueTask CreateControlStream()
+ internal ValueTask CreateControlStream(PipeScheduler clientWriterScheduler = null)
{
- return CreateControlStream(id: 0);
+ return CreateControlStream(id: 0, clientWriterScheduler);
}
- internal async ValueTask CreateControlStream(int? id)
+ internal async ValueTask CreateControlStream(int? id, PipeScheduler clientWriterScheduler = null)
{
var testStreamContext = new TestStreamContext(canRead: true, canWrite: false, this);
- testStreamContext.Initialize(streamId: 2);
+ testStreamContext.Initialize(streamId: 2, clientWriterScheduler);
var stream = new Http3ControlStream(this, testStreamContext);
_runningStreams[stream.StreamId] = stream;
@@ -409,16 +409,17 @@ internal async ValueTask CreateControlStream(int? id)
return stream;
}
- internal async ValueTask CreateRequestStream(IEnumerable> headers, Http3RequestHeaderHandler headerHandler = null, bool endStream = false, TaskCompletionSource tsc = null)
+ internal async ValueTask CreateRequestStream(IEnumerable> headers,
+ Http3RequestHeaderHandler headerHandler = null, bool endStream = false, TaskCompletionSource tsc = null, PipeScheduler clientWriterScheduler = null)
{
- var stream = CreateRequestStreamCore(headerHandler);
+ var stream = CreateRequestStreamCore(headerHandler, clientWriterScheduler);
if (tsc is not null)
{
stream.StartStreamDisposeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
}
- if (headers is not null)
+ if (headers is not null && headers.Any())
{
await stream.SendHeadersAsync(headers, endStream);
}
@@ -430,9 +431,10 @@ internal async ValueTask CreateRequestStream(IEnumerable CreateRequestStream(Http3HeadersEnumerator headers, Http3RequestHeaderHandler headerHandler = null, bool endStream = false, TaskCompletionSource tsc = null)
+ internal async ValueTask CreateRequestStream(Http3HeadersEnumerator headers, Http3RequestHeaderHandler headerHandler = null,
+ bool endStream = false, TaskCompletionSource tsc = null, PipeScheduler clientWriterScheduler = null)
{
- var stream = CreateRequestStreamCore(headerHandler);
+ var stream = CreateRequestStreamCore(headerHandler, clientWriterScheduler);
if (tsc is not null)
{
@@ -448,7 +450,7 @@ internal async ValueTask CreateRequestStream(Http3HeadersEnu
return stream;
}
- private Http3RequestStream CreateRequestStreamCore(Http3RequestHeaderHandler headerHandler)
+ private Http3RequestStream CreateRequestStreamCore(Http3RequestHeaderHandler headerHandler, PipeScheduler clientWriterScheduler)
{
var requestStreamId = GetStreamId(0x00);
if (!_streamContextPool.TryDequeue(out var testStreamContext))
@@ -459,7 +461,7 @@ private Http3RequestStream CreateRequestStreamCore(Http3RequestHeaderHandler hea
{
Logger.LogDebug($"Reusing context for request stream {requestStreamId}.");
}
- testStreamContext.Initialize(requestStreamId);
+ testStreamContext.Initialize(requestStreamId, clientWriterScheduler);
return new Http3RequestStream(this, Connection, testStreamContext, headerHandler ?? new Http3RequestHeaderHandler());
}
@@ -559,7 +561,7 @@ internal async ValueTask ReceiveFrameAsync(bool expectEnd
throw new InvalidOperationException("No data received.");
}
- if (Http3FrameReader.TryReadFrame(ref buffer, frame, out var framePayload))
+ if (Http3FrameReader.TryReadFrame(ref buffer, frame, isContinuedFrame: false, out var framePayload))
{
consumed = examined = framePayload.End;
frame.Payload = framePayload.ToArray();
@@ -837,16 +839,14 @@ internal async ValueTask> ExpectSettingsAsync()
var settings = new Dictionary();
while (true)
{
- var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _);
- if (id == -1)
+ if (!VariableLengthIntegerHelper.TryGetInteger(payload, out var consumed, out var id))
{
break;
}
payload = payload.Slice(consumed);
- var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _);
- if (value == -1)
+ if (!VariableLengthIntegerHelper.TryGetInteger(payload, out consumed, out var value))
{
break;
}
@@ -927,9 +927,9 @@ public async ValueTask TryReadStreamIdAsync()
{
if (!readableBuffer.IsEmpty)
{
- var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined);
- if (id != -1)
+ if (VariableLengthIntegerHelper.TryGetInteger(readableBuffer, out consumed, out var id))
{
+ examined = consumed;
return id;
}
}
@@ -1005,6 +1005,7 @@ public TestMultiplexedConnectionContext(Http3InMemory testBase)
Features.Set(this);
Features.Set(this);
ConnectionClosedRequested = ConnectionClosingCts.Token;
+ ConnectionClosed = ConnectionClosedCts.Token;
}
public override string ConnectionId { get; set; }
@@ -1017,6 +1018,8 @@ public TestMultiplexedConnectionContext(Http3InMemory testBase)
public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource();
+ public CancellationTokenSource ConnectionClosedCts { get; set; } = new CancellationTokenSource();
+
public long Error
{
get => _error ?? -1;
@@ -1033,6 +1036,7 @@ public override void Abort(ConnectionAbortedException abortReason)
{
ToServerAcceptQueue.Writer.TryComplete();
ToClientAcceptQueue.Writer.TryComplete();
+ ConnectionClosedCts.Cancel();
}
public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default)
@@ -1106,38 +1110,30 @@ public TestStreamContext(bool canRead, bool canWrite, Http3InMemory testBase)
_testBase = testBase;
}
- public void Initialize(long streamId)
+ public void Initialize(long streamId, PipeScheduler clientWriterScheduler = null)
{
- if (!_isComplete)
- {
- // Create new pipes when test stream context is reused rather than reseting them.
- // This is required because the client tests read from these directly from these pipes.
- // When a request is finished they'll check to see whether there is anymore content
- // in the Application.Output pipe. If it has been reset then that code will error.
- var inputOptions = Http3InMemory.GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
- var outputOptions = Http3InMemory.GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
-
- _inputPipe = new Pipe(inputOptions);
- _outputPipe = new Pipe(outputOptions);
-
- _transportPipeReader = new CompletionPipeReader(_inputPipe.Reader);
- _transportPipeWriter = new CompletionPipeWriter(_outputPipe.Writer);
-
- _pair = new DuplexPipePair(
- new DuplexPipe(_transportPipeReader, _transportPipeWriter),
- new DuplexPipe(_outputPipe.Reader, _inputPipe.Writer));
- }
- else
+ if (_isComplete)
{
_pair.Application.Input.Complete();
_pair.Application.Output.Complete();
+ }
- _transportPipeReader.Reset();
- _transportPipeWriter.Reset();
+ // Create new pipes when test stream context is reused rather than reseting them.
+ // This is required because the client tests read from these directly from these pipes.
+ // When a request is finished they'll check to see whether there is anymore content
+ // in the Application.Output pipe. If it has been reset then that code will error.
+ var inputOptions = Http3InMemory.GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, clientWriterScheduler ?? PipeScheduler.ThreadPool);
+ var outputOptions = Http3InMemory.GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
- _inputPipe.Reset();
- _outputPipe.Reset();
- }
+ _inputPipe = new Pipe(inputOptions);
+ _outputPipe = new Pipe(outputOptions);
+
+ _transportPipeReader = new CompletionPipeReader(_inputPipe.Reader);
+ _transportPipeWriter = new CompletionPipeWriter(_outputPipe.Writer);
+
+ _pair = new DuplexPipePair(
+ new DuplexPipe(_transportPipeReader, _transportPipeWriter),
+ new DuplexPipe(_outputPipe.Reader, _inputPipe.Writer));
Features.Set(this);
Features.Set(this);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs
index b1ff24b43bf8..054577365d7d 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs
@@ -354,6 +354,34 @@ await Http3Api.WaitForConnectionErrorAsync(
expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed);
}
+ [Theory]
+ [InlineData((int)Http3FrameType.Settings, 20_000)]
+ //[InlineData((int)Http3FrameType.GoAway, 30)] // GoAway frames trigger graceful connection close which races with sending FRAME_ERROR
+ [InlineData((int)Http3FrameType.CancelPush, 30)]
+ [InlineData((int)Http3FrameType.MaxPushId, 30)]
+ [InlineData(int.MaxValue, 20_000)] // Unknown frame type
+ public async Task ControlStream_ClientToServer_LargeFrame_ConnectionError(int frameType, int length)
+ {
+ await Http3Api.InitializeConnectionAsync(_noopApplication);
+
+ var controlStream = await Http3Api.CreateControlStream();
+
+ // Need to send settings frame before other frames, otherwise it's a connection error
+ if (frameType != (int)Http3FrameType.Settings)
+ {
+ await controlStream.SendSettingsAsync(new List());
+ }
+
+ await controlStream.SendFrameAsync((Http3FrameType)frameType, new byte[length]);
+
+ await Http3Api.WaitForConnectionErrorAsync(
+ ignoreNonGoAwayFrames: true,
+ expectedLastStreamId: 0,
+ expectedErrorCode: Http3ErrorCode.FrameError,
+ matchExpectedErrorMessage: AssertExpectedErrorMessages,
+ expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamFrameTooLarge(Http3Formatting.ToFormattedType((Http3FrameType)frameType)));
+ }
+
[Fact]
public async Task SETTINGS_MaxFieldSectionSizeSent_ServerReceivesValue()
{
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index 728bc3458b74..c23e11328e43 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -1,11 +1,9 @@
// 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.Buffers;
using System.Globalization;
-using System.IO;
-using System.Linq;
+using System.IO.Pipelines;
using System.Net.Http;
using System.Runtime.ExceptionServices;
using System.Text;
@@ -17,6 +15,7 @@
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
@@ -1982,7 +1981,7 @@ public async Task RequestTrailers_CanReadTrailersFromRequest()
var trailers = new[]
{
new KeyValuePair("TestName", "TestValue"),
- };
+ };
var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c =>
{
await c.Request.Body.DrainAsync(default);
@@ -2360,6 +2359,21 @@ public Task HEADERS_Received_HeaderBlockOverLimitx2_ConnectionError()
return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
}
+ [Fact]
+ public Task HEADERS_Received_HeaderValueOverLimit_ConnectionError()
+ {
+ var limit = _serviceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize;
+ // Single header value exceeds limit
+ var headers = new[]
+ {
+ new KeyValuePair("a", new string('a', limit + 1)),
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers,
+ SR.Format(SR.net_http_headers_exceeded_length, limit),
+ Http3ErrorCode.InternalError);
+ }
+
[Fact]
public async Task HEADERS_Received_TooManyHeaders_431()
{
@@ -2976,4 +2990,296 @@ public async Task GetMemory_AfterAbort_GetsFakeMemory(int sizeHint)
context.Response.BodyWriter.Advance(memory.Length);
}, headers);
}
+
+ [Fact]
+ public async Task ControlStream_CloseBeforeSendingSettings()
+ {
+ await Http3Api.InitializeConnectionAsync(_noopApplication);
+
+ var outboundcontrolStream = await Http3Api.CreateControlStream();
+
+ await outboundcontrolStream.EndStreamAsync();
+
+ await outboundcontrolStream.ReceiveEndAsync();
+ }
+
+ [Fact]
+ public async Task ControlStream_PartialFrameThenClose()
+ {
+ await Http3Api.InitializeConnectionAsync(_noopApplication);
+
+ var outboundcontrolStream = await Http3Api.CreateControlStream();
+
+ var settings = new List
+ {
+ new Http3PeerSetting(Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100),
+ new Http3PeerSetting(Internal.Http3.Http3SettingType.EnableWebTransport, 1),
+ new Http3PeerSetting(Internal.Http3.Http3SettingType.H3Datagram, 1)
+ };
+ var len = Http3FrameWriter.CalculateSettingsSize(settings);
+
+ Http3FrameWriter.WriteHeader(Http3FrameType.Settings, len, outboundcontrolStream.Pair.Application.Output);
+
+ var parameterLength = VariableLengthIntegerHelper.WriteInteger(outboundcontrolStream.Pair.Application.Output.GetSpan(), (long)Internal.Http3.Http3SettingType.MaxFieldSectionSize);
+ outboundcontrolStream.Pair.Application.Output.Advance(parameterLength);
+ await outboundcontrolStream.Pair.Application.Output.FlushAsync();
+
+ await outboundcontrolStream.EndStreamAsync();
+
+ await outboundcontrolStream.ReceiveEndAsync();
+ }
+
+ [Fact]
+ public async Task SendDataObservesBackpressureFromApp()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(InternalHeaderNames.Method, "Custom"),
+ new KeyValuePair(InternalHeaderNames.Path, "/"),
+ new KeyValuePair(InternalHeaderNames.Scheme, "http"),
+ new KeyValuePair(InternalHeaderNames.Authority, "localhost:80"),
+ };
+
+ // Http3Stream hardcodes a 64k size for the RequestBodyPipe there is also the transport Pipe which we can influence with MaxRequestBufferSize
+ // So we need to send enough to fill up the 64k Pipe as well as the 100 byte Pipe.
+ var sendSize = 1024 * 65;
+ _serviceContext.ServerOptions.Limits.MaxRequestBufferSize = 100;
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var startedReadingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c =>
+ {
+ // Read a single byte to make sure data has gotten here before we start verifying backpressure in the test code
+ var res = await c.Request.BodyReader.ReadAsync();
+ Assert.Equal(sendSize, res.Buffer.Length);
+ c.Request.BodyReader.AdvanceTo(res.Buffer.Slice(1).Start);
+ startedReadingTcs.SetResult();
+
+ await tcs.Task;
+ res = await c.Request.BodyReader.ReadAsync();
+ Assert.Equal(sendSize - 1, res.Buffer.Length);
+ c.Request.BodyReader.AdvanceTo(res.Buffer.End);
+ }, headers);
+
+ var sendTask = requestStream.SendDataAsync(Encoding.ASCII.GetBytes(new string('a', sendSize)));
+
+ // Wait for "app" code to start reading to ensure it has gotten bytes before we start verifying backpressure
+ await startedReadingTcs.Task;
+ Assert.False(sendTask.IsCompleted);
+ tcs.SetResult();
+
+ await sendTask;
+
+ var responseHeaders = await requestStream.ExpectHeadersAsync();
+ Assert.Equal("200", responseHeaders[InternalHeaderNames.Status]);
+
+ await requestStream.ExpectReceiveEndOfStream();
+ }
+
+ [Fact]
+ public async Task Request_FrameParsingSingleByteAtATimeWorks()
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var total = 0;
+ var trailerValue = string.Empty;
+ await Http3Api.InitializeConnectionAsync(async context =>
+ {
+ var buffer = new byte[100];
+ var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
+ var captureTcs = tcs;
+ tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ captureTcs.SetResult();
+ Assert.Equal(1, read);
+ total = read;
+ while (read > 0)
+ {
+ read = await context.Request.Body.ReadAsync(buffer, total, buffer.Length - total);
+ captureTcs = tcs;
+ tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ captureTcs.SetResult();
+ total += read;
+ if (read == 0)
+ {
+ break;
+ }
+ Assert.Equal(1, read);
+ }
+
+ trailerValue = context.Request.GetTrailer("TestName");
+ });
+
+ // Use Inline scheduling and buffer size of 1 to guarantee each write will wait for the parsing loop to complete before writing more data
+ _serviceContext.ServerOptions.Limits.MaxRequestBufferSize = 1;
+ var stream = await Http3Api.CreateRequestStream(headers: [], clientWriterScheduler: PipeScheduler.Inline);
+
+ // Use local pipe to write frames so we can get the entire buffer in order to write it one byte at a time
+ var bufferPipe = new Pipe();
+ Http3FrameWriter.WriteHeader(Http3FrameType.Headers, frameLength: 38, bufferPipe.Writer);
+
+ var headersTotalSize = 0;
+ var headers = new Http3HeadersEnumerator();
+ headers.Initialize(new Dictionary() {
+ { InternalHeaderNames.Method, "POST" },
+ { InternalHeaderNames.Path, "/" },
+ { InternalHeaderNames.Scheme, "http" }, });
+
+ var mem = bufferPipe.Writer.GetMemory();
+ var done = QPackHeaderWriter.BeginEncodeHeaders(headers, mem.Span, ref headersTotalSize, out var length);
+ Assert.True(done);
+ bufferPipe.Writer.Advance(length);
+ await bufferPipe.Writer.FlushAsync();
+
+ // Write header frame one byte at a time
+ await WriteOneByteAtATime(bufferPipe.Reader, stream.Pair.Application.Output);
+
+ Http3FrameWriter.WriteHeader(Http3FrameType.Data, frameLength: 12, bufferPipe.Writer);
+ await bufferPipe.Writer.FlushAsync();
+
+ // Write data header one byte at a time
+ await WriteOneByteAtATime(bufferPipe.Reader, stream.Pair.Application.Output);
+
+ bufferPipe.Writer.Write(new byte[12]);
+ await bufferPipe.Writer.FlushAsync();
+
+ // Write data in data frame one byte at a time
+ // Don't use WriteOneByteAtATime() as we want to wait on the TCS after every flush to make sure app code consumed the data
+ // before we send another byte
+ var res = await bufferPipe.Reader.ReadAsync();
+ for (var i = 0; i < res.Buffer.Length; i++)
+ {
+ mem = stream.Pair.Application.Output.GetMemory();
+ mem.Span[0] = res.Buffer.Slice(i).FirstSpan[0];
+ stream.Pair.Application.Output.Advance(1);
+ // Use TCS to make sure app can read data before we send more
+ var capturedTcs = tcs;
+ await stream.Pair.Application.Output.FlushAsync();
+ await capturedTcs.Task;
+ }
+ bufferPipe.Reader.AdvanceTo(res.Buffer.End);
+
+ var trailers = new Http3HeadersEnumerator();
+ trailers.Initialize(new Dictionary()
+ {
+ { "TestName", "TestValue" }
+ });
+
+ Http3FrameWriter.WriteHeader(Http3FrameType.Headers, frameLength: 22, bufferPipe.Writer);
+ mem = bufferPipe.Writer.GetMemory();
+ done = QPackHeaderWriter.BeginEncodeHeaders(trailers, mem.Span, ref headersTotalSize, out length);
+ Assert.True(done);
+ bufferPipe.Writer.Advance(length);
+ await bufferPipe.Writer.FlushAsync();
+
+ // Write trailer frame one byte at a time
+ await WriteOneByteAtATime(bufferPipe.Reader, stream.Pair.Application.Output);
+
+ await stream.EndStreamAsync();
+
+ var responseHeaders = await stream.ExpectHeadersAsync();
+ Assert.Equal(3, responseHeaders.Count);
+ Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", responseHeaders[InternalHeaderNames.Status]);
+ Assert.Equal("0", responseHeaders["content-length"]);
+
+ await stream.ExpectReceiveEndOfStream();
+
+ Assert.Equal(12, total);
+ Assert.Equal("TestValue", trailerValue);
+ }
+
+ [Fact]
+ public async Task Control_FrameParsingSingleByteAtATimeWorks()
+ {
+ await Http3Api.InitializeConnectionAsync(_noopApplication);
+
+ // Use Inline scheduling and buffer size of 1 to guarantee each write will wait for the parsing loop to complete before writing more data
+ _serviceContext.ServerOptions.Limits.MaxRequestBufferSize = 1;
+ var outboundcontrolStream = await Http3Api.CreateControlStream(clientWriterScheduler: PipeScheduler.Inline);
+
+ // Use local pipe to write frames so we can get the entire buffer in order to write it one byte at a time
+ var bufferPipe = new Pipe();
+
+ var settings = new List
+ {
+ new Http3PeerSetting(Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100),
+ new Http3PeerSetting(Internal.Http3.Http3SettingType.EnableWebTransport, 1),
+ new Http3PeerSetting(Internal.Http3.Http3SettingType.H3Datagram, 1)
+ };
+ var len = Http3FrameWriter.CalculateSettingsSize(settings);
+
+ Http3FrameWriter.WriteHeader(Http3FrameType.Settings, len, bufferPipe.Writer);
+ var mem = bufferPipe.Writer.GetMemory();
+ Http3FrameWriter.WriteSettings(settings, mem.Span);
+
+ bufferPipe.Writer.Advance(len);
+ await bufferPipe.Writer.FlushAsync();
+
+ // Write Settings frame one byte at a time
+ await WriteOneByteAtATime(bufferPipe.Reader, outboundcontrolStream.Pair.Application.Output);
+
+ var fieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout();
+
+ Assert.Equal(Internal.Http3.Http3SettingType.MaxFieldSectionSize, fieldSetting.Key);
+ Assert.Equal(100, fieldSetting.Value);
+
+ fieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout();
+ Assert.Equal(Internal.Http3.Http3SettingType.EnableWebTransport, fieldSetting.Key);
+ Assert.Equal(1, fieldSetting.Value);
+
+ fieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout();
+ Assert.Equal(Internal.Http3.Http3SettingType.H3Datagram, fieldSetting.Key);
+ Assert.Equal(1, fieldSetting.Value);
+
+ // Frames must be well-formed otherwise we close the connection with a frame error
+ Http3FrameWriter.WriteHeader(Http3FrameType.CancelPush, frameLength: 2, bufferPipe.Writer);
+ var idLength = VariableLengthIntegerHelper.WriteInteger(bufferPipe.Writer.GetSpan(), longToEncode: 1026);
+ bufferPipe.Writer.Advance(idLength);
+ await bufferPipe.Writer.FlushAsync();
+
+ // Write CancelPush frame one byte at a time
+ await WriteOneByteAtATime(bufferPipe.Reader, outboundcontrolStream.Pair.Application.Output);
+
+ // Frames must be well-formed otherwise we close the connection with a frame error
+ Http3FrameWriter.WriteHeader(Http3FrameType.GoAway, frameLength: 4, bufferPipe.Writer);
+ idLength = VariableLengthIntegerHelper.WriteInteger(bufferPipe.Writer.GetSpan(), longToEncode: 100026);
+ bufferPipe.Writer.Advance(idLength);
+ await bufferPipe.Writer.FlushAsync();
+
+ try
+ {
+ // Write GoAway frame one byte at a time
+ await WriteOneByteAtATime(bufferPipe.Reader, outboundcontrolStream.Pair.Application.Output);
+ }
+ // As soon as the GOAWAY frame identifier is processed we initiate the connection close process.
+ // That means it's possible to still be writing to the stream when we close the
+ // connection which would result in an exception. We'll just ignore the exception in this case.
+ catch (Exception) { }
+
+ await outboundcontrolStream.EndStreamAsync();
+
+ // Check that connection is closed.
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ Http3Api.MultiplexedConnectionContext.ConnectionClosed.Register(() => tcs.TrySetResult());
+ await tcs.Task;
+
+ await outboundcontrolStream.ReceiveEndAsync();
+ }
+
+ private async Task WriteOneByteAtATime(PipeReader reader, PipeWriter writer)
+ {
+ var res = await reader.ReadAsync();
+ try
+ {
+ for (var i = 0; i < res.Buffer.Length; i++)
+ {
+ var mem = writer.GetMemory();
+ mem.Span[0] = res.Buffer.Slice(i).FirstSpan[0];
+ writer.Advance(1);
+ await writer.FlushAsync();
+ }
+ }
+ finally
+ {
+ reader.AdvanceTo(res.Buffer.End);
+ }
+ }
}
diff --git a/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs b/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs
index 3a343a62a4cc..c7f1ec908d0f 100644
--- a/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs
+++ b/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs
@@ -128,19 +128,19 @@ static bool TryReadSlow(ref SequenceReader reader, out long value)
}
}
- public static long GetInteger(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined)
+ // If callsite has 'examined', set it to buffer.End if the integer wasn't successfully read, otherwise set examined = consumed.
+ public static bool TryGetInteger(in ReadOnlySequence buffer, out SequencePosition consumed, out long integer)
{
var reader = new SequenceReader(buffer);
- if (TryRead(ref reader, out long value))
+ if (TryRead(ref reader, out integer))
{
- consumed = examined = buffer.GetPosition(reader.Consumed);
- return value;
+ consumed = buffer.GetPosition(reader.Consumed);
+ return true;
}
else
{
- consumed = default;
- examined = buffer.End;
- return -1;
+ consumed = buffer.Start;
+ return false;
}
}
diff --git a/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs b/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs
index d67d24c0ba25..e461bfd41ed4 100644
--- a/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs
+++ b/src/Shared/test/Shared.Tests/runtime/Http3/VariableLengthIntegerHelperTests.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
@@ -223,12 +223,12 @@ public void GetInteger_ValidSegmentedSequence()
MemorySegment memorySegment2 = memorySegment1.Append(new byte[] { 0, 0, 0, 0, 0, 0, 2 });
ReadOnlySequence readOnlySequence = new ReadOnlySequence(
memorySegment1, 0, memorySegment2, memorySegment2.Memory.Length);
- long result = VariableLengthIntegerHelper.GetInteger(readOnlySequence,
- out SequencePosition consumed, out SequencePosition examined);
+ bool result = VariableLengthIntegerHelper.TryGetInteger(readOnlySequence,
+ out SequencePosition consumed, out long integer);
- Assert.Equal(2, result);
+ Assert.True(result);
+ Assert.Equal(2, integer);
Assert.Equal(7, consumed.GetInteger());
- Assert.Equal(7, examined.GetInteger());
}
[Fact]
@@ -238,12 +238,11 @@ public void GetInteger_NotValidSegmentedSequence()
MemorySegment memorySegment2 = memorySegment1.Append(new byte[] { 0, 0, 0, 0, 0, 2 });
ReadOnlySequence readOnlySequence = new ReadOnlySequence(
memorySegment1, 0, memorySegment2, memorySegment2.Memory.Length);
- long result = VariableLengthIntegerHelper.GetInteger(readOnlySequence,
- out SequencePosition consumed, out SequencePosition examined);
+ bool result = VariableLengthIntegerHelper.TryGetInteger(readOnlySequence,
+ out SequencePosition consumed, out long integer);
- Assert.Equal(-1, result);
+ Assert.False(result);
Assert.Equal(0, consumed.GetInteger());
- Assert.Equal(6, examined.GetInteger());
}
[Fact]