dotnet/aspnetcore · error · RuntimeException
Negotiate redirection limit exceeded.
Error message
Negotiate redirection limit exceeded.
What it means
Thrown by startNegotiate when the server returns a redirect URL and the negotiate attempt count has reached MAX_NEGOTIATE_ATTEMPTS (100). SignalR negotiate can redirect (e.g. for Azure SignalR Service routing); the client follows each redirect and re-negotiates, but caps the chain at 100 to avoid loops. Exceeding the cap means redirects are not converging on a final endpoint.
Solutions
- Inspect the redirect chain (URLs and attempt count) - a loop or oscillation points to the misconfigured endpoint.
- Fix the server/infrastructure so negotiate returns a terminal response (no redirect) within a small number of hops.
- Verify Azure SignalR Service / reverse proxy routing rules do not create cycles.
- If you genuinely need many redirects, that is a defect to fix server-side rather than raise the cap.
Example fix
// before: two endpoints redirect to each other // A/negotiate -> Location: B/negotiate -> Location: A/negotiate -> ... throws after 100 // after: configure endpoints so negotiate at A returns a terminal response // (fix server-side routing / Azure SignalR Service config; no client change needed).
Defensive patterns
Strategy: try-catch
Validate before calling
// No client-side validation prevents a server-driven redirect loop.
// In test/staging, probe the negotiate endpoint and count redirects manually:
int hops = countNegotiateRedirects(url); // curl -i loop in your env
if (hops > 5) { /* investigate server/Azure SignalR routing config */ } Type guard
// Remote redirect chain cannot be type-guarded. // Log each redirect URL to detect loops or oscillation.
Try / catch
try {
conn.start().blockingAwait();
} catch (RuntimeException e) {
if ("Negotiate redirection limit exceeded.".equals(e.getMessage())) {
// inspect server/Azure SignalR Service routing; fix the loop server-side
} else throw e;
} Prevention
- Audit Azure SignalR Service / reverse-proxy routing rules for cycles.
- Confirm each negotiate endpoint returns a terminal response in a small number of hops.
- Log the redirect chain during staging to catch loops before production.
When it happens
Trigger: Each negotiate response carries getRedirectUrl() != null and the client follows it; after 100 such redirects without a terminal response, startNegotiate throws. Caused by a redirect loop, a misconfigured multi-endpoint setup, or an infrastructure layer that keeps bouncing the client.
Common situations: Azure SignalR Service misconfigured with a redirect loop between endpoints; a load balancer/proxy rewriting Location into another redirect; multiple SignalR services pointing at each other; a server bug always returning a redirect.
Related errors
- Error reading NegotiateResponse
- ${negotiateResponse.getError()}
- Negotiation can only be skipped when using the WebSocket…
- There were no compatible transports on the server.
- Unexpected status code returned from negotiate
AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11).
Data as JSON: /api/errors/cece881db27f27e7.
Report an issue: GitHub.
Appendix: 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 3600ca084e)