Skip to content

Commit afa4860

Browse files
Merge HTTP/2 and HTTP/3 request cookies on Kestrel (#41591)
1 parent 82fe8dd commit afa4860

File tree

9 files changed

+125
-29
lines changed

9 files changed

+125
-29
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ internal partial class HttpRequestHeaders : IHeaderDictionary
383383
private HeaderReferences _headers;
384384

385385
public bool HasConnection => (_bits & 0x2L) != 0;
386+
public bool HasCookie => (_bits & 0x20000L) != 0;
386387
public bool HasTransferEncoding => (_bits & 0x20000000000L) != 0;
387388

388389
public int HostCount => _headers._Host.Count;

src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ public void OnHeadersComplete()
4848
Clear(headersToClear);
4949
}
5050

51+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
52+
public void MergeCookies()
53+
{
54+
if (HasCookie && _headers._Cookie.Count > 1)
55+
{
56+
_headers._Cookie = string.Join("; ", _headers._Cookie.ToArray());
57+
}
58+
}
59+
5160
protected override void ClearFast()
5261
{
5362
if (!ReuseHeaderValues)

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
201201
// Suppress pseudo headers from the public headers collection.
202202
HttpRequestHeaders.ClearPseudoRequestHeaders();
203203

204+
// Cookies should be merged into a single string separated by "; "
205+
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
206+
HttpRequestHeaders.MergeCookies();
207+
204208
return true;
205209
}
206210

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,10 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
907907
// Suppress pseudo headers from the public headers collection.
908908
HttpRequestHeaders.ClearPseudoRequestHeaders();
909909

910+
// Cookies should be merged into a single string separated by "; "
911+
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34#section-4.1.1.2
912+
HttpRequestHeaders.MergeCookies();
913+
910914
return true;
911915
}
912916

src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
54
using System.Buffers;
6-
using System.Buffers.Binary;
75
using System.Diagnostics;
8-
using System.IO;
96
using System.IO.Pipelines;
10-
using System.Linq;
117
using System.Net.Http.HPack;
12-
using System.Threading.Tasks;
138
using BenchmarkDotNet.Attributes;
149
using Microsoft.AspNetCore.Http;
1510
using Microsoft.AspNetCore.Http.Features;
1611
using Microsoft.AspNetCore.Server.Kestrel.Core;
17-
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
1812
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1913
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
20-
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
2114
using Microsoft.AspNetCore.Testing;
22-
using Microsoft.Extensions.Logging.Abstractions;
2315
using Microsoft.Extensions.Primitives;
2416
using Microsoft.Net.Http.Headers;
2517
using Http2HeadersEnumerator = Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2HeadersEnumerator;
@@ -36,15 +28,20 @@ public abstract class Http2ConnectionBenchmarkBase
3628
private int _currentStreamId;
3729
private byte[] _headersBuffer;
3830
private DuplexPipe.DuplexPipePair _connectionPair;
39-
private Http2Frame _httpFrame;
4031
private int _dataWritten;
32+
private Task _requestProcessingTask;
33+
34+
private readonly Http2Frame _receiveHttpFrame = new();
35+
private readonly Http2Frame _sendHttpFrame = new();
4136

4237
protected abstract Task ProcessRequest(HttpContext httpContext);
4338

39+
[Params(0, 1, 3)]
40+
public int NumCookies { get; set; }
41+
4442
public virtual void GlobalSetup()
4543
{
4644
_memoryPool = PinnedBlockMemoryPoolFactory.Create();
47-
_httpFrame = new Http2Frame();
4845

4946
var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
5047

@@ -56,6 +53,16 @@ public virtual void GlobalSetup()
5653
_httpRequestHeaders[HeaderNames.Scheme] = new StringValues("http");
5754
_httpRequestHeaders[HeaderNames.Authority] = new StringValues("localhost:80");
5855

56+
if (NumCookies > 0)
57+
{
58+
var cookies = new string[NumCookies];
59+
for (var index = 0; index < NumCookies; index++)
60+
{
61+
cookies[index] = $"{index}={index + 1}";
62+
}
63+
_httpRequestHeaders[HeaderNames.Cookie] = cookies;
64+
}
65+
5966
_headersBuffer = new byte[1024 * 16];
6067
_hpackEncoder = new DynamicHPackEncoder();
6168

@@ -79,7 +86,7 @@ public virtual void GlobalSetup()
7986

8087
_currentStreamId = 1;
8188

82-
_ = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory()));
89+
_requestProcessingTask = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory()));
8390

