Repository navigation
DevTunnel resource always reports Unhealthy due to 'devtunnel access list <id> -p <port>' failing to resolve bare tunnel id #18790
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
tunnelIdis passed toAddDevTunnel(name, tunnelId: "..."), and - no
tunnelIdis 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
- 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();
aspire start/aspire run.- 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). - Observe in
aspire describe(or the dashboard) that theDevTunnelresource staysUnhealthy, 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. - Confirm the tunnel is actually working:
curlthe publichttps://<code>-<port>.<cluster>.devtunnels.msURL — it correctly proxies to the local resource. - Manually reproduce the failing health-check call:
Note it returns
devtunnel access list <bare-id> -p <port> --jsonTunnel service error: Not Found, whiledevtunnel access list <bare-id>.<cluster> -p <port> --jsonsucceeds.
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
-ptoaccess listwhen only checking overall tunnel/port reachability, or - Fall back to
devtunnel port show <id> -p <port>(confirmed working) instead ofaccess 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 toeuwservice endpoint, tunnels get provisioned ineun1) - OS: Windows
- TargetFramework: net10.0
Activity
perkops commentedon Jul 23, 2026
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
devtunnelCLI 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 ineun1(North Europe), so it returns "Not Found". The cluster-qualified ID resolves correctly.Deterministic repro on my setup (same tunnel, same instant):
devtunnel show <bare>anddevtunnel port list <bare>(no-p) both resolve the bare ID fine — the failure only appears when-pis combined with a bare ID whose tunnel lives in a non-home cluster.Second manifestation:
CreatePortAsyncfails at startup (same root cause, different command)Because
Aspire.Hosting.DevTunnelspasses bare IDs, the same routing quirk also breaks port creation at resource startup, not just the health-checkaccess listcall this issue documents. The resource fails to start: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-followingport create(<bare>+-p) still fails "Not Found":So exactly like the
access list <bare> -pcase in this issue: commands that take a bare ID without-p(create,port list) resolve fine, but a command combining the bare ID with-pis misrouted to the home cluster and returns "Not Found". Same root cause, same fix.Suggested fix
Have
Aspire.Hosting.DevTunnelsuse the cluster-qualified tunnel ID (returned astunnelIdin thedevtunnel createJSON, e.g.devtunnel-xxxxxxxx.eun1) for all CLI calls that combine the ID with-p— bothCreatePortAsyncand the health check'saccess list. That resolves both paths deterministically. As a defensive measure, retry-on-Not Foundimmediately aftercreatewould also cover the propagation window.Environment
Aspire.Hosting.DevTunnels13.4.6devtunnelCLI 1.0.1972+07cc55c789euw(West Europe); tunnels consistently provisioned ineun1(North Europe) — including brand-new ones.perkops commentedon Aug 20, 2026
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
cpizzato commentedon Aug 24, 2026
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.
chore(deps): Bump the nuget-minor-patch group with 4 updates (#905)
Bump the aspire group with 3 updates (#151)
deps: Bump Aspire.Hosting.AppHost and Scalar.AspNetCore (#96)
chore(deps): Bump the dotnet group with 30 updates (#16)
chore(deps): Bump Aspire.Hosting.AppHost from 13.5.3 to 13.5.4 (#835)