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

  1. Inspect the redirect chain (each negotiateResponse.url) to find the loop participant.
  2. Fix the server/load-balancer/Azure SignalR routing so negotiate resolves on a single host.
  3. If using a custom negotiate URL resolver, ensure it terminates instead of redirecting indefinitely.
  4. 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

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


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)