dotnet/aspnetcore · error · Error
Negotiation can only be skipped when using the WebSocket…
Error message
Negotiation can only be skipped when using the WebSocket transport directly.
What it means
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.
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.
Example fix
// before
.withUrl(url, {
skipNegotiation: true,
transport: signalR.HttpTransportType.ServerSentEvents,
})
// after — option A: drop skipNegotiation
.withUrl(url, { transport: signalR.HttpTransportType.ServerSentEvents })
// after — option B: keep skipNegotiation, use WebSockets
.withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }) Defensive patterns
Strategy: validation
Validate before calling
import { HttpTransportType } from "@microsoft/signalr";
function assertSkipNegotiationCompatible(opts: signalR.IHttpConnectionOptions): void {
if (opts.skipNegotiation && opts.transport !== HttpTransportType.WebSockets) {
throw new Error("skipNegotiation requires transport === WebSockets");
}
} Type guard
function canSkipNegotiation(opts: signalR.IHttpConnectionOptions): boolean {
return !opts.skipNegotiation || opts.transport === signalR.HttpTransportType.WebSockets;
} Try / catch
try { await connection.start(); }
catch (e) {
if (e instanceof Error && /Negotiation can only be skipped/.test(e.message)) {
// drop skipNegotiation or switch transport
opts.skipNegotiation = false;
return newConnection(opts);
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- negotiateResponse.error
- 'WebSocket' is not supported in your environment.
- 'EventSource' is not supported in your environment.
- Negotiation can only be skipped when using the WebSocket…
- There were no compatible transports on the server.
AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11).
Data as JSON: /api/errors/0ec22952ecf5b772.
Report an issue: GitHub.
Appendix: 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 3600ca084e)