Skip to content

Commit e3ecc83

Browse files
authored
Concatenate multiple cookies with semicolon (#67455)
* Concatenate cookies with semicolon * Restore tests that run on .NET Framework * Change Cookie header to Custom * PR feedback
1 parent f46f4c9 commit e3ecc83

File tree

6 files changed

+78
-49
lines changed

6 files changed

+78
-49
lines changed

src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,16 @@ await LoopbackServerFactory.CreateClientAndServerAsync(
168168
{
169169
HttpRequestData requestData = await server.HandleRequestAsync();
170170

171-
// Multiple Cookie header values are treated as any other header values and are
172-
// concatenated using ", " as the separator.
171+
// Multiple Cookie header values are concatenated using "; " as the separator.
173172

174173
string cookieHeaderValue = requestData.GetSingleHeaderValue("Cookie");
175174

176-
var cookieValues = cookieHeaderValue.Split(new string[] { ", " }, StringSplitOptions.None);
175+
#if NETFRAMEWORK
176+
var separator = ", ";
177+
#else
178+
var separator = "; ";
179+
#endif
180+
var cookieValues = cookieHeaderValue.Split(new string[] { separator }, StringSplitOptions.None);
177181
Assert.Contains("A=1", cookieValues);
178182
Assert.Contains("B=2", cookieValues);
179183
Assert.Contains("C=3", cookieValues);
@@ -262,32 +266,20 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
262266
HttpRequestData requestData = await serverTask;
263267
string cookieHeaderValue = GetCookieValue(requestData);
264268

265-
// Multiple Cookie header values are treated as any other header values and are
269+
#if NETFRAMEWORK
270+
// On .NET Framework multiple Cookie header values are treated as any other header values and are
266271
// concatenated using ", " as the separator. The container cookie is concatenated to
267272
// one of these values using the "; " cookie separator.
268273

269-
var cookieValues = cookieHeaderValue.Split(new string[] { ", " }, StringSplitOptions.None);
270-
Assert.Equal(2, cookieValues.Count());
271-
272-
// Find container cookie and remove it so we can validate the rest of the cookie header values
273-
bool sawContainerCookie = false;
274-
for (int i = 0; i < cookieValues.Length; i++)
275-
{
276-
if (cookieValues[i].Contains(';'))
277-
{
278-
Assert.False(sawContainerCookie);
279-
280-
var cookies = cookieValues[i].Split(new string[] { "; " }, StringSplitOptions.None);
281-
Assert.Equal(2, cookies.Count());
282-
Assert.Contains(s_expectedCookieHeaderValue, cookies);
283-
284-
sawContainerCookie = true;
285-
cookieValues[i] = cookies.Where(c => c != s_expectedCookieHeaderValue).Single();
286-
}
287-
}
288-
274+
var separators = new string[] { "; ", ", " };
275+
#else
276+
var separators = new string[] { "; " };
277+
#endif
278+
var cookieValues = cookieHeaderValue.Split(separators, StringSplitOptions.None);
279+
Assert.Contains(s_expectedCookieHeaderValue, cookieValues);
289280
Assert.Contains("A=1", cookieValues);
290281
Assert.Contains("B=2", cookieValues);
282+
Assert.Equal(3, cookieValues.Count());
291283
}
292284
});
293285
}

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<WindowsRID>win</WindowsRID>
44
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -83,6 +83,7 @@
8383
<Compile Include="System\Net\Http\Headers\CacheControlHeaderValue.cs" />
8484
<Compile Include="System\Net\Http\Headers\ContentDispositionHeaderValue.cs" />
8585
<Compile Include="System\Net\Http\Headers\ContentRangeHeaderValue.cs" />
86+
<Compile Include="System\Net\Http\Headers\CookieHeaderParser.cs" />
8687
<Compile Include="System\Net\Http\Headers\DateHeaderParser.cs" />
8788
<Compile Include="System\Net\Http\Headers\EntityTagHeaderValue.cs" />
8889
<Compile Include="System\Net\Http\Headers\GenericHeaderParser.cs" />
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.Diagnostics.CodeAnalysis;
5+
6+
namespace System.Net.Http.Headers
7+
{
8+
internal sealed class CookieHeaderParser : HttpHeaderParser
9+
{
10+
internal static readonly CookieHeaderParser Parser = new CookieHeaderParser();
11+
12+
// According to RFC 6265 Section 4.2 multiple cookies have
13+
// to be concatenated using "; " as the separator.
14+
private CookieHeaderParser()
15+
: base(true, "; ")
16+
{
17+
}
18+
19+
public override bool TryParseValue(string? value, object? storeValue, ref int index, [NotNullWhen(true)] out object? parsedValue)
20+
{
21+
// Some headers support empty/null values. This one doesn't.
22+
if (string.IsNullOrEmpty(value) || (index == value.Length))
23+
{
24+
parsedValue = null;
25+
return false;
26+
}
27+
28+
parsedValue = value;
29+
index = value.Length;
30+
31+
return true;
32+
}
33+
}
34+
}

