Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 01fa16f

Browse files
authored
Implement SocketsHttpHandler's Expect100ContinueTimeout and ConnectTimeout (#27242)
With the expectation that we'll want to expose this in 2.1, implement Expect100ContinueTimeout and ConnectTimeout. The members are currently internal but can be flipped public easily once the APIs are approved. This also fixes an issue with cancellation around the connect phase, where a cancellation request that came in during the SSL auth phase would not be respected.
1 parent e5db8de commit 01fa16f

File tree

9 files changed

+306
-25
lines changed

9 files changed

+306
-25
lines changed

src/Common/src/System/Net/Http/HttpHandlerDefaults.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ internal static class HttpHandlerDefaults
2424
public const bool DefaultCheckCertificateRevocationList = false;
2525
public static readonly TimeSpan DefaultPooledConnectionLifetime = Timeout.InfiniteTimeSpan;
2626
public static readonly TimeSpan DefaultPooledConnectionIdleTimeout = TimeSpan.FromMinutes(2);
27+
public static readonly TimeSpan DefaultExpect100ContinueTimeout = TimeSpan.FromSeconds(1);
28+
public static readonly TimeSpan DefaultConnectTimeout = Timeout.InfiniteTimeSpan;
2729
}
2830
}

src/System.Net.Http/src/ILLinkTrim.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
<assembly fullname="System.Net.Http">
33
<!-- Anonymous types are used with DiagnosticSource logging and subscribers reflect over those, calling their public getters. -->
44
<type fullname="*f__AnonymousType*" />
5+
<type fullname="System.Net.Http.SocketsHttpHandler" /> <!-- TODO #27235, #27145: Remove once public -->
56
</assembly>
67
</linker>

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ internal partial class HttpConnection : IDisposable
2828
/// <summary>Default size of the write buffer used for the connection.</summary>
2929
private const int InitialWriteBufferSize = InitialReadBufferSize;
3030
/// <summary>
31-
/// Delay after which we'll send the request payload for ExpectContinue if
32-
/// the server hasn't yet responded.
33-
/// </summary>
34-
private const int Expect100TimeoutMilliseconds = 1000;
35-
/// <summary>
3631
/// Size after which we'll close the connection rather than send the payload in response
3732
/// to final error status code sent by the server when using Expect: 100-continue.
3833
/// </summary>
@@ -353,7 +348,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
353348
allowExpect100ToContinue = new TaskCompletionSource<bool>();
354349
var expect100Timer = new Timer(
355350
s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
356-
allowExpect100ToContinue, TimeSpan.FromMilliseconds(Expect100TimeoutMilliseconds), Timeout.InfiniteTimeSpan);
351+
allowExpect100ToContinue, _pool.Settings._expect100ContinueTimeout, Timeout.InfiniteTimeSpan);
357352
_sendRequestContentTask = SendRequestContentWithExpect100ContinueAsync(
358353
request, allowExpect100ToContinue.Task, stream, expect100Timer, cancellationToken);
359354
}
@@ -580,10 +575,10 @@ private CancellationTokenRegistration RegisterCancellation(CancellationToken can
580575
}, _weakThisRef);
581576
}
582577

583-
private static bool ShouldWrapInOperationCanceledException(Exception error, CancellationToken cancellationToken) =>
578+
internal static bool ShouldWrapInOperationCanceledException(Exception error, CancellationToken cancellationToken) =>
584579
!(error is OperationCanceledException) && cancellationToken.IsCancellationRequested;
585580

586-
private static Exception CreateOperationCanceledException(Exception error, CancellationToken cancellationToken) =>
581+
internal static Exception CreateOperationCanceledException(Exception error, CancellationToken cancellationToken) =>
587582
new OperationCanceledException(s_cancellationMessage, error, cancellationToken);
588583

589584
private static bool LineIsEmpty(ArraySegment<byte> line) => line.Count == 0;

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -206,24 +206,60 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
206206

207207
private async ValueTask<HttpConnection> CreateConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
208208
{
209-
Stream stream = await
210-
(_proxyUri == null ?
211-
ConnectHelper.ConnectAsync(_host, _port, cancellationToken) :
212-
(_sslOptions == null ?
213-
ConnectHelper.ConnectAsync(_proxyUri.IdnHost, _proxyUri.Port, cancellationToken) :
214-
EstablishProxyTunnel(cancellationToken))).ConfigureAwait(false);
215-
216-
TransportContext transportContext = null;
217-
if (_sslOptions != null)
209+
// If a non-infinite connect timeout has been set, create and use a new CancellationToken that'll be canceled
210+
// when either the original token is canceled or a connect timeout occurs.
211+
CancellationTokenSource cancellationWithConnectTimeout = null;
212+
if (Settings._connectTimeout != Timeout.InfiniteTimeSpan)
218213
{
219-
SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptions, request, stream, cancellationToken).ConfigureAwait(false);
220-
stream = sslStream;
221-
transportContext = sslStream.TransportContext;
214+
cancellationWithConnectTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default);
215+
cancellationWithConnectTimeout.CancelAfter(Settings._connectTimeout);
216+
cancellationToken = cancellationWithConnectTimeout.Token;
222217
}
223218

