{"record":{"id":"133fd2b56afdb61f","repo":"dotnet/aspnetcore","slug":"negotiate-redirection-limit-exceeded","errorCode":null,"errorMessage":"Negotiate redirection limit exceeded.","messagePattern":"Negotiate redirection limit exceeded\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/SignalR/clients/ts/signalr/src/HttpConnection.ts","lineNumber":298,"sourceCode":"                        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.\");\n                    }\n\n                    if (negotiateResponse.url) {\n                        url = negotiateResponse.url;\n                    }\n\n                    if (negotiateResponse.accessToken) {\n                        // Replace the current access token factory with one that uses\n                        // the returned access token\n                        this._setTransportAccessToken(negotiateResponse.accessToken);\n                    }\n\n                    redirects++;\n                }\n                while (negotiateResponse.url && redirects < MAX_REDIRECTS);\n\n                if (redirects === MAX_REDIRECTS && negotiateResponse.url) {\n                    throw new Error(\"Negotiate redirection limit exceeded.\");\n                }\n\n                const finalNegotiateResponse = await this._createTransport(url, this._options.transport, negotiateResponse, transferFormat);\n                this._configureAuthenticationRefresh(finalNegotiateResponse);\n            }\n\n            if (this.transport instanceof LongPollingTransport) {\n                this.features.inherentKeepAlive = true;\n            }\n\n            if (this._connectionState === ConnectionState.Connecting) {\n                // Ensure the connection transitions to the connected state prior to completing this.startInternalPromise.\n                // start() will handle the case when stop was called and startInternal exits still in the disconnecting state.\n                this._logger.log(LogLevel.Debug, \"The HttpConnection connected successfully.\");\n                this._connectionState = ConnectionState.Connected;\n            }\n\n            // stop() is waiting on us via this.startInternalPromise so keep this.transport around so it can clean up.","sourceCodeStart":280,"sourceCodeEnd":316,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/3600ca084e9c8b5f4174fc5e747f4c52d2100806/src/SignalR/clients/ts/signalr/src/HttpConnection.ts#L280-L316","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const MAX_REDIRECTS = 100;\nasync function negotiateWithoutLoop(url: string) {\n  let next = url; let hops = 0;\n  while (hops++ < MAX_REDIRECTS) {\n    const r = await fetch(`${next}/negotiate?negotiateVersion=1`, { method: \"POST\" });\n    const body = await r.json();\n    if (!body.url) return body;\n    next = body.url;\n  }\n  throw new Error(\"Negotiate redirect loop detected\");\n}","typeGuard":"function isNegotiateRedirect(v: unknown): v is { url: string } {\n  return typeof v === \"object\" && v !== null && typeof (v as any).url === \"string\";\n}","tryCatchPattern":"try { await connection.start(); }\ncatch (e) {\n  if (e instanceof Error && /redirection limit exceeded/.test(e.message)) {\n    // inspect the chain server-side; this is a routing problem\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["negotiation","redirect","azure-signalr","load-balancer"],"backgroundTag":null,"analyzedSha":"3600ca084e9c8b5f4174fc5e747f4c52d2100806","analyzedAt":"2026-08-11T16:32:30.678Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}