8491
_connectionPair.Application.Output.Write(Http2Connection.ClientPreface);
8592
_connectionPair.Application.Output.WriteSettings(new Http2PeerSettings
@@ -89,45 +96,45 @@ public virtual void GlobalSetup()
8996
_connectionPair.Application.Output.FlushAsync().GetAwaiter().GetResult();
9097

9198
// Read past connection setup frames
92-
ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult();
93-
Debug.Assert(_httpFrame.Type == Http2FrameType.SETTINGS);
94-
ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult();
95-
Debug.Assert(_httpFrame.Type == Http2FrameType.WINDOW_UPDATE);
96-
ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult();
97-
Debug.Assert(_httpFrame.Type == Http2FrameType.SETTINGS);
99+
ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult();
100+
Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.SETTINGS);
101+
ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult();
102+
Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.WINDOW_UPDATE);
103+
ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult();
104+
Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.SETTINGS);
98105
}
99106

100107
[Benchmark]
101108
public async Task MakeRequest()
102109
{
103110
_requestHeadersEnumerator.Initialize(_httpRequestHeaders);
104111
_requestHeadersEnumerator.MoveNext();
105-
_connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame);
112+
_connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _sendHttpFrame);
106113
await _connectionPair.Application.Output.FlushAsync();
107114

108115
while (true)
109116
{
110-
await ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame);
117+
await ReceiveFrameAsync(_connectionPair.Application.Input);
111118

112-
if (_httpFrame.StreamId != _currentStreamId && _httpFrame.StreamId != 0)
119+
if (_receiveHttpFrame.StreamId != _currentStreamId && _receiveHttpFrame.StreamId != 0)
113120
{
114-
throw new Exception($"Unexpected stream ID: {_httpFrame.StreamId}");
121+
throw new Exception($"Unexpected stream ID: {_receiveHttpFrame.StreamId}");
115122
}
116123

117-
if (_httpFrame.Type == Http2FrameType.DATA)
124+
if (_receiveHttpFrame.Type == Http2FrameType.DATA)
118125
{
119-
_dataWritten += _httpFrame.DataPayloadLength;
126+
_dataWritten += _receiveHttpFrame.DataPayloadLength;
120127
}
121128

122129
if (_dataWritten > 1024 * 32)
123130
{
124-
_connectionPair.Application.Output.WriteWindowUpdateAsync(streamId: 0, _dataWritten, _httpFrame);
131+
_connectionPair.Application.Output.WriteWindowUpdateAsync(streamId: 0, _dataWritten, _sendHttpFrame);
125132
await _connectionPair.Application.Output.FlushAsync();
126133

127134
_dataWritten = 0;
128135
}
129136

