{"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/3600ca084e9c8b5f4174fc5e747f4c52d2100806/src/SignalR/clients/ts/signalr/src/HttpConnection.ts#L244-L280","documentation":"When skipNegotiation is true, SignalR can only skip the /negotiate round-trip for the WebSocket transport, because WebSockets are a single upgraded connection. For ServerSentEvents and LongPolling, negotiation is mandatory to obtain a connectionId and transport token. The client enforces this invariant by throwing if skipNegotiation is combined with a non-WebSocket transport.","triggerScenarios":"Configuring HttpConnection with { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents } or { skipNegotiation: true, transport: HttpTransportType.LongPolling }. Also triggered if transport is auto-select but the only available transports are SSE/LongPolling.","commonSituations":"Developer copied a tutorial that used skipNegotiation with WebSockets but then forced a different transport for a restricted network. Setting skipNegotiation:true as a 'performance' tweak without realizing only WebSockets support it. Auto transport where the server disables WebSockets.","solutions":["Either remove skipNegotiation (let negotiation run), or set transport to HttpTransportType.WebSockets when keeping skipNegotiation.","Confirm the server actually supports WebSockets (enabled in negotiate response transports list) before using skipNegotiation.","If you must use SSE/LongPolling, never set skipNegotiation: true."],"exampleFix":"// before\n.withUrl(url, {\n  skipNegotiation: true,\n  transport: signalR.HttpTransportType.ServerSentEvents,\n})\n\n// after — option A: drop skipNegotiation\n.withUrl(url, { transport: signalR.HttpTransportType.ServerSentEvents })\n// after — option B: keep skipNegotiation, use WebSockets\n.withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets })","handlingStrategy":"validation","validationCode":"import { HttpTransportType } from \"@microsoft/signalr\";\nfunction assertSkipNegotiationCompatible(opts: signalR.IHttpConnectionOptions): void {\n  if (opts.skipNegotiation && opts.transport !== HttpTransportType.WebSockets) {\n    throw new Error(\"skipNegotiation requires transport === WebSockets\");\n  }\n}","typeGuard":"function canSkipNegotiation(opts: signalR.IHttpConnectionOptions): boolean {\n  return !opts.skipNegotiation || opts.transport === signalR.HttpTransportType.WebSockets;\n}","tryCatchPattern":"try { await connection.start(); }\ncatch (e) {\n  if (e instanceof Error && /Negotiation can only be skipped/.test(e.message)) {\n    // drop skipNegotiation or switch transport\n    opts.skipNegotiation = false;\n    return newConnection(opts);\n  }\n  throw e;\n}","preventionTips":["Only pair skipNegotiation:true with transport: WebSockets.","Validate the options object in a helper before constructing the connection.","Confirm the server supports WebSockets before relying on skipNegotiation."],"tags":["negotiation","transport","configuration","websocket"],"backgroundTag":null,"analyzedSha":"3600ca084e9c8b5f4174fc5e747f4c52d2100806","analyzedAt":"2026-08-11T16:32:30.678Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}