OrchardCMS/OrchardCore · error · Error
Negotiate redirection limit exceeded.
Error message
Negotiate redirection limit exceeded.
What it means
The negotiate loop follows server-issued redirects (negotiateResponse.url) up to MAX_REDIRECTS times; if the limit is reached and the response still carries a redirect url, the client gives up with this error. It protects against infinite redirect loops between servers/load balancers during the negotiate handshake.
Solutions
- Inspect the sequence of /negotiate requests (DevTools/network logs) to find the redirect loop and fix the server that keeps redirecting.
- Ensure at least one server in the chain returns a terminal negotiate response (no url field).
- Fix load balancer/sticky session (ARR affinity) configuration so negotiate lands on a node that serves the connection.
- Remove redirect logic that points back to the originating URL.
Example fix
// server before: each node always redirects
return Ok(new { url = $"https://{otherNode}/hub" });
// server after: only one node redirects; terminal node omits url
return Ok(new {
connectionId,
availableTransports = /* ... */
// no 'url' field
}); Defensive patterns
Strategy: try-catch
Try / catch
try {
await connection.start();
} catch (err) {
if (err.message.includes("Negotiate redirection limit exceeded")) {
console.error("Negotiate redirect loop detected — inspect load balancer/server redirects");
alertOps("signalr-negotiate-redirect-loop");
} else { throw err; }
} Prevention
- Test negotiate behavior behind load balancers before production rollout.
- Ensure sticky sessions / ARR affinity are configured for multi-node farms.
- Monitor negotiate request chains in APM to catch loops early.
- Only issue redirects from negotiate when a terminal node exists.
When it happens
Trigger: The negotiate endpoint (or a chain of servers) repeatedly returns { url: ... } responses exceeding MAX_REDIRECTS — e.g. two load-balanced nodes redirecting to each other, or a server emitting a redirect that points back to itself.
Common situations: Misconfigured load balancers or reverse proxies ping-ponging negotiate requests; multi-server backplanes with per-node redirect logic without a terminating node; sticky-session misconfiguration causing each node to redirect to another.
Related errors
- Negotiation can only be skipped when using the WebSocket…
- The ' ' argument is required.
- The ' ' argument should not be empty.
- Unknown value: .
- could not find global
AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13).
Data as JSON: /api/errors/73294657a8af5834.
Report an issue: GitHub.
Appendix: source
Thrown at src/OrchardCore.Modules/OrchardCore.SignalR/wwwroot/Scripts/signalr.js:2867
if (negotiateResponse.ProtocolVersion) {
throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");
}
if (negotiateResponse.url) {
url = negotiateResponse.url;
}
if (negotiateResponse.accessToken) {
// Replace the current access token factory with one that uses
// the returned access token
const accessToken = negotiateResponse.accessToken;
this._accessTokenFactory = () => accessToken;
// set the factory to undefined so the AccessTokenHttpClient won't retry with the same token, since we know it won't change until a connection restart
this._httpClient._accessToken = accessToken;
this._httpClient._accessTokenFactory = undefined;
}
redirects++;
} while (negotiateResponse.url && redirects < MAX_REDIRECTS);
if (redirects === MAX_REDIRECTS && negotiateResponse.url) {
throw new Error("Negotiate redirection limit exceeded.");
}
await this._createTransport(url, this._options.transport, negotiateResponse, transferFormat);
}
if (this.transport instanceof LongPollingTransport) {
this.features.inherentKeepAlive = true;
}
if (this._connectionState === "Connecting" /* ConnectionState.Connecting */) {
// Ensure the connection transitions to the connected state prior to completing this.startInternalPromise.
// start() will handle the case when stop was called and startInternal exits still in the disconnecting state.
this._logger.log(LogLevel.Debug, "The HttpConnection connected successfully.");
this._connectionState = "Connected" /* ConnectionState.Connected */;
}
// stop() is waiting on us via this.startInternalPromise so keep this.transport around so it can clean up.
// This is the only case startInternal can exit in neither the connected nor disconnected state because stopConnection()
// will transition to the disconnected state. start() will wait for the transition using the stopPromise.
}
catch (e) {
this._logger.log(LogLevel.Error, "Failed to start the connection: " + e);View on GitHub (pinned to 4306c0717f)