{"record":{"id":"cece881db27f27e7","repo":"dotnet/aspnetcore","slug":"negotiate-redirection-limit-exceeded-cece88","errorCode":null,"errorMessage":"Negotiate redirection limit exceeded.","messagePattern":"Negotiate redirection limit exceeded\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java","lineNumber":374,"sourceCode":"                }\n\n                localStart.onError(error);\n            });\n        } finally {\n            this.state.lock.unlock();\n        }\n\n        return localStart;\n    }\n\n    private Single<NegotiateResponse> startNegotiate(String url, int negotiateAttempts, Map<String, String> localHeaders) {\n        if (this.state.getHubConnectionState() != HubConnectionState.CONNECTING) {\n            throw new RuntimeException(\"HubConnection trying to negotiate when not in the CONNECTING state.\");\n        }\n\n        return handleNegotiate(url, localHeaders).flatMap(response -> {\n            if (response.getRedirectUrl() != null && negotiateAttempts >= MAX_NEGOTIATE_ATTEMPTS) {\n                throw new RuntimeException(\"Negotiate redirection limit exceeded.\");\n            }\n\n            if (response.getRedirectUrl() == null) {\n                Set<String> transports = response.getAvailableTransports();\n                if (this.transportEnum == TransportEnum.ALL) {\n                    if (transports.contains(\"WebSockets\")) {\n                        response.setChosenTransport(TransportEnum.WEBSOCKETS);\n                    } else if (transports.contains(\"LongPolling\")) {\n                        response.setChosenTransport(TransportEnum.LONG_POLLING);\n                    } else {\n                        throw new RuntimeException(\"There were no compatible transports on the server.\");\n                    }\n                } else if (this.transportEnum == TransportEnum.WEBSOCKETS && !transports.contains(\"WebSockets\") ||\n                        (this.transportEnum == TransportEnum.LONG_POLLING && !transports.contains(\"LongPolling\"))) {\n                    throw new RuntimeException(\"There were no compatible transports on the server.\");\n                } else {\n                    response.setChosenTransport(this.transportEnum);\n                }","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/3600ca084e9c8b5f4174fc5e747f4c52d2100806/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java#L356-L392","documentation":"Thrown by startNegotiate when the server returns a redirect URL and the negotiate attempt count has reached MAX_NEGOTIATE_ATTEMPTS (100). SignalR negotiate can redirect (e.g. for Azure SignalR Service routing); the client follows each redirect and re-negotiates, but caps the chain at 100 to avoid loops. Exceeding the cap means redirects are not converging on a final endpoint.","triggerScenarios":"Each negotiate response carries getRedirectUrl() != null and the client follows it; after 100 such redirects without a terminal response, startNegotiate throws. Caused by a redirect loop, a misconfigured multi-endpoint setup, or an infrastructure layer that keeps bouncing the client.","commonSituations":"Azure SignalR Service misconfigured with a redirect loop between endpoints; a load balancer/proxy rewriting Location into another redirect; multiple SignalR services pointing at each other; a server bug always returning a redirect.","solutions":["Inspect the redirect chain (URLs and attempt count) - a loop or oscillation points to the misconfigured endpoint.","Fix the server/infrastructure so negotiate returns a terminal response (no redirect) within a small number of hops.","Verify Azure SignalR Service / reverse proxy routing rules do not create cycles.","If you genuinely need many redirects, that is a defect to fix server-side rather than raise the cap."],"exampleFix":"// before: two endpoints redirect to each other\n// A/negotiate -> Location: B/negotiate -> Location: A/negotiate -> ... throws after 100\n\n// after: configure endpoints so negotiate at A returns a terminal response\n// (fix server-side routing / Azure SignalR Service config; no client change needed).","handlingStrategy":"try-catch","validationCode":"// No client-side validation prevents a server-driven redirect loop.\n// In test/staging, probe the negotiate endpoint and count redirects manually:\nint hops = countNegotiateRedirects(url); // curl -i loop in your env\nif (hops > 5) { /* investigate server/Azure SignalR routing config */ }","typeGuard":"// Remote redirect chain cannot be type-guarded.\n// Log each redirect URL to detect loops or oscillation.","tryCatchPattern":"try {\n  conn.start().blockingAwait();\n} catch (RuntimeException e) {\n  if (\"Negotiate redirection limit exceeded.\".equals(e.getMessage())) {\n    // inspect server/Azure SignalR Service routing; fix the loop server-side\n  } else throw e;\n}","preventionTips":["Audit Azure SignalR Service / reverse-proxy routing rules for cycles.","Confirm each negotiate endpoint returns a terminal response in a small number of hops.","Log the redirect chain during staging to catch loops before production."],"tags":["signalr","java","network","negotiate","redirect","configuration"],"backgroundTag":null,"analyzedSha":"3600ca084e9c8b5f4174fc5e747f4c52d2100806","analyzedAt":"2026-08-11T16:32:30.678Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}