diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 10524d020b0c..683deaa09ec8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -975,6 +975,8 @@ private Task DecodeHeadersAsync(bool endHeaders, in ReadOnlySequence paylo if (endHeaders) { + _currentHeadersStream.OnHeadersComplete(); + StartStream(); ResetRequestHeaderParsingState(); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index ad210a33c979..9ac3ec48d966 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.HPack; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -207,6 +208,73 @@ public async Task RequestHeaderStringReuse_MultipleStreams_KnownHeaderReused() await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task RequestHeaderStringReuse_MultipleStreams_KnownHeaderClearedIfNotReused() + { + const BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance; + + IEnumerable> requestHeaders1 = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/hello"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair(HeaderNames.ContentType, "application/json") + }; + + // Note: No content-type + IEnumerable> requestHeaders2 = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/hello"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80") + }; + + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(1, requestHeaders1, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 36, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + // TriggerTick will trigger the stream to be returned to the pool so we can assert it + TriggerTick(); + + // Stream has been returned to the pool + Assert.Equal(1, _connection.StreamPool.Count); + Assert.True(_connection.StreamPool.TryPeek(out var stream1)); + + // Hacky but required because header references is private. + var headerReferences1 = typeof(HttpRequestHeaders).GetField("_headers", privateFlags).GetValue(stream1.RequestHeaders); + var contentTypeValue1 = (StringValues)headerReferences1.GetType().GetField("_ContentType").GetValue(headerReferences1); + + await StartStreamAsync(3, requestHeaders2, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 6, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + + // TriggerTick will trigger the stream to be returned to the pool so we can assert it + TriggerTick(); + + // Stream has been returned to the pool + Assert.Equal(1, _connection.StreamPool.Count); + Assert.True(_connection.StreamPool.TryPeek(out var stream2)); + + // Hacky but required because header references is private. + var headerReferences2 = typeof(HttpRequestHeaders).GetField("_headers", privateFlags).GetValue(stream2.RequestHeaders); + var contentTypeValue2 = (StringValues)headerReferences2.GetType().GetField("_ContentType").GetValue(headerReferences2); + + Assert.Equal("application/json", contentTypeValue1); + Assert.Equal(StringValues.Empty, contentTypeValue2); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task StreamPool_SingleStream_ReturnedToPool() {