{"record":{"id":"0ec22952ecf5b772","repo":"dotnet/aspnetcore","slug":"negotiation-can-only-be-skipped-when-using-the-web","errorCode":null,"errorMessage":"Negotiation can only be skipped when using the WebSocket transport directly.","messagePattern":"Negotiation can only be skipped when using the WebSocket transport directly\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/SignalR/clients/ts/signalr/src/HttpConnection.ts","lineNumber":262,"sourceCode":"        this._connectionToken = undefined;\n        this._connectionUrl = undefined;\n        this._initialTokenLifetimeInSeconds = undefined;\n        this._transportAccessTokenFromServer = false;\n        delete this.features.authenticationRefresh;\n        this._accessTokenFactory = this._options.accessTokenFactory;\n        this._httpClient._accessTokenFactory = this._accessTokenFactory;\n        this._httpClient.setRefreshAccessTokenFactory(this._options.accessTokenFactory);\n\n        try {\n            if (this._options.skipNegotiation) {\n                if (this._options.transport === HttpTransportType.WebSockets) {\n                    // No need to add a connection ID in this case\n                    this.transport = this._constructTransport(HttpTransportType.WebSockets);\n                    // We should just call connect directly in this case.\n                    // No fallback or negotiate in this case.\n                    await this._startTransport(url, transferFormat);\n                } else {\n                    throw new Error(\"Negotiation can only be skipped when using the WebSocket transport directly.\");\n                }\n            } else {\n                let negotiateResponse: INegotiateResponse | null = null;\n                let redirects = 0;\n\n                do {\n                    negotiateResponse = await this._getNegotiationResponse(url);\n                    // the user tries to stop the connection when it is being started\n                    if (this._connectionState === ConnectionState.Disconnecting || this._connectionState === ConnectionState.Disconnected) {\n                        throw new AbortError(\"The connection was stopped during negotiation.\");\n                    }\n\n                    if (negotiateResponse.error) {\n                        throw new Error(negotiateResponse.error);\n                    }\n\n                    if ((negotiateResponse as any).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.\");","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/294cab2f9b2e03af6b953820c7ab497c3c8b7ad9/src/SignalR/clients/ts/signalr/src/HttpConnection.ts#L244-L280","documentation":"Thrown from _startInternal (HttpConnection.ts:262) when `skipNegotiation: true` is set in IHttpConnectionOptions but the requested transport is not WebSockets. Negotiation can only be skipped for the WebSocket transport because it does not need the connection token that negotiate provides; all other transports (ServerSentEvents, LongPolling) require the token returned by negotiate to construct their connect URL.","triggerScenarios":"Calling `withUrl(url, { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents })` or `LongPolling`, or setting `skipNegotiation: true` with `transport` undefined while the server's first available transport happens to be non-WebSocket (though in that path negotiate runs anyway). The literal throw fires when the explicit transport option is not WebSockets.","commonSituations":"Copy-pasting a config that used WebSockets and forgetting to remove skipNegotiation after switching to SSE/LongPolling; trying to reduce latency by skipping negotiate without understanding it only works for WebSockets; mixing options objects across transports.","solutions":["Set `skipNegotiation: true` ONLY together with `transport: HttpTransportType.WebSockets`.","Remove `skipNegotiation` from the options when using ServerSentEvents or LongPolling.","If you need SSE/LongPolling, leave negotiate enabled so the connection token is fetched."],"exampleFix":"// before\nnew HubConnectionBuilder()\n  .withUrl(url, { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents })\n  .build();\n\n// after\nnew HubConnectionBuilder()\n  .withUrl(url, { transport: HttpTransportType.ServerSentEvents })\n  .build();","handlingStrategy":"validation","validationCode":"function assertSkipNegotiationValid(opts) {\n  if (opts.skipNegotiation && opts.transport !== HttpTransportType.WebSockets) {\n    throw new Error('skipNegotiation requires transport=WebSockets');\n  }\n}\nassertSkipNegotiationValid(options);","typeGuard":"function isValidSkipNegotiation(opts: IHttpConnectionOptions): boolean {\n  return !opts.skipNegotiation || opts.transport === HttpTransportType.WebSockets;\n}","tryCatchPattern":"try { await conn.start(transferFormat); }\ncatch (e) {\n  if (/Negotiation can only be skipped/.test(String(e))) {\n    options.skipNegotiation = false;\n    return new HttpConnection(url, options).start(transferFormat);\n  }\n  throw e;\n}","preventionTips":["Treat skipNegotiation as a WebSocket-only optimization.","Don't share the same options object across different transports.","Document the constraint in your connection-config helper."],"tags":["configuration","transport","websocket","negotiate"],"analyzedSha":"294cab2f9b2e03af6b953820c7ab497c3c8b7ad9","analyzedAt":"2026-08-06T20:08:02.189Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}