dotnet/aspnetcore · error · RuntimeException

Negotiate redirection limit exceeded.

Error message

Negotiate redirection limit exceeded.

What it means

A RuntimeException thrown by startNegotiate when the server keeps returning redirect URLs and the negotiate attempt count reaches MAX_NEGOTIATE_ATTEMPTS (100). SignalR supports negotiate redirection (e.g. for sticky sessions or regional redirect), but unbounded redirects indicate a misconfiguration or a redirect loop, so the client caps attempts.

Source

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

                }

                localStart.onError(error);
            });
        } finally {
            this.state.lock.unlock();
        }

        return localStart;
    }

    private Single<NegotiateResponse> startNegotiate(String url, int negotiateAttempts, Map<String, String> localHeaders) {
        if (this.state.getHubConnectionState() != HubConnectionState.CONNECTING) {
            throw new RuntimeException("HubConnection trying to negotiate when not in the CONNECTING state.");
        }

        return handleNegotiate(url, localHeaders).flatMap(response -> {
            if (response.getRedirectUrl() != null && negotiateAttempts >= MAX_NEGOTIATE_ATTEMPTS) {
                throw new RuntimeException("Negotiate redirection limit exceeded.");
            }

            if (response.getRedirectUrl() == null) {
                Set<String> transports = response.getAvailableTransports();
                if (this.transportEnum == TransportEnum.ALL) {
                    if (transports.contains("WebSockets")) {
                        response.setChosenTransport(TransportEnum.WEBSOCKETS);
                    } else if (transports.contains("LongPolling")) {
                        response.setChosenTransport(TransportEnum.LONG_POLLING);
                    } else {
                        throw new RuntimeException("There were no compatible transports on the server.");
                    }
                } else if (this.transportEnum == TransportEnum.WEBSOCKETS && !transports.contains("WebSockets") ||
                        (this.transportEnum == TransportEnum.LONG_POLLING && !transports.contains("LongPolling"))) {
                    throw new RuntimeException("There were no compatible transports on the server.");
                } else {
                    response.setChosenTransport(this.transportEnum);
                }

View on GitHub (pinned to 294cab2f9b)

Solutions

  1. Inspect the negotiate response (network trace) to see the redirect chain and identify the loop source.
  2. Fix the reverse proxy / load balancer / Azure SignalR Service configuration so negotiate stabilizes (no redirect or a single redirect to a valid endpoint).
  3. Ensure the base URL scheme/host is correct (https vs http, correct region) to avoid bouncing redirects.
  4. If using Azure SignalR Service, verify the connection string and endpoint configuration.

Example fix

// before: misconfigured base URL causes redirect loop on negotiate
HubConnection conn = HubConnectionBuilder.create("http://app.example.com/chat").build();

// after: point directly at the correct SignalR endpoint, no redirect loop
HubConnection conn = HubConnectionBuilder.create("https://signalr.example.com/chat").build();
Defensive patterns

Strategy: retry

Validate before calling

// Pre-validate the negotiate redirect chain (manual check) before relying on the client.
// Inspect one negotiate response: if it always redirects to the same host, suspect a loop.
NegotiateResponse r = probeNegotiate(url);
if (r.getRedirectUrl() != null && r.getRedirectUrl().equals(url)) {
    logger.error("Self-referential negotiate redirect detected; fix server/proxy config");
}

Try / catch

connection.start().subscribe(onComplete, error -> {
    if (error.getMessage().contains("redirection limit exceeded")) {
        // do not retry blindly; fix server/proxy config first
        logger.error("Negotiate redirect loop; check reverse proxy / Azure SignalR config", error);
    }
});

Prevention

When it happens

Trigger: The /negotiate response includes a Url (redirect) field on every attempt, causing startNegotiate to recurse with attempt+1 until negotiateAttempts >= 100, at which point the error is thrown. Typical of a redirect loop where the target redirects back to the origin.

Common situations: A misconfigured reverse proxy or load balancer that always returns a redirect on negotiate (e.g. Azure SignalR Service or backplane misconfiguration). An HTTPS<->HTTP redirect loop. A sticky-session cookie problem causing repeated re-negotiation. The server's redirect URL is unreachable or self-referential.

Related errors


AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06). Data as JSON: /api/errors/cece881db27f27e7. Report an issue: GitHub.