{"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/294cab2f9b2e03af6b953820c7ab497c3c8b7ad9/src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/HubConnection.java#L356-L392","documentation":"A RuntimeException thrown by startNegotiate when the server keeps returning redirect URLs and the negotiate attempt count reaches MAX_NEGOTIATE_ATTEMPTS (100). SignalR supports negotiate redirection (e.g. for sticky sessions or regional redirect), but unbounded redirects indicate a misconfiguration or a redirect loop, so the client caps attempts.","triggerScenarios":"The /negotiate response includes a Url (redirect) field on every attempt, causing startNegotiate to recurse with attempt+1 until negotiateAttempts >= 100, at which point the error is thrown. Typical of a redirect loop where the target redirects back to the origin.","commonSituations":"A misconfigured reverse proxy or load balancer that always returns a redirect on negotiate (e.g. Azure SignalR Service or backplane misconfiguration). An HTTPS<->HTTP redirect loop. A sticky-session cookie problem causing repeated re-negotiation. The server's redirect URL is unreachable or self-referential.","solutions":["Inspect the negotiate response (network trace) to see the redirect chain and identify the loop source.","Fix the reverse proxy / load balancer / Azure SignalR Service configuration so negotiate stabilizes (no redirect or a single redirect to a valid endpoint).","Ensure the base URL scheme/host is correct (https vs http, correct region) to avoid bouncing redirects.","If using Azure SignalR Service, verify the connection string and endpoint configuration."],"exampleFix":"// before: misconfigured base URL causes redirect loop on negotiate\nHubConnection conn = HubConnectionBuilder.create(\"http://app.example.com/chat\").build();\n\n// after: point directly at the correct SignalR endpoint, no redirect loop\nHubConnection conn = HubConnectionBuilder.create(\"https://signalr.example.com/chat\").build();","handlingStrategy":"retry","validationCode":"// Pre-validate the negotiate redirect chain (manual check) before relying on the client.\n// Inspect one negotiate response: if it always redirects to the same host, suspect a loop.\nNegotiateResponse r = probeNegotiate(url);\nif (r.getRedirectUrl() != null && r.getRedirectUrl().equals(url)) {\n    logger.error(\"Self-referential negotiate redirect detected; fix server/proxy config\");\n}","typeGuard":null,"tryCatchPattern":"connection.start().subscribe(onComplete, error -> {\n    if (error.getMessage().contains(\"redirection limit exceeded\")) {\n        // do not retry blindly; fix server/proxy config first\n        logger.error(\"Negotiate redirect loop; check reverse proxy / Azure SignalR config\", error);\n    }\n});","preventionTips":["Inspect the negotiate response chain (curl -L or a network trace) to find the loop source.","For Azure SignalR Service, verify the endpoint and connection string are correct and not bouncing between regions.","Ensure HTTPS is used consistently to avoid http<->https redirect loops.","Do not auto-retry start() indefinitely when a redirect loop is the cause."],"tags":["signalr","java","network","negotiate","redirect","configuration"],"analyzedSha":"294cab2f9b2e03af6b953820c7ab497c3c8b7ad9","analyzedAt":"2026-08-06T20:08:02.189Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}