Skip to content

DevTunnel resource always reports Unhealthy due to 'devtunnel access list <id> -p <port>' failing to resolve bare tunnel id #18790

Closed

Description

Description

The DevTunnel resource created via Aspire.Hosting.DevTunnels is always reported as Unhealthy in the Aspire dashboard, even though the tunnel is actually running correctly and the public URL is reachable and forwards traffic to the referenced resource as expected.

This reproduces both when:

  • an explicit tunnelId is passed to AddDevTunnel(name, tunnelId: "..."), and
  • no tunnelId is passed and Aspire auto-generates one (e.g. shibboleth-tunnel-af080a54), on a tunnel that never previously existed.

So the issue is not related to reusing/naming a pre-existing tunnel.

Root cause (found via manual repro)

DevTunnelHealthCheck calls DevTunnelCliClient.GetAccessAsync(tunnelId, portNumber, ...), which shells out to:

devtunnel access list <tunnelId> -p <portNumber> --json

using the short/bare tunnel id (the one supplied to/generated by AddDevTunnel, without the cluster suffix).

This command call fails:

devtunnel access list shibboleth-tunnel-af080a54 -p 57877 --json
Tunnel service error: Not Found The requested tunnel was not found.

However, the very same bare id works fine for other CLI operations against the same tunnel:

devtunnel show shibboleth-tunnel-af080a54 --json          # OK
devtunnel port show shibboleth-tunnel-af080a54 -p 57877 --json   # OK
devtunnel access list shibboleth-tunnel-af080a54 --json    # OK (no -p)

And it succeeds if the fully cluster-qualified id is used instead:

devtunnel access list shibboleth-tunnel-af080a54.eun1 -p 57877 --json   # OK

So the combination of a bare tunnel id + -p <port> for devtunnel access list appears to fail to resolve the tunnel on the service side, while the same bare id works for every other subcommand (including access list without -p). Because DevTunnelHealthCheck/DevTunnelCliClient.GetAccessAsync always passes the bare id together with the port number, the health check fails on every run, permanently marking the DevTunnel resource (and downstream DevTunnelPort resource) as Unhealthy/Starting, even though the tunnel is fully functional.

Reproduction steps

  1. Create an AppHost with:
    var builder = DistributedApplication.CreateBuilder(args);
    
    var app = builder.AddContainer("app", "some/image", "latest")
        .WithHttpsEndpoint(targetPort: 443, name: "app-https");
    
    var tunnel = builder.AddDevTunnel("app-tunnel")
        .WithAnonymousAccess()
        .WithReference(app);
    
    builder.Build().Run();
  2. aspire start / aspire run.
  3. Wait for the container to become Healthy and the tunnel to finish hosting (Dev tunnel '...' created / Ready to accept connections... in the tunnel resource logs).
  4. Observe in aspire describe (or the dashboard) that the DevTunnel resource stays Unhealthy, with a health report similar to:
    Failed to check dev tunnel '<id>': Failed to get access details for '<id>' port <port>.
    Exit code 1: Tunnel service error: Not Found The requested tunnel was not found.
    
  5. Confirm the tunnel is actually working: curl the public https://<code>-<port>.<cluster>.devtunnels.ms URL — it correctly proxies to the local resource.
  6. Manually reproduce the failing health-check call:
    devtunnel access list <bare-id> -p <port> --json
    
    Note it returns Tunnel service error: Not Found, while devtunnel access list <bare-id>.<cluster> -p <port> --json succeeds.

Expected behavior

The DevTunnel resource should report Healthy once the tunnel is created and hosting connections are established, matching the tunnel's real, working state.

Actual behavior

The DevTunnel resource is permanently Unhealthy, and the associated DevTunnelPort resource never leaves the Starting state, because the health check's devtunnel access list <id> -p <port> CLI call fails to resolve the tunnel when given a non-cluster-qualified id.

Suggested fix

DevTunnelCliClient/DevTunnelHealthCheck could:

  • Use the cluster-qualified tunnel id (as returned by devtunnel create/devtunnel update, e.g. <id>.<cluster>) for subsequent CLI calls that combine an id with -p <port>, instead of the original bare id, or
  • Avoid passing -p to access list when only checking overall tunnel/port reachability, or
  • Fall back to devtunnel port show <id> -p <port> (confirmed working) instead of access list ... -p <port> for the health check.

Versions used

  • Aspire.AppHost.Sdk / Aspire.Hosting.DevTunnels: 13.4.6
  • Aspire CLI: 13.4.6+87fe259e4fc244c599019a7b1304c85a1488f248
  • devtunnel CLI: 1.0.1972+07cc55c789
  • Tunnel service cluster involved: eun1 (devtunnel CLI reports connecting to euw service endpoint, tunnels get provisioned in eun1)
  • OS: Windows
  • TargetFramework: net10.0

