Skip to content

Commit c9d1fd6

Browse files
authored
Add support for HttpWebRequest.ReadWriteTimeout (#47648)
Uses the recently added support for SocketsHttpHandler.ConnectCallback to be used for synchronous operations to add support for HttpWebRequest.ReadWriteTimeout. That value is now used to set the Read/WriteTimeout on the underlying socket, with it then affecting all synchronous reads and writes on that connection. The ReadWriteTimeout is added to the cache key for whether the cached HttpClient instance can be used, as is whether the request is sync or async because that influences how connections are created from the HttpClient instance.
1 parent cbd8349 commit c9d1fd6

File tree

2 files changed

+94
-13
lines changed

2 files changed

+94
-13
lines changed

src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Net.Cache;
99
using System.Net.Http;
1010
using System.Net.Security;
11+
using System.Net.Sockets;
1112
using System.Runtime.Serialization;
1213
using System.Security.Authentication;
1314
using System.Security.Cryptography.X509Certificates;
@@ -97,11 +98,13 @@ private enum Booleans : uint
9798

9899
private class HttpClientParameters
99100
{
101+
public readonly bool Async;
100102
public readonly DecompressionMethods AutomaticDecompression;
101103
public readonly bool AllowAutoRedirect;
102104
public readonly int MaximumAutomaticRedirections;
103105
public readonly int MaximumResponseHeadersLength;
104106
public readonly bool PreAuthenticate;
107+
public readonly int ReadWriteTimeout;
105108
public readonly TimeSpan Timeout;
106109
public readonly SecurityProtocolType SslProtocols;
107110
public readonly bool CheckCertificateRevocationList;
@@ -111,13 +114,15 @@ private class HttpClientParameters
111114
public readonly X509CertificateCollection? ClientCertificates;
112115
public readonly CookieContainer? CookieContainer;
113116

114-
public HttpClientParameters(HttpWebRequest webRequest)
117+
public HttpClientParameters(HttpWebRequest webRequest, bool async)
115118
{
119+
Async = async;
116120
AutomaticDecompression = webRequest.AutomaticDecompression;
117121
AllowAutoRedirect = webRequest.AllowAutoRedirect;
118122
MaximumAutomaticRedirections = webRequest.MaximumAutomaticRedirections;
119123
MaximumResponseHeadersLength = webRequest.MaximumResponseHeadersLength;
120124
PreAuthenticate = webRequest.PreAuthenticate;
125+
ReadWriteTimeout = webRequest.ReadWriteTimeout;
121126
Timeout = webRequest.Timeout == Threading.Timeout.Infinite
122127
? Threading.Timeout.InfiniteTimeSpan
123128
: TimeSpan.FromMilliseconds(webRequest.Timeout);
@@ -132,11 +137,13 @@ public HttpClientParameters(HttpWebRequest webRequest)
132137

133138
public bool Matches(HttpClientParameters requestParameters)
134139
{
135-
return AutomaticDecompression == requestParameters.AutomaticDecompression
140+
return Async == requestParameters.Async
141+
&& AutomaticDecompression == requestParameters.AutomaticDecompression
136142
&& AllowAutoRedirect == requestParameters.AllowAutoRedirect
137143
&& MaximumAutomaticRedirections == requestParameters.MaximumAutomaticRedirections
138144
&& MaximumResponseHeadersLength == requestParameters.MaximumResponseHeadersLength
139145
&& PreAuthenticate == requestParameters.PreAuthenticate
146+
&& ReadWriteTimeout == requestParameters.ReadWriteTimeout
140147
&& Timeout == requestParameters.Timeout
141148
&& SslProtocols == requestParameters.SslProtocols
142149
&& CheckCertificateRevocationList == requestParameters.CheckCertificateRevocationList
@@ -1122,7 +1129,7 @@ private async Task<WebResponse> SendRequest(bool async)
11221129
HttpClient? client = null;
11231130
try
11241131
{
1125-
client = GetCachedOrCreateHttpClient(out disposeRequired);
1132+
client = GetCachedOrCreateHttpClient(async, out disposeRequired);
11261133
if (_requestStream != null)
11271134
{
11281135
ArraySegment<byte> bytes = _requestStream.GetBuffer();
@@ -1443,9 +1450,9 @@ private bool TryGetHostUri(string hostName, [NotNullWhen(true)] out Uri? hostUri
14431450
return Uri.TryCreate(s, UriKind.Absolute, out hostUri);
14441451
}
14451452

1446-
private HttpClient GetCachedOrCreateHttpClient(out bool disposeRequired)
1453+
private HttpClient GetCachedOrCreateHttpClient(bool async, out bool disposeRequired)
14471454
{
1448-
var parameters = new HttpClientParameters(this);
1455+
var parameters = new HttpClientParameters(this, async);
14491456
if (parameters.AreParametersAcceptableForCaching())
14501457
{
14511458
disposeRequired = false;
@@ -1477,7 +1484,7 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http
14771484
HttpClient? client = null;
14781485
try
14791486
{
1480-
var handler = new HttpClientHandler();
1487+
var handler = new SocketsHttpHandler();
14811488
client = new HttpClient(handler);
14821489
handler.AutomaticDecompression = parameters.AutomaticDecompression;
14831490
handler.Credentials = parameters.Credentials;
@@ -1528,20 +1535,55 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http
15281535

15291536
if (parameters.ClientCertificates != null)
15301537
{
1531-
handler.ClientCertificates.AddRange(parameters.ClientCertificates);
1538+
handler.SslOptions.ClientCertificates = new X509CertificateCollection(parameters.ClientCertificates);
15321539
}
15331540

15341541
// Set relevant properties from ServicePointManager
1535-
handler.SslProtocols = (SslProtocols)parameters.SslProtocols;
1536-
handler.CheckCertificateRevocationList = parameters.CheckCertificateRevocationList;
1542+
handler.SslOptions.EnabledSslProtocols = (SslProtocols)parameters.SslProtocols;
1543+
handler.SslOptions.CertificateRevocationCheckMode = parameters.CheckCertificateRevocationList ? X509RevocationMode.Online : X509RevocationMode.NoCheck;
15371544
RemoteCertificateValidationCallback? rcvc = parameters.ServerCertificateValidationCallback;
15381545
if (rcvc != null)
15391546
{
1540-
RemoteCertificateValidationCallback localRcvc = rcvc;
1541-
HttpWebRequest localRequest = request!;
1542-
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => localRcvc(localRequest, cert, chain, errors);
1547+
handler.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => rcvc(request!, cert, chain, errors);
15431548
}
15441549

1550+
// Set up a ConnectCallback so that we can control Socket-specific settings, like ReadWriteTimeout => socket.Send/ReceiveTimeout.
1551+
handler.ConnectCallback = async (context, cancellationToken) =>
1552+
{
1553+
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
1554+
1555+
try
1556+
{
1557+
socket.NoDelay = true;
1558+
if (parameters.ReadWriteTimeout > 0) // default is 5 minutes, so this is generally going to be true
1559+
{
1560+
socket.SendTimeout = socket.ReceiveTimeout = parameters.ReadWriteTimeout;
1561+
}
1562+
1563+
if (parameters.Async)
1564+
{
1565+
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
1566+
}
1567+
else
1568+
{
1569+
using (cancellationToken.UnsafeRegister(s => ((Socket)s!).Dispose(), socket))
1570+
{
1571+
socket.Connect(context.DnsEndPoint);
1572+
}
1573+
1574+
// Throw in case cancellation caused the socket to be disposed after the Connect completed
1575+
cancellationToken.ThrowIfCancellationRequested();
1576+
}
1577+
}
1578+
catch
1579+
{
1580+
socket.Dispose();
1581+
throw;
1582+
}
1583+
1584+
return new NetworkStream(socket, ownsSocket: true);
1585+
};
1586+
15451587
return client;
15461588
}
15471589
catch

src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,45 @@ public void ReadWriteTimeout_NegativeOrZeroValue_Fail()
10631063
Assert.Throws<ArgumentOutOfRangeException>(() => { request.ReadWriteTimeout = -10; });
10641064
}
10651065

1066+
[Fact]
1067+
public async Task ReadWriteTimeout_CancelsResponse()
1068+
{
1069+
var tcs = new TaskCompletionSource();
1070+
await LoopbackServer.CreateClientAndServerAsync(uri => Task.Run(async () =>
1071+
{
1072+
try
1073+
{
1074+
HttpWebRequest request = WebRequest.CreateHttp(uri);
1075+
request.ReadWriteTimeout = 10;
1076+
IOException e = await Assert.ThrowsAsync<IOException>(async () => // exception type is WebException on .NET Framework
1077+
{
1078+
using WebResponse response = await GetResponseAsync(request);
1079+
using (Stream myStream = response.GetResponseStream())
1080+
{
1081+
while (myStream.ReadByte() != -1) ;
1082+
}
1083+
});
1084+
Assert.True(e.InnerException is SocketException se && se.SocketErrorCode == SocketError.TimedOut);
1085+
}
1086+
finally
1087+
{
1088+
tcs.SetResult();
1089+
}
1090+
}), async server =>
1091+
{
1092+
try
1093+
{
1094+
await server.AcceptConnectionAsync(async connection =>
1095+
{
1096+
await connection.ReadRequestHeaderAsync();
1097+
await connection.WriteStringAsync("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nHello Wor");
1098+
await tcs.Task;
1099+
});
1100+
}
1101+
catch { }
1102+
});
1103+
}
1104+
10661105
[Theory, MemberData(nameof(EchoServers))]
10671106
public void CookieContainer_SetThenGetContainer_Success(Uri remoteServer)
10681107
{
@@ -1668,7 +1707,7 @@ public void GetResponseAsync_ParametersAreNotCachable_CreateNewClient(HttpWebReq
16681707

16691708
Task<Socket> secondAccept = listener.AcceptAsync();
16701709

1671-
Task<WebResponse> secondResponseTask = request1.GetResponseAsync();
1710+
Task<WebResponse> secondResponseTask = bool.Parse(async) ? request1.GetResponseAsync() : Task.Run(() => request1.GetResponse());
16721711
await ReplyToClient(responseContent, server, serverReader);
16731712
if (bool.Parse(connectionReusedString))
16741713
{

0 commit comments

Comments
 (0)