Skip to content

Commit 4f1c56f

Browse files
authored
Respect the Keep-Alive response header on HTTP/1.1 as well (#73585)
* Respect the Keep-Alive response header on HTTP/1.1 as well * Add some more comments
1 parent 207e31c commit 4f1c56f

2 files changed

Lines changed: 41 additions & 62 deletions

File tree

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ async ValueTask<int> ReadAheadWithZeroByteReadAsync()
236236

237237
private bool CheckKeepAliveTimeoutExceeded()
238238
{
239-
// We only honor a Keep-Alive timeout on HTTP/1.0 responses.
239+
// We intentionally honor the Keep-Alive timeout on all HTTP/1.X versions, not just 1.0. This is to maximize compat with
240+
// servers that use a lower idle timeout than the client, but give us a hint in the form of a Keep-Alive timeout parameter.
240241
// If _keepAliveTimeoutSeconds is 0, no timeout has been set.
241242
return _keepAliveTimeoutSeconds != 0 &&
242243
GetIdleTicks(Environment.TickCount64) >= _keepAliveTimeoutSeconds * 1000;
@@ -665,11 +666,6 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
665666
ParseHeaderNameValue(this, line.Span, response, isFromTrailer: false);
666667
}
667668

668-
if (response.Version.Minor == 0)
669-
{
670-
ProcessHttp10KeepAliveHeader(response);
671-
}
672-
673669
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStop();
674670

675671
if (allowExpect100ToContinue != null)
@@ -1124,49 +1120,58 @@ private static void ParseHeaderNameValue(HttpConnection connection, ReadOnlySpan
11241120
}
11251121
else
11261122
{
1127-
// Request headers returned on the response must be treated as custom headers.
11281123
string headerValue = connection.GetResponseHeaderValueWithCaching(descriptor, value, valueEncoding);
1124+
1125+
if (descriptor.Equals(KnownHeaders.KeepAlive))
1126+
{
1127+
// We are intentionally going against RFC to honor the Keep-Alive header even if
1128+
// we haven't received a Keep-Alive connection token to maximize compat with servers.
1129+
connection.ProcessKeepAliveHeader(headerValue);
1130+
}
1131+
1132+
// Request headers returned on the response must be treated as custom headers.
11291133
response.Headers.TryAddWithoutValidation(
11301134
(descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor,
11311135
headerValue);
11321136
}
11331137
}
11341138

1135-
private void ProcessHttp10KeepAliveHeader(HttpResponseMessage response)
1139+
private void ProcessKeepAliveHeader(string keepAlive)
11361140
{
1137-
if (response.Headers.NonValidated.TryGetValues(KnownHeaders.KeepAlive.Name, out HeaderStringValues keepAliveValues))
1138-
{
1139-
string keepAlive = keepAliveValues.ToString();
1140-
var parsedValues = new UnvalidatedObjectCollection<NameValueHeaderValue>();
1141+
var parsedValues = new UnvalidatedObjectCollection<NameValueHeaderValue>();
11411142

1142-
if (NameValueHeaderValue.GetNameValueListLength(keepAlive, 0, ',', parsedValues) == keepAlive.Length)
1143+
if (NameValueHeaderValue.GetNameValueListLength(keepAlive, 0, ',', parsedValues) == keepAlive.Length)
1144+
{
1145+
foreach (NameValueHeaderValue nameValue in parsedValues)
11431146
{
1144-
foreach (NameValueHeaderValue nameValue in parsedValues)
1147+
// The HTTP/1.1 spec does not define any parameters for the Keep-Alive header, so we are using the de facto standard ones - timeout and max.
1148+
if (string.Equals(nameValue.Name, "timeout", StringComparison.OrdinalIgnoreCase))
11451149
{
1146-
if (string.Equals(nameValue.Name, "timeout", StringComparison.OrdinalIgnoreCase))
1150+
if (!string.IsNullOrEmpty(nameValue.Value) &&
1151+
HeaderUtilities.TryParseInt32(nameValue.Value, out int timeout) &&
1152+
timeout >= 0)
11471153
{
1148-
if (!string.IsNullOrEmpty(nameValue.Value) &&
1149-
HeaderUtilities.TryParseInt32(nameValue.Value, out int timeout) &&
1150-
timeout >= 0)
1154+
// Some servers are very strict with closing the connection exactly at the timeout.
1155+
// Avoid using the connection if it is about to exceed the timeout to avoid resulting request failures.
1156+
const int OffsetSeconds = 1;
1157+
1158+
if (timeout <= OffsetSeconds)
11511159
{
1152-
if (timeout == 0)
1153-
{
1154-
_connectionClose = true;
1155-
}
1156-
else
1157-
{
1158-
_keepAliveTimeoutSeconds = timeout;
1159-
}
1160+
_connectionClose = true;
11601161
}
1161-
}
1162-
else if (string.Equals(nameValue.Name, "max", StringComparison.OrdinalIgnoreCase))
1163-
{
1164-
if (nameValue.Value == "0")
1162+
else
11651163
{
1166-
_connectionClose = true;
1164+
_keepAliveTimeoutSeconds = timeout - OffsetSeconds;
11671165
}
11681166
}
11691167
}
1168+
else if (string.Equals(nameValue.Name, "max", StringComparison.OrdinalIgnoreCase))
1169+
{
1170+
if (nameValue.Value == "0")
1171+
{
1172+
_connectionClose = true;
1173+
}
1174+
}
11701175
}
11711176
}
11721177
}

src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http1KeepAlive.cs

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri =>
4444

4545
[OuterLoop("Uses Task.Delay")]
4646
[Fact]
47-
public async Task Http10ResponseWithKeepAliveTimeout_ConnectionRecycledAfterTimeout()
47+
public async Task Http1ResponseWithKeepAliveTimeout_ConnectionRecycledAfterTimeout()
4848
{
4949
await LoopbackServer.CreateClientAndServerAsync(async uri =>
5050
{
@@ -60,7 +60,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri =>
6060
await server.AcceptConnectionAsync(async connection =>
6161
{
6262
await connection.ReadRequestDataAsync();
63-
await connection.WriteStringAsync("HTTP/1.0 200 OK\r\nKeep-Alive: timeout=1\r\nContent-Length: 1\r\n\r\n1");
63+
await connection.WriteStringAsync("HTTP/1.1 200 OK\r\nKeep-Alive: timeout=2\r\nContent-Length: 1\r\n\r\n1");
6464
connection.CompleteRequestProcessing();
6565

6666
await Assert.ThrowsAnyAsync<Exception>(() => connection.ReadRequestDataAsync());
@@ -74,6 +74,7 @@ await server.AcceptConnectionAsync(async connection =>
7474
[InlineData("timeout=1000", true)]
7575
[InlineData("timeout=30", true)]
7676
[InlineData("timeout=0", false)]
77+
[InlineData("timeout=1", false)]
7778
[InlineData("foo, bar=baz, timeout=30", true)]
7879
[InlineData("foo, bar=baz, timeout=0", false)]
7980
[InlineData("timeout=-1", true)]
@@ -86,7 +87,7 @@ await server.AcceptConnectionAsync(async connection =>
8687
[InlineData("timeout=30, max=0", false)]
8788
[InlineData("timeout=0, max=1", false)]
8889
[InlineData("timeout=0, max=0", false)]
89-
public async Task Http10ResponseWithKeepAlive_ConnectionNotReusedForShortTimeoutOrMax0(string keepAlive, bool shouldReuseConnection)
90+
public async Task Http1ResponseWithKeepAlive_ConnectionNotReusedForShortTimeoutOrMax0(string keepAlive, bool shouldReuseConnection)
9091
{
9192
await LoopbackServer.CreateClientAndServerAsync(async uri =>
9293
{
@@ -100,7 +101,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri =>
100101
await server.AcceptConnectionAsync(async connection =>
101102
{
102103
await connection.ReadRequestDataAsync();
103-
await connection.WriteStringAsync($"HTTP/1.0 200 OK\r\nKeep-Alive: {keepAlive}\r\nContent-Length: 1\r\n\r\n1");
104+
await connection.WriteStringAsync($"HTTP/1.{Random.Shared.Next(10)} 200 OK\r\nKeep-Alive: {keepAlive}\r\nContent-Length: 1\r\n\r\n1");
104105
connection.CompleteRequestProcessing();
105106

106107
if (shouldReuseConnection)
@@ -119,32 +120,5 @@ await server.AcceptConnectionAsync(async connection =>
119120
}
120121
});
121122
}
122-
123-
[Theory]
124-
[InlineData("timeout=1")]
125-
[InlineData("timeout=0")]
126-
[InlineData("max=1")]
127-
[InlineData("max=0")]
128-
public async Task Http11ResponseWithKeepAlive_KeepAliveIsIgnored(string keepAlive)
129-
{
130-
await LoopbackServer.CreateClientAndServerAsync(async uri =>
131-
{
132-
using HttpClient client = CreateHttpClient();
133-
134-
await client.GetAsync(uri);
135-
await client.GetAsync(uri);
136-
},
137-
async server =>
138-
{
139-
await server.AcceptConnectionAsync(async connection =>
140-
{
141-
await connection.ReadRequestDataAsync();
142-
await connection.WriteStringAsync($"HTTP/1.1 200 OK\r\nKeep-Alive: {keepAlive}\r\nContent-Length: 1\r\n\r\n1");
143-
connection.CompleteRequestProcessing();
144-
145-
await connection.HandleRequestAsync();
146-
});
147-
});
148-
}
149123
}
150124
}

0 commit comments

Comments
 (0)