Activity

  1. perkops commented on Jul 23, 2026

    @perkops

    Confirming this on my machine, and reporting what looks like a second manifestation of the same root cause on a different code path.

    Root cause (as this issue describes)

    When the devtunnel CLI is given a bare (non-cluster-qualified) tunnel ID together with the -p <port> flag, the port sub-query is routed to my account's home cluster (euw / West Europe) — but my tunnels are provisioned in eun1 (North Europe), so it returns "Not Found". The cluster-qualified ID resolves correctly.

    Deterministic repro on my setup (same tunnel, same instant):

    $ devtunnel access list devtunnel-xxxxxxxx -p 49612
    Tunnel service error: Not Found The requested tunnel was not found.     # exit 1
    
    $ devtunnel access list devtunnel-xxxxxxxx.eun1 -p 49612
    Access control list for port 49612:                                     # exit 0
    

    devtunnel show <bare> and devtunnel port list <bare> (no -p) both resolve the bare ID fine — the failure only appears when -p is combined with a bare ID whose tunnel lives in a non-home cluster.

    Second manifestation: CreatePortAsync fails at startup (same root cause, different command)

    Because Aspire.Hosting.DevTunnels passes bare IDs, the same routing quirk also breaks port creation at resource startup, not just the health-check access list call this issue documents. The resource fails to start:

    Aspire.Hosting.DistributedApplicationException: Failed to create port '49612' for tunnel
    'devtunnel-xxxxxxxx' after 3 attempts. Exit code 1: Tunnel service error: Not Found
    The requested tunnel was not found.
       at Aspire.Hosting.DevTunnels.DevTunnelCliClient.CreatePortAsync(...) DevTunnelCliClient.cs:line 212
       at Aspire.Hosting.DevTunnelsResourceBuilderExtensions...StartPortAsync ...DevTunnelResourceBuilderExtensions.cs:line 204
    

    This is deterministic, and the log makes clear it is not a missing tunnel/port or a propagation timing issue. On an already-existing, warmed tunnel that already has port 49612, devtunnel port list <bare> (no -p) succeeds and returns the port — yet the immediately-following port create (<bare> + -p) still fails "Not Found":

    Dev tunnel 'devtunnel-xxxxxxxx' access policies cleared successfully.
    Dev tunnel 'devtunnel-xxxxxxxx' created
    Getting port list for dev tunnel 'devtunnel-xxxxxxxx'.
    Invoking devtunnel CLI: devtunnel port list devtunnel-xxxxxxxx --json --nologo
    devtunnel CLI exited with exit code '0'.
      -> { "ports": [ { "portNumber": 49612, "protocol": "https" } ] }     # port list (no -p) FINDS the port
    Error executing command 'start'.
      Failed to create port '49612' for tunnel 'devtunnel-xxxxxxxx' after 3 attempts.
      Exit code 1: Tunnel service error: Not Found  The requested tunnel was not found.   # port create (with -p) FAILS
    

    So exactly like the access list <bare> -p case in this issue: commands that take a bare ID without -p (create, port list) resolve fine, but a command combining the bare ID with -p is misrouted to the home cluster and returns "Not Found". Same root cause, same fix.

    Suggested fix

    Have Aspire.Hosting.DevTunnels use the cluster-qualified tunnel ID (returned as tunnelId in the devtunnel create JSON, e.g. devtunnel-xxxxxxxx.eun1) for all CLI calls that combine the ID with -p — both CreatePortAsync and the health check's access list. That resolves both paths deterministically. As a defensive measure, retry-on-Not Found immediately after create would also cover the propagation window.

    Environment

    • Aspire.Hosting.DevTunnels 13.4.6
    • Aspire CLI 13.2.2
    • devtunnel CLI 1.0.1972+07cc55c789
    • Account home cluster: euw (West Europe); tunnels consistently provisioned in eun1 (North Europe) — including brand-new ones.
    • OS: Windows 11
  2. perkops commented on Aug 20, 2026

    @perkops

    Workaround for me with Aspire 13.5.0 is to set Region to UKSouth in DevTunnelOptions, which doesnt mixup between euw/eun - only sad thing is that dev tunnel url is not visible from UI currently #19496

  3. cpizzato commented on Aug 24, 2026

    @cpizzato

    As Per Kops (@perkops), I was planning to use the feature that enable the region to be set to mitigate this. However/unfortunately, I can't because addDevTunnel polyglot binding is missing region option for Typescript app host. I opened an issue for that (#19495) because I guess this was by mistake, the PR that introduced probably forgot to add the same to AddDevTunnelForPolyglot.

  4. locked and limited conversation to collaborators on Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-integrationsIssues pertaining to Aspire Integrations packages

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions