Skip to content

fix(client): ConnectionMultiplexer.GetServers uses System.Net.IPEndpoint instead of System.Net.Endpoint, causing exceptions with Elasticache #419

Description

@e8-ChuckRistau

Describe the bug

When calling ConnectionMultiplexer.GetServers(false), the server topology check that happens assumes that endpoints are using IP addresses. Sometimes endpoints are DNS names, like for AWS's Elasticache service.

When the library tries to force a conversion from a DNS hostname to an IP address, an exception is thrown.

Expected Behavior

Client correctly parses the results of ConnectionMultiplexer.GetServers(false) to System.Net.Endpoint objects, similar to the behavior seen in Redis.StackExchange for the same objects.

Current Behavior

Client throws a system error when failing to parse the DNS hostnames into System.Net.IPEndpoint objects. This is unique to the underlying "GetServers()", as most other parts seem to use System.Net.Endpoint instead.

Exception has occurred: CLR/System.FormatException
An exception of type 'System.FormatException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'An invalid IPEndPoint was specified.'
at System.Net.IPEndPoint.Parse(ReadOnlySpan1 s) at Valkey.Glide.ConnectionMultiplexer.<GetServers>b__11_0(String addr) at System.Linq.Enumerable.IEnumerableSelectIterator2.MoveNext()
at System.Linq.Enumerable.g__EnumerableToArray|324_0[TSource](IEnumerable`1 source)
at Valkey.Glide.ConnectionMultiplexer.GetServers()
at Valkey.Glide.ConnectionMultiplexer.GetEndPoints(Boolean configuredOnly)
at EncompassWebServices.BC_CacheTools.d__5.MoveNext() in /data/Chuck/Inetpub/ECP/EncompassWebServices/DevOps/CacheTools.cs:line 84

Valkey GLIDE C# client version

Other

Valkey GLIDE C# client version (Other)

v1.1.0

.NET version

Other

.NET version (Other)

.NET 10

Engine type and version

Other

Engine type and version (Other)

Valkey 8.2

Engine Environment

ElastiCache Provisioned

Engine Environment (Other)

No response

OS

Amazon Linux

OS (Other)

No response

Logs

⚪ None

Cluster information

  • Topology: cluster
  • Shards: 2
  • Replicas per shard: 0

Reproduction Steps

// Bug: ConnectionMultiplexer.GetEndPoints(false) throws FormatException on cluster with DNS hostnames
// Valkey.Glide v1.1.0, .NET 10.0, Amazon Linux 2023 (ARM64)
// Cluster: AWS ElastiCache Valkey, 2-shard cluster mode enabled
//
// GetEndPoints() internally calls GetServers() which passes discovered DNS hostnames
// through IPEndPoint.Parse(), which fails because they are not IP addresses.
//
// Stack trace:
//   System.FormatException: An invalid IPEndPoint was specified.
//     at System.Net.IPEndPoint.Parse(ReadOnlySpan`1 s)
//     at Valkey.Glide.ConnectionMultiplexer.<GetServers>b__11_0(String addr)
//     at System.Linq.Enumerable.IEnumerableSelectIterator`2.MoveNext()
//     at System.Linq.Enumerable.<ToArray>g__EnumerableToArray|324_0[TSource](IEnumerable`1 source)
//     at Valkey.Glide.ConnectionMultiplexer.GetServers()
//     at Valkey.Glide.ConnectionMultiplexer.GetEndPoints(Boolean configuredOnly)

using Valkey.Glide;

// Connect to an ElastiCache Valkey cluster using the clustercfg DNS endpoint
ConfigurationOptions config = new ConfigurationOptions
{
    EndPoints = { "my-cluster.clustercfg.usw2.cache.amazonaws.com:6379" },
    ConnectTimeout = 10000,
    ResponseTimeout = 5000,
    Ssl = true,
};

ConnectionMultiplexer multiplexer = ConnectionMultiplexer.Connect(config);

// This works — basic commands route correctly through the multiplexer
IDatabase db = multiplexer.GetDatabase();
await db.StringSetAsync("test-key", "hello");
string value = await db.StringGetAsync("test-key"); // "hello" ✓

// BUG: configuredOnly:false calls throw System.FormatException
// because discovered topology contains DNS hostnames like:
//   "my-cluster-0001-001.my-cluster.ozsykg.usw2.cache.amazonaws.com:6379"
// and the library tries to parse them with IPEndPoint.Parse()

try
{
    // configuredOnly: false — discovered endpoints
    System.Net.EndPoint[] discovered = multiplexer.GetEndPoints(false);
    Console.WriteLine($"Discovered endpoints: {discovered.Length}"); // never reached
}
catch (FormatException ex)
{
    Console.WriteLine($"GetEndPoints(false) threw: {ex.Message}");
}

try
{
    // configuredOnly: true — this works because GetServers() not called
    System.Net.EndPoint[] configured = multiplexer.GetEndPoints(true);
    Console.WriteLine($"Configured endpoints: {configured.Length}"); 
}
catch (FormatException ex)
{
    Console.WriteLine($"GetEndPoints(true) threw: {ex.Message}"); //never reached, since this path works correctly
}

// Expected: GetEndPoints(false) returns System.Net.Endpoint objects (or similar like DnsEndPoint) for cluster nodes
// Actual: FormatException because IPEndPoint.Parse() cannot handle DNS hostnames

// Workaround: execute CLUSTER NODES directly and parse the output
ValkeyResult result = await db.ExecuteAsync("CLUSTER", "NODES");
string clusterNodes = (string)result;
// Parse "node-id ip:port@cport flags ..." lines to extract master addresses

Possible Solution

Potential fixes for ConnectionMultiplexer.cs:

public IServer[] GetServers()
{
    if (_db.IsCluster)
    {
        Dictionary<string, string> result = _db.Command(Request.Info(Array.Empty<InfoOptions.Section>()).ToMultiNodeValue(), Route.AllNodes).GetAwaiter().GetResult();
        return result.Keys.Select((string addr) => new ValkeyServer(_db, ParseEndPoint(addr))).ToArray();
    }

    // ... standalone case unchanged ...
}

private static EndPoint ParseEndPoint(string addr)
{
    // Try IPEndPoint first (handles "1.2.3.4:6379")
    if (IPEndPoint.TryParse(addr, out IPEndPoint ipEndPoint))
    {
        return ipEndPoint;
    }

    // Fall back to DnsEndPoint for hostnames (handles "my-node.cache.amazonaws.com:6379")
    int lastColon = addr.LastIndexOf(':');
    if (lastColon > 0 && int.TryParse(addr.AsSpan(lastColon + 1), out int port))
    {
        return new DnsEndPoint(addr.Substring(0, lastColon), port);
    }

    throw new FormatException($"Unable to parse endpoint: '{addr}'");
}

And also, a fix to the comparison in GetServer to go along with this change:

public IServer GetServer(EndPoint endpoint)
{
    IServer[] servers = GetServers();
    string target = endpoint.ToString();
    foreach (IServer server in servers)
    {
        if (server.EndPoint.Equals(endpoint) || server.EndPoint.ToString() == target)
        {
            return server;
        }
    }

    throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint));
}

Related

⚪ None

Additional Information

If I have time to properly check out and build the repo and test my proposed fix, I'll make a PR for it later.

Metadata

Metadata

Assignees

Labels

compatibilityStackExchange.Redis API compatibilitycoreCore library (`sources/Valkey.Glide/`)

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions