dotnet/aspnetcore · error · Error
Negotiation can only be skipped when using the WebSocket tra
Error message
Negotiation can only be skipped when using the WebSocket transport directly.
What it means
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.
Source
Thrown at src/SignalR/clients/ts/signalr/src/HttpConnection.ts:262
this._connectionToken = undefined;
this._connectionUrl = undefined;
this._initialTokenLifetimeInSeconds = undefined;
this._transportAccessTokenFromServer = false;
delete this.features.authenticationRefresh;
this._accessTokenFactory = this._options.accessTokenFactory;
this._httpClient._accessTokenFactory = this._accessTokenFactory;
this._httpClient.setRefreshAccessTokenFactory(this._options.accessTokenFactory);
try {
if (this._options.skipNegotiation) {
if (this._options.transport === HttpTransportType.WebSockets) {
// No need to add a connection ID in this case
this.transport = this._constructTransport(HttpTransportType.WebSockets);
// We should just call connect directly in this case.
// No fallback or negotiate in this case.
await this._startTransport(url, transferFormat);
} else {
throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");
}
} else {
let negotiateResponse: INegotiateResponse | null = null;
let redirects = 0;
do {
negotiateResponse = await this._getNegotiationResponse(url);
// the user tries to stop the connection when it is being started
if (this._connectionState === ConnectionState.Disconnecting || this._connectionState === ConnectionState.Disconnected) {
throw new AbortError("The connection was stopped during negotiation.");
}
if (negotiateResponse.error) {
throw new Error(negotiateResponse.error);
}
if ((negotiateResponse as any).ProtocolVersion) {
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.");View on GitHub (pinned to 294cab2f9b)
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.
Example fix
// before
new HubConnectionBuilder()
.withUrl(url, { skipNegotiation: true, transport: HttpTransportType.ServerSentEvents })
.build();
// after
new HubConnectionBuilder()
.withUrl(url, { transport: HttpTransportType.ServerSentEvents })
.build(); Defensive patterns
Strategy: validation
Validate before calling
function assertSkipNegotiationValid(opts) {
if (opts.skipNegotiation && opts.transport !== HttpTransportType.WebSockets) {
throw new Error('skipNegotiation requires transport=WebSockets');
}
}
assertSkipNegotiationValid(options); Type guard
function isValidSkipNegotiation(opts: IHttpConnectionOptions): boolean {
return !opts.skipNegotiation || opts.transport === HttpTransportType.WebSockets;
} Try / catch
try { await conn.start(transferFormat); }
catch (e) {
if (/Negotiation can only be skipped/.test(String(e))) {
options.skipNegotiation = false;
return new HttpConnection(url, options).start(transferFormat);
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Negotiation can only be skipped when using the WebSocket tra
- There were no compatible transports on the server.
- 'WebSocket' is not supported in your environment.
- Negotiate redirection limit exceeded.
- Circuit options have already been configured.
AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06).
Data as JSON: /api/errors/0ec22952ecf5b772.
Report an issue: GitHub.