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
- Verify the tunnel exists and the port number matches one you created with CreatePortAsync/SetAccessAsync before deleting
- Run `devtunnel status` or re-login with `devtunnel user login` if exit code indicates auth failure
- Log/inspect the trailing error text in the message; it is the CLI's stderr explaining the real cause
- 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
- Check tunnel/port existence before deleting
- Treat deletion as idempotent cleanup and swallow DistributedApplicationException
- Keep devtunnel CLI updated and logged in
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
- CLI path must be provided
- Failed to get access details for
- Failed to get devtunnel CLI version. Output
- Failed to get user login status. Exit code
- Failed to parse JSON output into type
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)