src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal static class KnownHeaders
4040
public static readonly KnownHeader ContentRange = new KnownHeader("Content-Range", HttpHeaderType.Content | HttpHeaderType.NonTrailing, GenericHeaderParser.ContentRangeParser, null, H2StaticTable.ContentRange);
4141
public static readonly KnownHeader ContentSecurityPolicy = new KnownHeader("Content-Security-Policy", http3StaticTableIndex: H3StaticTable.ContentSecurityPolicyAllNone);
4242
public static readonly KnownHeader ContentType = new KnownHeader("Content-Type", HttpHeaderType.Content | HttpHeaderType.NonTrailing, MediaTypeHeaderParser.SingleValueParser, null, H2StaticTable.ContentType, H3StaticTable.ContentTypeApplicationDnsMessage);
43-
public static readonly KnownHeader Cookie = new KnownHeader("Cookie", H2StaticTable.Cookie, H3StaticTable.Cookie);
43+
public static readonly KnownHeader Cookie = new KnownHeader("Cookie", HttpHeaderType.Custom, CookieHeaderParser.Parser, null, H2StaticTable.Cookie, H3StaticTable.Cookie);
4444
public static readonly KnownHeader Cookie2 = new KnownHeader("Cookie2");
4545
public static readonly KnownHeader Date = new KnownHeader("Date", HttpHeaderType.General | HttpHeaderType.NonTrailing, DateHeaderParser.Parser, null, H2StaticTable.Date, H3StaticTable.Date);
4646
public static readonly KnownHeader ETag = new KnownHeader("ETag", HttpHeaderType.Response, GenericHeaderParser.SingleValueEntityTagParser, null, H2StaticTable.ETag, H3StaticTable.ETag);

src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Headers.cs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -427,22 +427,22 @@ await server.HandleRequestAsync(headers: new[]
427427
});
428428
}
429429

430-
private static readonly (string Name, Encoding ValueEncoding, string[] Values)[] s_nonAsciiHeaders = new[]
430+
private static readonly (string Name, Encoding ValueEncoding, string Separator, string[] Values)[] s_nonAsciiHeaders = new[]
431431
{
432-
("foo", Encoding.ASCII, new[] { "bar" }),
433-
("header-0", Encoding.UTF8, new[] { "\uD83D\uDE03", "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A" }),
434-
("Cache-Control", Encoding.UTF8, new[] { "no-cache" }),
435-
("header-1", Encoding.UTF8, new[] { "\uD83D\uDE03" }),
436-
("Some-Header1", Encoding.Latin1, new[] { "\uD83D\uDE03", "UTF8-best-fit-to-latin1" }),
437-
("Some-Header2", Encoding.Latin1, new[] { "\u00FF", "\u00C4nd", "Ascii\u00A9" }),
438-
("Some-Header3", Encoding.ASCII, new[] { "\u00FF", "\u00C4nd", "Ascii\u00A9", "Latin1-best-fit-to-ascii" }),
439-
("header-2", Encoding.UTF8, new[] { "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A" }),
440-
("header-3", Encoding.UTF8, new[] { "\uFFFD" }),
441-
("header-4", Encoding.UTF8, new[] { "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A", "\uD83D\uDE03" }),
442-
("Cookie", Encoding.UTF8, new[] { "Cookies", "\uD83C\uDF6A", "everywhere" }),
443-
("Set-Cookie", Encoding.UTF8, new[] { "\uD83C\uDDF8\uD83C\uDDEE" }),
444-
("header-5", Encoding.UTF8, new[] { "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A", "foo", "\uD83D\uDE03", "bar" }),
445-
("bar", Encoding.UTF8, new[] { "foo" })
432+
("foo", Encoding.ASCII, ", ", new[] { "bar" }),
433+
("header-0", Encoding.UTF8, ", ", new[] { "\uD83D\uDE03", "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A" }),
434+
("Cache-Control", Encoding.UTF8, ", ", new[] { "no-cache" }),
435+
("header-1", Encoding.UTF8, ", ", new[] { "\uD83D\uDE03" }),
436+
("Some-Header1", Encoding.Latin1, ", ", new[] { "\uD83D\uDE03", "UTF8-best-fit-to-latin1" }),
437+
("Some-Header2", Encoding.Latin1, ", ", new[] { "\u00FF", "\u00C4nd", "Ascii\u00A9" }),
438+
("Some-Header3", Encoding.ASCII, ", ", new[] { "\u00FF", "\u00C4nd", "Ascii\u00A9", "Latin1-best-fit-to-ascii" }),
439+
("header-2", Encoding.UTF8, ", ", new[] { "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A" }),
440+
("header-3", Encoding.UTF8, ", ", new[] { "\uFFFD" }),
441+
("header-4", Encoding.UTF8, ", ", new[] { "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A", "\uD83D\uDE03" }),
442+
("Cookie", Encoding.UTF8, "; ", new[] { "Cookies", "\uD83C\uDF6A", "everywhere" }),
443+
("Set-Cookie", Encoding.UTF8, ", ", new[] { "\uD83C\uDDF8\uD83C\uDDEE" }),
444+
("header-5", Encoding.UTF8, ", ", new[] { "\uD83D\uDE48\uD83D\uDE49\uD83D\uDE4A", "foo", "\uD83D\uDE03", "bar" }),
445+
("bar", Encoding.UTF8, ", ", new[] { "foo" })
446446
};
447447

