microsoft/aspire · error · InvalidOperationException

Browser debug connection closed by the remote endpoint with…

Error message

Browser debug connection closed by the remote endpoint with status '{closeStatus}' ({(int)closeStatus}): {result.CloseStatusDescription}

What it means

Thrown by BrowserLogsWebSocketCdpTransport.ReceiveAsync (BrowserLogsCdpConnection.cs:465) when the browser closes the CDP WebSocket instead of sending more messages. It is an InvalidOperationException raised because Aspire expects the browser debug connection to stay open for the lifetime of the tracked browser session, so a Close frame is unexpected and its status/description are surfaced as diagnostics for reconnect logic and resource logs.

Solutions

  1. Check the close status in the message: 1006/abnormal or empty description means the browser process died — inspect the browser's own logs/exit code first.
  2. Ensure the browser is not closed manually while the AppHost is tracking it; keep the browser window open during the session.
  3. Verify no proxy, VPN, or antivirus terminates idle WebSocket connections between the AppHost and the browser's debug port.
  4. If the browser exits immediately, confirm the debug port is not already in use by another browser instance (another instance will exit with 'DevToolsActivePort file doesn't exist' style errors).
  5. Dispose/recreate the BrowserLogsCdpConnection and relaunch the browser to re-establish the connection.

Example fix

// before
var connection = new BrowserLogsCdpConnection(...);
await connection.StreamLogsForeverAsync(ct);

// after
try
{
    await connection.StreamLogsForeverAsync(ct);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("closed by the remote endpoint"))
{
    logger.LogWarning(ex, "Browser CDP connection dropped; relaunching browser.");
    connection = await RelaunchBrowserAsync();
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before relying on a long-lived CDP session, confirm the browser process is alive:
bool IsBrowserAlive(Process? browser) => browser is { HasExited: false };

Type guard

static bool IsUnexpectedClosure(InvalidOperationException ex) =>
    ex.Message.StartsWith("Browser debug connection closed by the remote endpoint", StringComparison.Ordinal);

Try / catch

try
{
    await transport.ReceiveAsync(ct);
}
catch (InvalidOperationException ex) when (IsUnexpectedClosure(ex))
{
    logger.LogWarning(ex, "CDP WebSocket closed by browser; scheduling relaunch.");
    await RelaunchBrowserAndReconnectAsync();
}

Prevention

When it happens

Trigger: The remote browser sends a WebSocket Close frame during _webSocket.ReceiveAsync while the transport is reading CDP events — e.g. the browser process exits or is killed while logs are being streamed over the --remote-debugging-port WebSocket transport.

Common situations: The user closes the browser window; the browser crashes; the AppHost or OS kills the browser (timeout, shutdown); a proxy or firewall terminates the idle WebSocket; browser extensions force a restart of the browser process.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


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

Appendix: source

Thrown at src/Aspire.Hosting.Browsers/BrowserLogsCdpConnection.cs:465

    private readonly TimeSpan _closeTimeout = closeTimeout;
    private readonly WebSocket _webSocket = webSocket;

    public async Task SendAsync(ReadOnlyMemory<byte> frame, CancellationToken cancellationToken)
    {
        await _webSocket.SendAsync(frame, WebSocketMessageType.Text, endOfMessage: true, cancellationToken).ConfigureAwait(false);
    }

    public async Task<byte[]> ReceiveAsync(CancellationToken cancellationToken)
    {
        var buffer = new byte[16 * 1024];
        using var messageBuffer = new MemoryStream();

        while (true)
        {
            var result = await _webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
            if (result.MessageType == WebSocketMessageType.Close)
            {
                throw CreateUnexpectedConnectionClosureException(result);
            }

            // Large CDP events can span multiple websocket frames. Buffer until EndOfMessage so protocol parsing
            // always sees one complete JSON message, matching the frames observed from a real browser.
            messageBuffer.Write(buffer, 0, result.Count);
            if (result.EndOfMessage)
            {
                return messageBuffer.ToArray();
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        try
        {
            if (_webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived)
            {

View on GitHub (pinned to 25830f84bd)