130-
if ((_httpFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
137+
if ((_receiveHttpFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
131138
{
132139
break;
133140
}
@@ -136,7 +143,7 @@ public async Task MakeRequest()
136143
_currentStreamId += 2;
137144
}
138145

139-
internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame frame, uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize)
146+
internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize)
140147
{
141148
while (true)
142149
{
@@ -147,7 +154,7 @@ internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame fra
147154

148155
try
149156
{
150-
if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload))
157+
if (Http2FrameReader.TryReadFrame(ref buffer, _receiveHttpFrame, maxFrameSize, out var framePayload))
151158
{
152159
consumed = examined = framePayload.End;
153160
return;
@@ -170,9 +177,10 @@ internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame fra
170177
}
171178

172179
[GlobalCleanup]
173-
public void Dispose()
180+
public async ValueTask DisposeAsync()
174181
{
175182
_connectionPair.Application.Output.Complete();
183+
await _requestProcessingTask;
176184
_memoryPool?.Dispose();
177185
}
178186
}

src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks;
99

1010
public class Http2ConnectionBenchmark : Http2ConnectionBenchmarkBase
1111
{
12-
[Params(0, 128, 1024)]
12+
[Params(0)]
1313
public int ResponseDataLength { get; set; }
1414

1515
private string _responseData;

src/Servers/Kestrel/shared/KnownHeaders.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ static KnownHeaders()
101101
};
102102
var requestHeadersExistence = new[]
103103
{
104+
HeaderNames.Cookie,
104105
HeaderNames.Connection,
105106
HeaderNames.TransferEncoding,
106107
};

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2840,6 +2840,33 @@ public async Task HEADERS_Received_RequestLineLength_StreamError()
28402840
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
28412841
}
28422842

2843+
[Fact]
2844+
public async Task HEADERS_CookiesMergedIntoOne()
2845+
{
2846+
var headers = new[]
2847+
{
2848+
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
2849+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
2850+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
2851+
new KeyValuePair<string, string>(HeaderNames.Cookie, "a=0"),
2852+
new KeyValuePair<string, string>(HeaderNames.Cookie, "b=1"),
2853+
new KeyValuePair<string, string>(HeaderNames.Cookie, "c=2"),
2854+
};
2855+
2856+
await InitializeConnectionAsync(_readHeadersApplication);
2857+
2858+
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
2859+
2860+
await ExpectAsync(Http2FrameType.HEADERS,
2861+
withLength: 36,
2862+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
2863+
withStreamId: 1);
2864+
2865+
Assert.Equal("a=0; b=1; c=2", _receivedHeaders[HeaderNames.Cookie]);
2866+
2867+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
2868+
}
2869+
28432870
[Fact]
28442871
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
28452872
{

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,48 @@ await Http3Api.InitializeConnectionAsync(async context =>
130130
await requestStream.ExpectReceiveEndOfStream();
131131
}
132132

133+
[Fact]
134+
public async Task HEADERS_CookiesMergedIntoOne()
135+
{
136+
var requestHeaders = new[]
137+
{
138+
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
139+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
140+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
141+
new KeyValuePair<string, string>(HeaderNames.Cookie, "a=0"),
142+
new KeyValuePair<string, string>(HeaderNames.Cookie, "b=1"),
143+
new KeyValuePair<string, string>(HeaderNames.Cookie, "c=2"),
144+
};
145+
146+
var receivedHeaders = "";
147+
148+
await Http3Api.InitializeConnectionAsync(async context =>
149+
{
150+
var buffer = new byte[16 * 1024];
151+
var received = 0;
152+
153+
// verify that the cookies are all merged into a single string
154+
receivedHeaders = context.Request.Headers[HeaderNames.Cookie];
155+
156+
while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
157+
{
158+
await context.Response.Body.WriteAsync(buffer, 0, received);
159+
}
160+
});
161+
162+
await Http3Api.CreateControlStream();
163+
await Http3Api.GetInboundControlStream();
164+
var requestStream = await Http3Api.CreateRequestStream();
165+
166+
await requestStream.SendHeadersAsync(requestHeaders, endStream: true);
167+
var responseHeaders = await requestStream.ExpectHeadersAsync();
168+
169+
await requestStream.ExpectReceiveEndOfStream();
170+
await requestStream.OnDisposedTask.DefaultTimeout();
171+
172+
Assert.Equal("a=0; b=1; c=2", receivedHeaders);
173+
}
174+
133175
[Theory]
134176
[InlineData(0, 0)]
135177
[InlineData(1, 4)]

0 commit comments

Comments
 (0)