dotnet/aspnetcore · error · RuntimeException

The HubConnection failed to transition from the

Error message

The HubConnection failed to transition from the '%s' state to the '%s' state because it was actually in the '%s' state.

What it means

changeState(from, to) is a guarded state transition: it only succeeds if the connection is currently exactly in the 'from' state. If the actual state differs (because another thread already moved it, or the caller assumed the wrong starting state), it logs the mismatch and throws a formatted RuntimeException. This protects the state machine from illegal transitions.

Solutions

  1. Do not call the internal changeState directly; rely on start()/stop() and the built-in reconnect.
  2. Serialize lifecycle calls - do not call start() and stop() concurrently or issue overlapping reconnects.
  3. If wrapping reconnect, use a single-threaded executor and an AtomicReference<HubConnectionState> so transitions are owned by one thread.

Example fix

// before - concurrent lifecycle
new Thread(() -> hub.start()).start();
new Thread(() -> hub.stop()).start();

// after - serialized lifecycle
ScheduledExecutorService es = Executors.newSingleThreadExecutor();
es.submit(() -> hub.start().blockingAwait());
es.submit(() -> hub.stop().blockingAwait());
Defensive patterns

Strategy: try-catch

Try / catch

// Internal state-machine throw; surface as a connection failure and reconnect.
hub.onClosed(ex -> {
  if (ex != null && ex.getMessage().contains("failed to transition")) {
    log.warn("Illegal state transition; rebuilding connection");
    rebuildHubAndStart();
  }
});

Prevention

When it happens

Trigger: An internal or plugin code path calls changeState(CONNECTING, CONNECTED) when the state is actually DISCONNECTED (e.g. a late callback after stop()); or two reconnect attempts race and both try to transition from CONNECTING.

Common situations: Concurrent reconnect logic; a transport callback (onClose/onOpen) firing after the user called stop(); bugs in custom retry/reconnect wrappers layered on top of the client.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/5a7267bac6679f28. Report an issue: GitHub.

Appendix: source

Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java:1717

                }
                return this.state;
            } finally {
                this.lock.unlock();
            }
        }

        public HubConnectionState getHubConnectionState() {
            return this.hubConnectionState;
        }

        public void changeState(HubConnectionState from, HubConnectionState to) {
            this.lock.lock();
            try {
                logger.debug("The HubConnection is attempting to transition from the {} state to the {} state.", from, to);
                if (this.hubConnectionState != from) {
                    logger.debug("The HubConnection failed to transition from the {} state to the {} state because it was actually in the {} state.",
                        from, to, this.hubConnectionState);
                    throw new RuntimeException(String.format("The HubConnection failed to transition from the '%s' state to the '%s' state because it was actually in the '%s' state.",
                        from, to, this.hubConnectionState));
                }

                this.hubConnectionState = to;
            } finally {
                this.lock.unlock();
            }
        }

        public void changeState(HubConnectionState to) {
            this.lock.lock();
            try {
                logger.debug("The HubConnection is transitioning from the {} state to the {} state.", this.hubConnectionState, to);
                this.hubConnectionState = to;
            } finally {
                this.lock.unlock();
            }
        }

View on GitHub (pinned to 3600ca084e)