224-
return _maxConnections == int.MaxValue ?
225-
new HttpConnection(this, stream, transportContext) :
226-
new HttpConnectionWithFinalizer(this, stream, transportContext); // finalizer needed to signal the pool when a connection is dropped
219+
try
220+
{
221+
Stream stream = await
222+
(_proxyUri == null ?
223+
ConnectHelper.ConnectAsync(_host, _port, cancellationToken) :
224+
(_sslOptions == null ?
225+
ConnectHelper.ConnectAsync(_proxyUri.IdnHost, _proxyUri.Port, cancellationToken) :
226+
EstablishProxyTunnel(cancellationToken))).ConfigureAwait(false);
227+
228+
TransportContext transportContext = null;
229+
if (_sslOptions != null)
230+
{
231+
// TODO #25206 and #24430: Register/IsCancellationRequested should be removable once SslStream auth and sockets respect cancellation.
232+
CancellationTokenRegistration ctr = cancellationToken.Register(s => ((Stream)s).Dispose(), stream);
233+
try
234+
{
235+
SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptions, request, stream, cancellationToken).ConfigureAwait(false);
236+
stream = sslStream;
237+
transportContext = sslStream.TransportContext;
238+
cancellationToken.ThrowIfCancellationRequested(); // to handle race condition where stream is dispose of by cancellation after auth
239+
}
240+
catch (Exception exc)
241+
{
242+
stream.Dispose(); // in case cancellation occurs after successful SSL auth
243+
if (HttpConnection.ShouldWrapInOperationCanceledException(exc, cancellationToken))
244+
{
245+
throw HttpConnection.CreateOperationCanceledException(exc, cancellationToken);
246+
}
247+
throw;
248+
}
249+
finally
250+
{
251+
ctr.Dispose();
252+
}
253+
}
254+
255+
return _maxConnections == int.MaxValue ?
256+
new HttpConnection(this, stream, transportContext) :
257+
new HttpConnectionWithFinalizer(this, stream, transportContext); // finalizer needed to signal the pool when a connection is dropped
258+
}
259+
finally
260+
{
261+
cancellationWithConnectTimeout?.Dispose();
262+
}
227263
}
228264

229265
// TODO (#23136):

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal sealed class HttpConnectionSettings
3030

3131
internal TimeSpan _pooledConnectionLifetime = HttpHandlerDefaults.DefaultPooledConnectionLifetime;
3232
internal TimeSpan _pooledConnectionIdleTimeout = HttpHandlerDefaults.DefaultPooledConnectionIdleTimeout;
33+
internal TimeSpan _expect100ContinueTimeout = HttpHandlerDefaults.DefaultExpect100ContinueTimeout;
34+
internal TimeSpan _connectTimeout = HttpHandlerDefaults.DefaultConnectTimeout;
3335

3436
internal SslClientAuthenticationOptions _sslOptions;
3537

@@ -48,8 +50,10 @@ public HttpConnectionSettings Clone()
4850
_allowAutoRedirect = _allowAutoRedirect,
4951
_automaticDecompression = _automaticDecompression,
5052
_cookieContainer = _cookieContainer,
53+
_connectTimeout = _connectTimeout,
5154
_credentials = _credentials,
5255
_defaultProxyCredentials = _defaultProxyCredentials,
56+
_expect100ContinueTimeout = _expect100ContinueTimeout,
5357
_maxAutomaticRedirections = _maxAutomaticRedirections,
5458
_maxConnectionsPerServer = _maxConnectionsPerServer,
5559
_maxResponseHeadersLength = _maxResponseHeadersLength,

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,38 @@ public TimeSpan PooledConnectionIdleTimeout
207207
}
208208
}
209209

210+
internal TimeSpan ConnectTimeout // TODO #27235: Expose publicly
211+
{
212+
get => _settings._connectTimeout;
213+
set
214+
{
215+
if ((value <= TimeSpan.Zero && value != Timeout.InfiniteTimeSpan) ||
216+
(value.TotalMilliseconds > int.MaxValue))
217+
{
218+
throw new ArgumentOutOfRangeException(nameof(value));
219+
}
220+
221+
CheckDisposedOrStarted();
222+
_settings._connectTimeout = value;
223+
}
224+
}
225+
226+
internal TimeSpan Expect100ContinueTimeout // TODO #27145: Expose publicly
227+
{
228+
get => _settings._expect100ContinueTimeout;
229+
set
230+
{
231+
if ((value < TimeSpan.Zero && value != Timeout.InfiniteTimeSpan) ||
232+
(value.TotalMilliseconds > int.MaxValue))
233+
{
234+
throw new ArgumentOutOfRangeException(nameof(value));
235+
}
236+
237+
CheckDisposedOrStarted();
238+
_settings._expect100ContinueTimeout = value;
239+
}
240+
}
241+
210242
public IDictionary<string, object> Properties =>
211243
_settings._properties ?? (_settings._properties = new Dictionary<string, object>());
212244

0 commit comments

Comments
 (0)