Skip to content

SocketsHttpHandler.ConnectionFactory usability with non-DNS endpoints #41149

@JamesNK

Description

@JamesNK

Today it feels like there is some friction when using a SocketsConnectionFactory that needs a non-DNS endpoint. The endpoint passed into SocketsConnectionFactory.ConnectAsync comes from HttpRequestMessage.RequestUri, and that must always be a valid URI and always is turned into a DnsEndPoint.

To specify a custom endpoint you need to implement a custom SocketsConnectionFactory and override the endpoint. One option is to hardcode the endpoint when the factory is created:

public class UnixDomainSocketConnectionFactory : SocketsConnectionFactory
{
    private readonly UnixDomainSocketEndPoint _endPoint;

    public UnixDomainSocketConnectionFactory(UnixDomainSocketEndPoint endPoint) : base(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified)
    {
        _endPoint = endPoint;
    }

    public override ValueTask<Connection> ConnectAsync(EndPoint? endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default)
    {
        return base.ConnectAsync(_endPoint, options, cancellationToken);
    }
}

This works, but you're limited to a single endpoint for the SocketsHttpHandler. You can't have different HttpRequestMessage instances be sent to different UDS endpoints.

An alternative could be to put the endpoint in HttpRequestMessage.Option. Something like this (I haven't tried it):

public class UnixDomainSocketConnectionFactory2 : SocketsConnectionFactory
{
    public UnixDomainSocketConnectionFactory2() : base(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified)
    {
    }

    public override ValueTask<Connection> ConnectAsync(EndPoint? endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default)
    {
        if (options.TryGet(typeof(HttpRequestMessage), out var request))
        {
            request.Options.TryGetValue(new HttpRequestOptionsKey<EndPoint>(nameof(EndPoint)), out endPoint);
        }

        return base.ConnectAsync(endPoint, options, cancellationToken);
    }
}

Now the endpoint can be different per-request. However I'm not sure what the rules are around deciding whether ConnectAsync should be called. Would the RequestUri need to change if the endpoint specified on HttpRequestMessage.Options changed?


Idea: Have SocketsHttpHandler check for a custom endpoint in HttpRequestMessage.Options automatically. If there is a request with an endpoint in the options then SocketsHttpHandler will use it to determine if a connection already exists for that endpoint, and it will pass that endpoint to SocketsConnectionFactory.ConnectAsync.

Usage:

var socketsHttpHandler = new SocketsHttpHandler
{
    ConnectionFactory = new SocketsConnectionFactory(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
};
var httpClient = new HttpClient(socketsHttpHandler);

// Create request and specify UDS endpoint in its options.
// SocketsHttpHandler knows about this option name and will use it to figure out if a connection exists
// and will pass this endpoint to ConnectAsync.
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
var endPointKey = new HttpRequestOptionsKey<EndPoint>(nameof(EndPoint));
request.Options.Set(endPointKey, new UnixDomainSocketEndPoint(SocketPath));

var response = await httpClient.SendAsync(request);

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Net.HttpdocumentationDocumentation bug or enhancement, does not impact product or test code

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions