BeyondDimension/SteamTools · error · AggregateException

Unable to connect to {host}

Error message

Unable to connect to {host}

What it means

Thrown by TunnelMiddleware when none of the resolved upstream endpoints for a host could be connected to within connectTimeout. Each per-endpoint failure is collected into innerExceptions (after honouring cancellation) and then aggregated, naming the host that could not be reached.

Source

Thrown at src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/HttpServer/Middleware/TunnelMiddleware.cs:85

        var innerExceptions = new List<Exception>();
        await foreach (var endPoint in GetUpstreamEndPointsAsync(host, cancellationToken))
        {
            var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            try
            {
                using var timeoutTokenSource = new CancellationTokenSource(connectTimeout);
                using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
                await socket.ConnectAsync(endPoint, linkedTokenSource.Token);
                return new NetworkStream(socket, ownsSocket: true);
            }
            catch (Exception ex)
            {
                socket.Dispose();
                cancellationToken.ThrowIfCancellationRequested();
                innerExceptions.Add(ex);
            }
        }
        throw new AggregateException($"Unable to connect to {host}", innerExceptions);
    }

    /// <summary>
    /// 获取目标终节点
    /// </summary>
    /// <param name="host"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    async IAsyncEnumerable<EndPoint> GetUpstreamEndPointsAsync(HostString host, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        const int HTTPS_PORT = 443;
        var targetHost = host.Host;
        var targetPort = host.Port ?? HTTPS_PORT;

        if (IPAddress.TryParse(targetHost, out var address) == true)
        {
            yield return new IPEndPoint(address, targetPort);
        }

View on GitHub (pinned to c16ffa08e0)

Solutions

  1. Read innerExceptions to distinguish timeout vs connection-refused vs DNS failure.
  2. Confirm the host resolves and its service port (typically 443) is reachable from the host machine.
  3. Validate the Host header being forwarded is correct and not mangled by rewriting.
  4. Switch accelerator node / route, or disable the tunnel temporarily to confirm the target itself is up.

Example fix

// before
throw new AggregateException($"Unable to connect to {host}", innerExceptions);

// after: include attempt count and dominant error for faster diagnosis
var dominant = innerExceptions.GroupBy(e => e.GetType())
    .OrderByDescending(g => g.Count()).First().Key.Name;
throw new AggregateException(
    $"Unable to connect to {host} ({innerExceptions.Count} attempts, dominant: {dominant})",
    innerExceptions);
Defensive patterns

Strategy: retry

Validate before calling

// Validate the host and reachability before tunnelling.
if (!HostString.IsValid(host.Host) || host.Port is < 1 or > 65535)
    throw new ArgumentException($"Invalid upstream host: {host}.");

async Task<bool> IsUpstreamReachableAsync(HostString h, CancellationToken ct)
{
    await foreach (var ep in GetUpstreamEndPointsAsync(h, ct))
    {
        using var s = new TcpClient();
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(connectTimeout);
        try { await s.ConnectAsync(ep, cts.Token); return true; } catch { }
    }
    return false;
}

Try / catch

try { await ForwardTunnelAsync(host, ct); }
catch (AggregateException ex) when (ex.Message.StartsWith("Unable to connect to"))
{
    if (attempt < maxAttempts && ex.InnerExceptions.Any(e => e is TimeoutException))
    { await Task.Delay(backoff, ct); goto retry; }
    // Return 502 to the client with the host name; do not leak inner exceptions.
    context.Response.StatusCode = 502;
    await context.Response.WriteAsync($"Bad gateway: {host}", ct);
}

Prevention

When it happens

Trigger: Forwarding a tunneled (e.g. HTTPS CONNECT-style) request where socket.ConnectAsync fails or times out for every endpoint returned by GetUpstreamEndPointsAsync for the HostString.

Common situations: Target service offline; DNS for the host resolves to unreachable IPs; port 443 (HTTPS_PORT) blocked by firewall/ISP; accelerator node behind the host is down; host header malformed so resolution yields bad endpoints.

Related errors


AI-assisted analysis of BeyondDimension/SteamTools@c16ffa08e0 (2026-08-13). Data as JSON: /api/errors/29bd45c14a0040ec. Report an issue: GitHub.