448448
[Fact]
@@ -457,7 +457,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(
457457
Version = UseVersion
458458
};
459459

460-
foreach ((string name, _, string[] values) in s_nonAsciiHeaders)
460+
foreach ((string name, _, _, string[] values) in s_nonAsciiHeaders)
461461
{
462462
requestMessage.Headers.Add(name, values);
463463
}
@@ -479,7 +479,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(
479479

480480
await client.SendAsync(TestAsync, requestMessage);
481481

482-
foreach ((string name, _, _) in s_nonAsciiHeaders)
482+
foreach ((string name, _, _, _) in s_nonAsciiHeaders)
483483
{
484484
Assert.Contains(name, seenHeaderNames);
485485
}
@@ -491,9 +491,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync(
491491
Assert.All(requestData.Headers,
492492
h => Assert.False(h.HuffmanEncoded, "Expose raw decoded bytes once HuffmanEncoding is supported"));
493493

494-
foreach ((string name, Encoding valueEncoding, string[] values) in s_nonAsciiHeaders)
494+
foreach ((string name, Encoding valueEncoding, string separator, string[] values) in s_nonAsciiHeaders)
495495
{
496-
byte[] valueBytes = valueEncoding.GetBytes(string.Join(", ", values));
496+
byte[] valueBytes = valueEncoding.GetBytes(string.Join(separator, values));
497497
Assert.Single(requestData.Headers,
498498
h => h.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && h.Raw.AsSpan().IndexOf(valueBytes) != -1);
499499
}
@@ -536,20 +536,20 @@ await LoopbackServerFactory.CreateClientAndServerAsync(
536536

537537
using HttpResponseMessage response = await client.SendAsync(TestAsync, requestMessage);
538538

539-
foreach ((string name, Encoding valueEncoding, string[] values) in s_nonAsciiHeaders)
539+
foreach ((string name, Encoding valueEncoding, string separator, string[] values) in s_nonAsciiHeaders)
540540
{
541541
Assert.Contains(name, seenHeaderNames);
542542
IEnumerable<string> receivedValues = Assert.Single(response.Headers, h => h.Key.Equals(name, StringComparison.OrdinalIgnoreCase)).Value;
543543
string value = Assert.Single(receivedValues);
544544

545-
string expected = valueEncoding.GetString(valueEncoding.GetBytes(string.Join(", ", values)));
545+
string expected = valueEncoding.GetString(valueEncoding.GetBytes(string.Join(separator, values)));
546546
Assert.Equal(expected, value, StringComparer.OrdinalIgnoreCase);
547547
}
548548
},
549549
async server =>
550550
{
551551
List<HttpHeaderData> headerData = s_nonAsciiHeaders
552-
.Select(h => new HttpHeaderData(h.Name, string.Join(", ", h.Values), valueEncoding: h.ValueEncoding))
552+
.Select(h => new HttpHeaderData(h.Name, string.Join(h.Separator, h.Values), valueEncoding: h.ValueEncoding))
553553
.ToList();
554554

555555
await server.HandleRequestAsync(headers: headerData);

src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<StringResourcesPath>../../src/Resources/Strings.resx</StringResourcesPath>
44
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -99,6 +99,8 @@
9999
Link="ProductionCode\System\Net\Http\Headers\ContentDispositionHeaderValue.cs" />
100100
<Compile Include="..\..\src\System\Net\Http\Headers\ContentRangeHeaderValue.cs"
101101
Link="ProductionCode\System\Net\Http\Headers\ContentRangeHeaderValue.cs" />
102+
<Compile Include="..\..\src\System\Net\Http\Headers\CookieHeaderParser.cs"
103+
Link="ProductionCode\System\Net\Http\Headers\CookieHeaderParser.cs" />
102104
<Compile Include="..\..\src\System\Net\Http\Headers\DateHeaderParser.cs"
103105
Link="ProductionCode\System\Net\Http\Headers\DateHeaderParser.cs" />
104106
<Compile Include="..\..\src\System\Net\Http\Headers\EntityTagHeaderValue.cs"

0 commit comments

Comments
 (0)