Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,4 @@ Diagnostic id values for experimental APIs must not be recycled, as that could s
| __`SYSLIB5004`__ | .NET 9 | TBD | `X86Base.DivRem` is experimental since performance is not as optimized as `T.DivRem` |
| __`SYSLIB5005`__ | .NET 9 | .NET 10 | `System.Formats.Nrbf` is experimental |
| __`SYSLIB5006`__ | .NET 10 | TBD | Types for Post-Quantum Cryptography (PQC) are experimental. |
| __`SYSLIB5008`__ | .NET 11 | TBD | `SocketsHttpHandler` connection eviction control and `HttpRequestMessage.ConnectionId` APIs are experimental. |
3 changes: 3 additions & 0 deletions src/libraries/Common/src/System/Experimentals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal static class Experimentals
// Types for Post-Quantum Cryptography (PQC) are experimental.
internal const string PostQuantumCryptographyDiagId = "SYSLIB5006";

// SocketsHttpHandler connection eviction control and HttpRequestMessage.ConnectionId APIs are experimental.
internal const string SocketsHttpHandlerExperimentalDiagId = "SYSLIB5008";

// When adding a new diagnostic ID, add it to the table in docs\project\list-of-diagnostics.md as well.
// Keep new const identifiers above this comment.
}
Expand Down
18 changes: 18 additions & 0 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ public partial class HttpRequestMessage : System.IDisposable
public HttpRequestMessage() { }
public HttpRequestMessage(System.Net.Http.HttpMethod method, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Uri")] string? requestUri) { }
public HttpRequestMessage(System.Net.Http.HttpMethod method, System.Uri? requestUri) { }
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5008", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public long? ConnectionId { get { throw null; } set { } }
Comment thread
MihaZupan marked this conversation as resolved.
public System.Net.Http.HttpContent? Content { get { throw null; } set { } }
public System.Net.Http.Headers.HttpRequestHeaders Headers { get { throw null; } }
public System.Net.Http.HttpMethod Method { get { throw null; } set { } }
Expand Down Expand Up @@ -424,9 +426,21 @@ protected override void SerializeToStream(System.IO.Stream stream, System.Net.Tr
public sealed partial class SocketsHttpConnectionContext
{
internal SocketsHttpConnectionContext() { }
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5008", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public long ConnectionId { get { throw null; } }
public System.Net.DnsEndPoint DnsEndPoint { get { throw null; } }
public System.Net.Http.HttpRequestMessage InitialRequestMessage { get { throw null; } }
}
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5008", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public sealed partial class SocketsHttpConnectionEvictionContext
{
internal SocketsHttpConnectionEvictionContext() { }
public System.TimeSpan Age { get { throw null; } }
public long ConnectionId { get { throw null; } }
public System.Net.DnsEndPoint DnsEndPoint { get { throw null; } }
public System.Version HttpVersion { get { throw null; } }
public System.Net.IPEndPoint? RemoteEndPoint { get { throw null; } }
}
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public sealed partial class SocketsHttpHandler : System.Net.Http.HttpMessageHandler
{
Expand Down Expand Up @@ -465,6 +479,8 @@ public SocketsHttpHandler() { }
public System.Net.Http.HeaderEncodingSelector<System.Net.Http.HttpRequestMessage>? RequestHeaderEncodingSelector { get { throw null; } set { } }
public System.TimeSpan ResponseDrainTimeout { get { throw null; } set { } }
public System.Net.Http.HeaderEncodingSelector<System.Net.Http.HttpRequestMessage>? ResponseHeaderEncodingSelector { get { throw null; } set { } }
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5008", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public System.Func<System.Net.Http.SocketsHttpConnectionEvictionContext, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>>? ShouldEvictConnection { get { throw null; } set { } }
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public System.Net.Security.SslClientAuthenticationOptions SslOptions { get { throw null; } set { } }
public bool UseCookies { get { throw null; } set { } }
Expand All @@ -476,6 +492,8 @@ protected override void Dispose(bool disposing) { }
public sealed partial class SocketsHttpPlaintextStreamFilterContext
{
internal SocketsHttpPlaintextStreamFilterContext() { }
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5008", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public long ConnectionId { get { throw null; } }
public System.Net.Http.HttpRequestMessage InitialRequestMessage { get { throw null; } }
public System.Version NegotiatedHttpVersion { get { throw null; } }
public System.IO.Stream PlaintextStream { get { throw null; } }
Expand Down
7 changes: 7 additions & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-openbsd;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent)-haiku;$(NetCoreAppCurrent)-android;$(NetCoreAppCurrent)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>$(DefineConstants);HTTP_DLL</DefineConstants>
<!-- SocketsHttpHandler connection eviction control and HttpRequestMessage.ConnectionId (SYSLIB5008) APIs are experimental and consumed internally. -->
<NoWarn>$(NoWarn);SYSLIB5008</NoWarn>
<UseCompilerGeneratedDocXmlFile>false</UseCompilerGeneratedDocXmlFile>
</PropertyGroup>

Expand Down Expand Up @@ -160,6 +162,8 @@
Link="Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs" />
<Compile Include="$(CommonPath)System\Obsoletions.cs"
Link="Common\System\Obsoletions.cs" />
<Compile Include="$(CommonPath)System\Experimentals.cs"
Link="Common\System\Experimentals.cs" />
<Compile Include="$(CommonPath)System\Net\CredentialCacheKey.cs"
Link="Common\System\Net\CredentialCacheKey.cs" />
</ItemGroup>
Expand Down Expand Up @@ -218,6 +222,7 @@
<Compile Include="System\Net\Http\SocketsHttpHandler\PreAuthCredentialCache.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\RawConnectionStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\RedirectHandler.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionEvictionContext.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionContext.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpHandler.cs" />
<Compile Include="System\Net\Http\HttpTelemetry.AnyOS.cs" />
Expand Down Expand Up @@ -434,6 +439,7 @@
Link="Common\System\Net\HttpStatusDescription.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpNoProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionEvictionContext.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionContext.cs" />
<Compile Include="System\Net\Http\BrowserHttpHandler\SystemProxyInfo.Browser.cs" />
<Compile Include="System\Net\Http\BrowserHttpHandler\SocketsHttpHandler.cs" />
Expand All @@ -449,6 +455,7 @@
Link="Common\System\Net\HttpStatusDescription.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpKeepAlivePingPolicy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpNoProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionEvictionContext.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionContext.cs" />
<Compile Include="System\Net\Http\BrowserHttpHandler\SocketsHttpHandler.cs" />
<Compile Include="System\Net\Http\WasiHttpHandler\SystemProxyInfo.Wasi.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,12 @@ public Func<SocketsHttpPlaintextStreamFilterContext, CancellationToken, ValueTas
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}

[Experimental(Experimentals.SocketsHttpHandlerExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
public Func<SocketsHttpConnectionEvictionContext, CancellationToken, Task<bool>>? ShouldEvictConnection
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;

namespace System.Net.Http
{
Expand All @@ -15,15 +14,19 @@ public class HttpRequestMessage : IDisposable
internal static Version DefaultRequestVersion => HttpVersion.Version11;
internal static HttpVersionPolicy DefaultVersionPolicy => HttpVersionPolicy.RequestVersionOrLower;

private const int MessageNotYetSent = 0;
private const int MessageAlreadySent = 1;
private const int PropagatorStateInjectedByDiagnosticsHandler = 2;
private const int MessageDisposed = 4;
private const int AuthDisabled = 8;
[Flags]
private enum MessageFlags
{
AlreadySent = 1,
PropagatorStateInjectedByDiagnosticsHandler = 2,
Disposed = 4,
AuthDisabled = 8,
ConnectionIdSet = 16,
}

private MessageFlags _flags;

// Track whether the message has been sent.
// The message shouldn't be sent again if this field is equal to MessageAlreadySent.
private int _sendStatus = MessageNotYetSent;
private long _connectionId;

private HttpMethod _method;
private Uri? _requestUri;
Expand Down Expand Up @@ -122,6 +125,56 @@ public Uri? RequestUri
/// </summary>
public HttpRequestOptions Options => _options ??= new HttpRequestOptions();

/// <summary>
/// Gets or sets the identifier of the connection that this request was most recently sent on. The value is not
/// guaranteed to be set: it remains <see langword="null"/> when the request was not handled by a connection, for
/// example because it timed out before a connection could be obtained.
/// </summary>
/// <remarks>
/// When the request is sent through a <see cref="SocketsHttpHandler"/>, the value matches the connection id
/// reported through EventSource telemetry and the id passed to
/// <see cref="SocketsHttpHandler.ShouldEvictConnection"/> for the connection that served the request, allowing
/// a caller to correlate a request with that connection. It also matches the id surfaced to a custom
/// <see cref="SocketsHttpHandler.ConnectCallback"/>. When a request is sent over multiple connections (for
/// example after a redirect or a retry), the value reflects the most recent attempt.
/// <para>
/// HTTP CONNECT proxy tunnels are an exception to the correlation with a custom
/// <see cref="SocketsHttpHandler.ConnectCallback"/>: when the request is served over such a tunnel, the callback
/// observes the tunnel's underlying transport connection to the proxy, whose id differs from this one (which
/// identifies the tunneled connection that carried the request). Both ids remain observable through a
/// <see cref="SocketsHttpHandler.PlaintextStreamFilter"/>, which runs once per hop and reports the transport
/// connection's id for the CONNECT hop and this id for the tunneled hop.
/// </para>
/// <para>
/// These correlations apply only when the request is handled by <see cref="SocketsHttpHandler"/>. Another
/// <see cref="HttpMessageHandler"/> may never set this value, or may assign it a different meaning.
/// </para>
/// <para>
/// This property is intended to be read after the request has been sent. Assigning a value before the request
/// is sent has no effect on how the request is handled: it does not request or influence the use of a particular
/// connection, and any value set by the caller is overwritten with the id of the connection that actually serves
/// the request.
/// </para>
/// </remarks>
[Experimental(Experimentals.SocketsHttpHandlerExperimentalDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
public long? ConnectionId
{
// ConnectionIdSet is stored separately to avoid the extra bytes needed for a nullable 'long?' field.
get => _flags.HasFlag(MessageFlags.ConnectionIdSet) ? _connectionId : null;
set
{
if (value is null)
{
_flags &= ~MessageFlags.ConnectionIdSet;
}
else
{
_connectionId = value.Value;
_flags |= MessageFlags.ConnectionIdSet;
}
}
}

public HttpRequestMessage()
: this(HttpMethod.Get, (Uri?)null)
{
Expand Down Expand Up @@ -175,25 +228,30 @@ public override string ToString()
return sb.ToString();
}

internal bool MarkAsSent() => Interlocked.CompareExchange(ref _sendStatus, MessageAlreadySent, MessageNotYetSent) == MessageNotYetSent;
internal bool MarkAsSent()
{
MessageFlags previousFlags = _flags;
_flags = previousFlags | MessageFlags.AlreadySent;
return !previousFlags.HasFlag(MessageFlags.AlreadySent);
}
Comment on lines +231 to +236

internal bool WasSentByHttpClient() => (_sendStatus & MessageAlreadySent) != 0;
internal bool WasSentByHttpClient() => _flags.HasFlag(MessageFlags.AlreadySent);

internal void MarkPropagatorStateInjectedByDiagnosticsHandler() => _sendStatus |= PropagatorStateInjectedByDiagnosticsHandler;
internal void MarkPropagatorStateInjectedByDiagnosticsHandler() => _flags |= MessageFlags.PropagatorStateInjectedByDiagnosticsHandler;

internal bool WasPropagatorStateInjectedByDiagnosticsHandler() => (_sendStatus & PropagatorStateInjectedByDiagnosticsHandler) != 0;
internal bool WasPropagatorStateInjectedByDiagnosticsHandler() => _flags.HasFlag(MessageFlags.PropagatorStateInjectedByDiagnosticsHandler);
Comment on lines +238 to +242

internal void DisableAuth() => _sendStatus |= AuthDisabled;
internal void DisableAuth() => _flags |= MessageFlags.AuthDisabled;

internal bool IsAuthDisabled() => (_sendStatus & AuthDisabled) != 0;
internal bool IsAuthDisabled() => _flags.HasFlag(MessageFlags.AuthDisabled);

private bool Disposed
{
get => (_sendStatus & MessageDisposed) != 0;
get => _flags.HasFlag(MessageFlags.Disposed);
set
{
Debug.Assert(value);
_sendStatus |= MessageDisposed;
_flags |= MessageFlags.Disposed;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,14 @@ private async Task InjectNewHttp11ConnectionAsync(RequestQueue<HttpConnection>.Q

internal async ValueTask<HttpConnection> CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
(Stream stream, TransportContext? transportContext, Activity? activity, IPEndPoint? remoteEndPoint) = await ConnectAsync(request, async, isForHttp2: false, cancellationToken).ConfigureAwait(false);
return await ConstructHttp11ConnectionAsync(async, stream, transportContext, request, activity, remoteEndPoint, cancellationToken).ConfigureAwait(false);
(Stream stream, TransportContext? transportContext, Activity? activity, IPEndPoint? remoteEndPoint, long connectionId) = await ConnectAsync(request, async, isForHttp2: false, cancellationToken).ConfigureAwait(false);
return await ConstructHttp11ConnectionAsync(async, stream, transportContext, request, activity, remoteEndPoint, connectionId, cancellationToken).ConfigureAwait(false);
}

private async ValueTask<HttpConnection> ConstructHttp11ConnectionAsync(bool async, Stream stream, TransportContext? transportContext, HttpRequestMessage request, Activity? activity, IPEndPoint? remoteEndPoint, CancellationToken cancellationToken)
private async ValueTask<HttpConnection> ConstructHttp11ConnectionAsync(bool async, Stream stream, TransportContext? transportContext, HttpRequestMessage request, Activity? activity, IPEndPoint? remoteEndPoint, long connectionId, CancellationToken cancellationToken)
{
Stream newStream = await ApplyPlaintextFilterAsync(async, stream, HttpVersion.Version11, request, cancellationToken).ConfigureAwait(false);
return new HttpConnection(this, newStream, transportContext, activity, remoteEndPoint);
Stream newStream = await ApplyPlaintextFilterAsync(async, stream, HttpVersion.Version11, request, connectionId, cancellationToken).ConfigureAwait(false);
return new HttpConnection(this, newStream, transportContext, activity, remoteEndPoint, connectionId);
}

private void HandleHttp11ConnectionFailure(HttpConnectionWaiter<HttpConnection>? requestWaiter, Exception e)
Expand Down Expand Up @@ -368,6 +368,11 @@ private void ReturnHttp11Connection(HttpConnection connection)
{
connection.MarkConnectionAsIdle();

// Eviction callback checks are normally triggered from a background timer that looks at all idle connections.
// If this connection was in use during that time, the eviction callback may have been skipped for it.
// Check whether that's the case now by comparing the last EvictionGeneration of the connection with that of the pool.
connection.RunEvictionEvaluationIfNeeded();

// The fast path when there are enough connections and no pending requests
// is that we'll see _http11RequestQueueIsEmptyAndNotDisposed being true both
// times, and all we'll have to do as part of returning the connection is
Expand Down
Loading
Loading