microsoft/aspire · error · DistributedApplicationException

Failed to delete port

Error message

Failed to delete port '{portNumber}' on dev tunnel '{tunnelId}'. Exit code {exitCode}: {error}

What it means

Aspire's DevTunnels integration wraps the `devtunnel` CLI. DeletePortAsync runs `devtunnel delete port` and parses JSON output; if the CLI exits non-zero (so no result object could be deserialized), the client throws this DistributedApplicationException carrying the exit code and CLI error text.

Solutions

  1. Verify the tunnel exists and the port number matches one you created with CreatePortAsync/SetAccessAsync before deleting
  2. Run `devtunnel status` or re-login with `devtunnel user login` if exit code indicates auth failure
  3. Log/inspect the trailing error text in the message; it is the CLI's stderr explaining the real cause
  4. Wrap DeletePortAsync in try-catch for DistributedApplicationException if deletion is best-effort cleanup

Example fix

// before
client.DeletePortAsync("my-tunnel", 8080); // throws if tunnel/port gone
// after
var access = await client.GetAccessAsync("my-tunnel");
if (access.Ports.Any(p => p.PortNumber == 8080))
{
    await client.DeletePortAsync("my-tunnel", 8080);
}
Defensive patterns

Strategy: try-catch

Validate before calling

var tunnels = await client.ListTunnelsAsync(logger, ct);
bool portExists = tunnels.Any(t => t.TunnelId == tunnelId); // plus port check via GetAccessAsync
if (!portExists) return;

Type guard

static bool IsValidTunnelId(string? id) => !string.IsNullOrEmpty(id) && id.All(c => char.IsAsciiLetterOrDigit(c) || c == '-');

Try / catch

try { await client.DeletePortAsync(tunnelId, port, logger, ct); }
catch (DistributedApplicationException ex) { logger.LogWarning(ex, "Port {Port} on {Tunnel} already gone", port, tunnelId); }

Prevention

When it happens

Trigger: Calling DeletePortAsync(tunnelId, portNumber) when the tunnel does not exist, the port number was never created on the tunnel, the CLI is not authenticated, or the devtunnel CLI binary fails/misbehaves (non-zero exit with no parseable JSON).

Common situations: Deleting a port after the tunnel was already deleted or expired (max 30 days); a typo'd tunnelId or port; expired devtunnel login; network/service errors returning CLI errors instead of JSON.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/8174b52bdb928350. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting.DevTunnels/DevTunnelCliClient.cs:250

            logger?.LogError("Failed to create port '{PortNumber}' for dev tunnel '{TunnelId}' (attempt {Attempt} of {MaxAttempts}). Exit code {ExitCode}: {Error}", portNumber, tunnelId, attempts, _maxCliAttempts, exitCode, error);
            if (attempts < _maxCliAttempts)
            {
                logger?.LogTrace("Waiting {WaitSeconds} seconds before retrying to create port '{PortNumber}' on dev tunnel '{TunnelId}'", _cliRetryOnErrorDelay.TotalSeconds, portNumber, tunnelId);
                await Task.Delay(_cliRetryOnErrorDelay, cancellationToken).ConfigureAwait(false);
            }
        }

        throw new DistributedApplicationException($"Failed to create port '{portNumber}' for tunnel '{tunnelId}' after {attempts} attempts. Exit code {exitCode}: {error}");
    }

    public async Task<DevTunnelPortDeleteResult> DeletePortAsync(string tunnelId, int portNumber, ILogger? logger = default, CancellationToken cancellationToken = default)
    {
        logger?.LogTrace("Deleting port '{PortNumber}' on dev tunnel '{TunnelId}'.", portNumber, tunnelId);
        var (result, exitCode, error) = await CallCliAsJsonAsync<DevTunnelPortDeleteResult>(
            (stdout, stderr, log, ct) => _cli.DeletePortAsync(tunnelId, portNumber, stdout, stderr, log, ct),
            logger, cancellationToken).ConfigureAwait(false);
        return result ?? throw new DistributedApplicationException($"Failed to delete port '{portNumber}' on dev tunnel '{tunnelId}'. Exit code {exitCode}: {error}");
    }

    public async Task<DevTunnelAccessStatus> GetAccessAsync(string tunnelId, int? portNumber = null, ILogger? logger = default, CancellationToken cancellationToken = default)
    {
        logger?.LogTrace("Getting access details for {PortInfo}dev tunnel '{TunnelId}'.", portNumber.HasValue ? $"port '{portNumber}' on " : string.Empty, tunnelId);
        var (access, exitCode, error) = await CallCliAsJsonAsync<DevTunnelAccessStatus>(
            (stdout, stderr, log, ct) => _cli.ListAccessAsync(tunnelId, portNumber, stdout, stderr, log, ct),
            logger, cancellationToken).ConfigureAwait(false);
        return access ?? throw new DistributedApplicationException($"Failed to get access details for '{tunnelId}'{(portNumber.HasValue ? $" port {portNumber}" : "")}. Exit code {exitCode}: {error}");
    }

    public async Task<UserLoginStatus> GetUserLoginStatusAsync(ILogger? logger = default, CancellationToken cancellationToken = default)
    {
        logger?.LogTrace("Getting dev tunnel user login status.");
        var (login, exitCode, error) = await CallCliAsJsonAsync<UserLoginStatus>(
            _cli.UserStatusAsync,
            logger, cancellationToken).ConfigureAwait(false);
        return login ?? throw new DistributedApplicationException($"Failed to get user login status. Exit code {exitCode}: {error}");

View on GitHub (pinned to 25830f84bd)