dotnet/aspnetcore · error · Error
Negotiate redirection limit exceeded.
Error message
Negotiate redirection limit exceeded.
What it means
During negotiate, if the server returns a 'url' field the client follows the redirect and increments a counter, looping while there is a url and redirects < MAX_REDIRECTS. Once the cap is reached and the response still wants to redirect, the client throws to prevent an infinite redirect loop.
Solutions
- Inspect the redirect chain (each negotiateResponse.url) to find the loop participant.
- Fix the server/load-balancer/Azure SignalR routing so negotiate resolves on a single host.
- If using a custom negotiate URL resolver, ensure it terminates instead of redirecting indefinitely.
- Verify Azure SignalR connection strings are not pointing endpoints at each other.
Defensive patterns
Strategy: validation
Validate before calling
const MAX_REDIRECTS = 100;
async function negotiateWithoutLoop(url: string) {
let next = url; let hops = 0;
while (hops++ < MAX_REDIRECTS) {
const r = await fetch(`${next}/negotiate?negotiateVersion=1`, { method: "POST" });
const body = await r.json();
if (!body.url) return body;
next = body.url;
}
throw new Error("Negotiate redirect loop detected");
} Type guard
function isNegotiateRedirect(v: unknown): v is { url: string } {
return typeof v === "object" && v !== null && typeof (v as any).url === "string";
} Try / catch
try { await connection.start(); }
catch (e) {
if (e instanceof Error && /redirection limit exceeded/.test(e.message)) {
// inspect the chain server-side; this is a routing problem
}
throw e;
} Prevention
- Audit Azure SignalR / load-balancer routing to ensure negotiate terminates on one host.
- If using a custom negotiate URL resolver, guarantee it stops redirecting.
- Trace each negotiateResponse.url during staging to catch loops before production.
When it happens
Trigger: A negotiate response keeps returning a url across more than MAX_REDIRECTS (typically 100) hops. Caused by a misconfigured load balancer, an Azure SignalR Service routing loop, or a server that always redirects negotiate.
Common situations: Multiple Azure SignalR Service instances misconfigured to redirect to each other. A reverse proxy rewrites the negotiate URL to itself. Application code supplying a negotiate URL factory that always emits a new url.
Related errors
- Detected a connection attempt to an ASP.NET SignalR Server…
- Enhanced navigation does not support making a non-GET…
- Negotiate redirection limit exceeded.
- negotiateResponse.error
- Negotiation can only be skipped when using the WebSocket…
AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11).
Data as JSON: /api/errors/133fd2b56afdb61f.
Report an issue: GitHub.
Appendix: source
Thrown at src/SignalR/clients/ts/signalr/src/HttpConnection.ts:298
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
this._setTransportAccessToken(negotiateResponse.accessToken);
}
redirects++;
}
while (negotiateResponse.url && redirects < MAX_REDIRECTS);
if (redirects === MAX_REDIRECTS && negotiateResponse.url) {
throw new Error("Negotiate redirection limit exceeded.");
}
const finalNegotiateResponse = await this._createTransport(url, this._options.transport, negotiateResponse, transferFormat);
this._configureAuthenticationRefresh(finalNegotiateResponse);
}
if (this.transport instanceof LongPollingTransport) {
this.features.inherentKeepAlive = true;
}
if (this._connectionState === 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 = ConnectionState.Connected;
}
// stop() is waiting on us via this.startInternalPromise so keep this.transport around so it can clean up.View on GitHub (pinned to 3600ca084e)