{"record":{"id":"73294657a8af5834","repo":"OrchardCMS/OrchardCore","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/OrchardCore.Modules/OrchardCore.SignalR/wwwroot/Scripts/signalr.js","lineNumber":2867,"sourceCode":"                    if (negotiateResponse.ProtocolVersion) {\n                        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                    if (negotiateResponse.url) {\n                        url = negotiateResponse.url;\n                    }\n                    if (negotiateResponse.accessToken) {\n                        // Replace the current access token factory with one that uses\n                        // the returned access token\n                        const accessToken = negotiateResponse.accessToken;\n                        this._accessTokenFactory = () => accessToken;\n                        // 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\n                        this._httpClient._accessToken = accessToken;\n                        this._httpClient._accessTokenFactory = undefined;\n                    }\n                    redirects++;\n                } while (negotiateResponse.url && redirects < MAX_REDIRECTS);\n                if (redirects === MAX_REDIRECTS && negotiateResponse.url) {\n                    throw new Error(\"Negotiate redirection limit exceeded.\");\n                }\n                await this._createTransport(url, this._options.transport, negotiateResponse, transferFormat);\n            }\n            if (this.transport instanceof LongPollingTransport) {\n                this.features.inherentKeepAlive = true;\n            }\n            if (this._connectionState === \"Connecting\" /* 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 = \"Connected\" /* ConnectionState.Connected */;\n            }\n            // stop() is waiting on us via this.startInternalPromise so keep this.transport around so it can clean up.\n            // This is the only case startInternal can exit in neither the connected nor disconnected state because stopConnection()\n            // will transition to the disconnected state. start() will wait for the transition using the stopPromise.\n        }\n        catch (e) {\n            this._logger.log(LogLevel.Error, \"Failed to start the connection: \" + e);","sourceCodeStart":2849,"sourceCodeEnd":2885,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore.Modules/OrchardCore.SignalR/wwwroot/Scripts/signalr.js#L2849-L2885","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// server before: each node always redirects\nreturn Ok(new { url = $\"https://{otherNode}/hub\" });\n// server after: only one node redirects; terminal node omits url\nreturn Ok(new {\n  connectionId,\n  availableTransports = /* ... */\n  // no 'url' field\n});","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await connection.start();\n} catch (err) {\n  if (err.message.includes(\"Negotiate redirection limit exceeded\")) {\n    console.error(\"Negotiate redirect loop detected — inspect load balancer/server redirects\");\n    alertOps(\"signalr-negotiate-redirect-loop\");\n  } else { throw err; }\n}","preventionTips":["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."],"tags":["signalr","redirect-loop","negotiation","load-balancer"],"backgroundTag":"redirect-limit-exceeded","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}