Skip to content

[API Proposal]: Enable easier correlation of HTTP requests to the ConnectionId #130108

Description

@MihaZupan

Background and motivation

Optional follow-up to #130102 (configurable HTTP connection eviction), and depends on it. This proposal reuses the same ConnectionId concept and [Experimental] diagnostic.

That proposal exposes a long ConnectionId on the connect and eviction callback contexts, so callers can associate custom state with a connection at creation time and recover it when deciding on eviction. To drive those eviction decisions from per-response or per-error signals, however, the caller needs to correlate a response or exception with the connection that produced it. Today that requires consuming EventSource telemetry with a lot of ceremony.

One motivating scenario is called out in #130102 ("evict connections with many consecutive 502s"), where a custom handler counts bad responses per connection and the eviction callback acts on the count. That handler needs request.ConnectionId.
The id is the same value already surfaced via telemetry and via the #130102 contexts.

API Proposal

namespace System.Net.Http;

public partial class HttpRequestMessage
{
    [Experimental] // Same diagnostic as #130102
    public long? ConnectionId { get; set; }
}

ConnectionId is an opaque identifier. The only guarantee is that it is unique within the current process; ids are not shared across handler instances (two handlers never hand out the same id). No ordering/format/stability across processes is guaranteed. This is the same id already exposed via telemetry and the #130102 contexts.

The ID is nullable because responses can be produced without a SocketsHttpHandler connection (custom HttpMessageHandlers, cached/synthesized responses, the browser handler, failures before a connection is established, ...).

The implementation may also choose not to convey the connection ID in all cases. E.g. if a request failed due to a timeout while waiting for a connection to be established, the ID won't be set. But if it timed out while waiting for a response on an existing connection it sent the request headers on, the ID would be set.
If a request was attempted on some connection, and then retried internally by SocketsHttpHandler before a response/exception is generated, the ID of that initial connection won't be reported.

The property on the request message would be cleared by SocketsHttpHandler right away if already set in order to lower the risk of failures being misattributed if the same request instance was reused (e.g. for retries).
Setting the value beforehand would have no impact on connection selection for that request (you can't use it to specify a preference).

API Usage

  • Evict connections with many consecutive 502s (the scenario referenced by [API Proposal]: Configurable HTTP connection eviction #130102):
    HttpMessageHandler handler = new SocketsHttpHandler
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(10),
        ShouldEvictConnection = (context, ct) =>
            Task.FromResult(_cache.TryGetValue(context.ConnectionId, out StrongBox<int> failures) && failures.Value > 10)
    };
    
    handler = new Http502ResponseCounterHandler(handler, _cache);
    
    sealed class Http502ResponseCounterHandler(HttpMessageHandler handler, IMemoryCache cache) : DelegatingHandler(handler)
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
    
            if (request.ConnectionId is long connectionId)
            {
                StrongBox<int> counter = cache.GetOrCreate(connectionId, _ => new StrongBox<int>());
                if (response.StatusCode == HttpStatusCode.BadGateway) Interlocked.Increment(ref counter.Value);
                else if (counter.Value > 0) Interlocked.Decrement(ref counter.Value);
            }
    
            return response;
        }
    }
  • Improve logs when request timeouts can be correlated to a bad connection:
    var request = new HttpRequestMessage(HttpMethod.Get, uri);
    try
    {
        using HttpResponseMessage response = await client.SendAsync(request);
    }
    catch (TimeoutException ex)
    {
        if (request.ConnectionId is long connectionId &&
            connectionIPs.TryGetValue(connectionId, out IPAddress ip))
        {
            _logger.LogWarning(ex, "Request to {Uri} timed out on connection to {IP}", request.RequestUri.AbsoluteUri, ip);
        }
    }

Alternative Designs

  • We could expose the ID on HttpResponseMessage, HttpRequestException, and HttpIOException instead of the request. The main reason why I chose not to is that HttpClient may throw timeout/cancellation exceptions where we don't have a good place to expose the new info. Consumers would possibly have to look at many different places and also dig through inner exceptions. With the request there's just a single location to check.
  • We could use 0 as a sentinel value and avoid the need to have the property be nullable.

Related to #63159

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions