microsoft/aspire · critical · InvalidOperationException

AppHost server process exited before the RPC connection…

Error message

AppHost server process exited before the RPC connection could be established. Exit code: {code}.

What it means

Thrown by AppHostServerSession.GetRpcClientAsync when the AppHost server process exits before the CLI's RPC connection attempt succeeds. The session races serverExitTask against connectTask; if the exit task wins, it cancels the connection and throws with the server's exit code when available, so the root cause (server crash) is surfaced instead of an opaque connect timeout.

Solutions

  1. Inspect the server process stdout/stderr captured by the session for the startup error
  2. Fix the AppHost build/runtime error causing the exit (missing SDK, compile error)
  3. Verify the server launch arguments and environment (DOTNET_ROOT etc.) are correct
  4. Check the exit code reported in the message and correlate with server logs

Example fix

// before
var session = await AppHostServerSession.CreateAsync(badProjectPath); // exits code 1
// after
dotnet build MyApp.AppHost.csproj // fix compile errors first
var session = await AppHostServerSession.CreateAsync(projectPath);
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the AppHost builds before launching the server session
var buildResult = await Process.RunAsync("dotnet", $"build {appHostProjectPath}");
if (buildResult.ExitCode != 0)
    throw new InvalidOperationException("AppHost build failed; fix before starting a session.");

Try / catch

try
{
    var rpc = await session.GetRpcClientAsync(ct);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("exited before"))
{
    var log = session.CapturedServerOutput;
    logger.LogError(ex, "Server exited early. Output: {Output}", log);
}

Prevention

When it happens

Trigger: Calling GetRpcClientAsync when the spawned server process terminates (crash, bad args, port/env problems) while ConnectToServerAsync is still retrying.

Common situations: AppHost project fails to build at startup, missing SDK or runtime on the machine, invalid command-line arguments passed to the server, unhandled exception in server startup, or the process killed by the OS.

Related errors


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

Appendix: source

Thrown at src/Aspire.Cli/Projects/AppHostServerSession.cs:331

        var connectTask = AppHostRpcClient.ConnectAsync(socketPath, _authenticationToken, _environment, _profilingTelemetry, connectCts.Token);
        var completedTask = await Task.WhenAny(connectTask, serverExitTask).ConfigureAwait(false);

        if (completedTask == connectTask)
        {
            // Stop the process-exit watcher once the RPC connection wins the race.
            connectCts.Cancel();
            ObserveFaultedTask(serverExitTask);
            _rpcClient = await connectTask.ConfigureAwait(false);
            return _rpcClient;
        }

        await serverExitTask.ConfigureAwait(false);
        // Stop the retrying connection attempt once the server has exited, then observe any
        // cancellation/failure it reports so the losing task cannot raise an unobserved exception.
        connectCts.Cancel();
        ObserveFaultedTask(connectTask);
        var exitCode = TryGetServerExitCode();
        throw new InvalidOperationException(
            exitCode is { } code
                ? $"AppHost server process exited before the RPC connection could be established. Exit code: {code}."
                : "AppHost server process exited before the RPC connection could be established.");
    }

    public async ValueTask DisposeAsync()
    {
        // Acquire _startGate the same way StartAsync does so dispose is serialized against an
        // in-flight start: we either flip _disposed before StartAsync acquires the gate (it then
        // throws ObjectDisposedException and spawns nothing) or after it has fully published the
        // execution + run task (we then tear that down below). The gate is released immediately;
        // the teardown runs outside it so a late StartAsync observes _disposed and bails fast.
        bool alreadyDisposed;
        await _startGate.WaitAsync().ConfigureAwait(false);
        try
        {
            alreadyDisposed = _disposed;
            _disposed = true;

View on GitHub (pinned to 25830f84bd)