CLI: Browser debug start/stop logs critical connection-reset errors
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues.
### Describe the bug
Successful one-shot CLI resource commands can leave their auxiliary AppHost connection open until process exit. On Windows, the AppHost then observes a connection reset and emits `JsonRpc Critical: 13`, followed by repeated exception stack traces.
This is visible when starting and stopping browser debugging through the Aspire VS Code panel. The resource reaches the expected **Running** and **Finished** states, but the accompanying logs suggest a serious failure.
The behavior also reproduces with direct CLI invocations against an isolated auxiliary-RPC server, without VS Code or a browser.
### Expected Behavior
A one-shot CLI command should release its **owned** auxiliary connection cleanly before exiting, including on failure or cancellation.
Successful resource operations should not generate critical connection-reset messages. Borrowed connections must remain available to their owner, and genuine transport failures should remain diagnosable.
### Steps To Reproduce
1. On Windows ARM64, prepare and open the repository's [`playground/BlazorHosted`](https://github.com/microsoft/aspire/tree/e01d15dae24cb7aab0bb873ebad99383fdacc570/playground/BlazorHosted) sample in VS Code, configured to use Chrome for browser debugging.
2. Start the AppHost in debug mode and wait for `blazorapp` to reach **Running**.
3. Execute **Debug in Browser**, then **Stop Browser Debug**, from the resource's Commands menu.
4. Repeat and inspect the AppHost's Debug Console output.
The resource starts and stops successfully, but the output includes critical auxiliary-backchannel disconnect errors.
The extension invokes the following CLI path for these operations:
```text
aspire resource --apphost --non-interactive
```
### Exceptions (if any)
```text
JsonRpc Critical: 13 : Connection closing
(StreamError: Reading JSON RPC from the stream failed with IOException:
Unable to read data from the transport connection:
An existing connection was forcibly closed by the remote host.)
System.Net.Sockets.SocketException (10054):
An existing connection was forcibly closed by the remote host.
Aspire.Hosting.Backchannel.AuxiliaryBackchannelService:
Debug: Client disconnected from auxiliary backchannel
```
Here, `13` is StreamJsonRpc's event ID for a closed connection, not an error count.
The same log shows successful resource transitions:
```text
Starting -> Running
Resource 'blazorapp-wasm-debugger' is ready.
Running -> Stopping
Stopping -> Finished
Executable 'blazorapp-wasm-debugger-...' was stopped.
```
### Aspire doctor output
Not captured for this report.
### Anything else?
**Identified ownership gap**
The explicit `--apphost` path in [AppHostConnectionResolver](https://github.com/microsoft/aspire/blob/e01d15dae24cb7aab0bb873ebad99383fdacc570/src/Aspire.Cli/Backchannel/AppHostConnectionResolver.cs#L145-L165) creates a new `AppHostAuxiliaryBackchannel` directly. This connection is not registered with the monitor's connection collection.
[ResourceCommand](https://github.com/microsoft/aspire/blob/e01d15dae24cb7aab0bb873ebad99383fdacc570/src/Aspire.Cli/Commands/ResourceCommand.cs#L143-L178) uses it without disposing it. Consequently, no component deterministically closes this connection before the CLI process exits.
The resolver's discovery path can instead return a **borrowed, monitor-owned connection**. Both paths currently return the same result type without an explicit ownership contract, so blindly disposing every returned connection would be unsafe.
**Proposed solution**
Make resolver results ownership-aware and provide deterministic asynchronous cleanup. For example, introduce explicit factories:
```csharp
// Newly created connection: the result owns cleanup.
AppHostConnectionResult.Owned(connection);
// Existing monitor connection: the result borrows it.
AppHostConnectionResult.Borrowed(connection);
```
Commands would scope the result with `await using`:
```diff
- var result = await _connectionResolver.ResolveConnectionAsync(
+ await using var result = await _connectionResolver.ResolveConnectionAsync(
```
For owned connections, asynchronous disposal should request RPC disposal and observe its completion before process exit. Cleanup should be bounded and idempotent, remain effective when the command token is already canceled, and preserve the original command failure if cleanup also fails.
Audit other resolver consumers for the same lifetime gap. Borrowed connections should remain open until their owner disposes them.
**Reproduction and prototype evidence**
Experiments used real CLI executables and Unix-domain sockets on Windows ARM64, with an isolated auxiliary-RPC server. Every command below succeeded and exited with code `0`.
| Variant | Result |
|---|---|
| Unmodified stable CLI 13.5.3 | 5/5 connection resets with Critical event 13 |
| Source CLI 13.6.0-dev; relevant code matches main `e01d15d` | 5/5 connection resets with Critical event 13 |
| Temporary CLI prototype explicitly disposing the connection | 20/20 clean disconnects |
| Temporary CLI prototype disposing and awaiting RPC completion | 20/20 clean disconnects |
The server used StreamJsonRpc 2.25.29; the source CLI used 2.23.32-alpha. The source CLI was built at `b0995b833cb830760342c0d3002cfa6341f1299b`; the relevant connection and command implementations are unchanged on the compared main revision `e01d15dae24cb7aab0bb873ebad99383fdacc570`.
A separate low-level immediate-exit experiment exposed a race with synchronous `Dispose()` alone, supporting awaited cleanup.
StreamJsonRpc emits the Critical trace when it classifies the reset as `StreamError`, before Aspire catches the exception as an expected disconnect. Preventing the reset is preferable to suppressing that diagnostic.
**Acceptance criteria**
- Owned connections are released on success, validation failure, RPC failure, and cancellation.
- Borrowed connections remain usable.
- Successful one-shot commands produce clean peer disconnects without Critical event 13.
- Cleanup neither hangs process exit nor obscures the original command error.
- Browser stop/start and cross-platform behavior are validated.
The tested prototype is isolated; no production fix has been implemented.
Contributor guide
Research direction
Start with AppHostConnectionResolver.cs and ResourceCommand.cs at the linked locations, then audit other resolver consumers and their connection lifetimes. Reproduce the one-shot resource command against the isolated auxiliary-RPC server or browser-debug flow. Done means owned connections cleanly disconnect on success and failure, borrowed connections remain usable, and no Critical event 13 appears.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- cli, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100