OrchardCMS/OrchardCore · 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
HttpConnection.startInternal only skips server negotiation when options.skipNegotiation is true AND the resolved transport is exactly WebSockets. Any other combination (skipNegotiation with SSE or LongPolling, or with an unresolved/default transport) throws this error, because only WebSockets can connect directly to a hub endpoint without a negotiate handshake.
Solutions
- Set transport: signalR.HttpTransportType.WebSockets together with skipNegotiation: true.
- Alternatively remove skipNegotiation: true and let the standard negotiate flow run.
- Ensure the server also has negotiation enabled if you keep skipNegotiation off.
- If the server only supports non-WebSocket transports, do not skip negotiation.
Example fix
// before
new signalR.HubConnectionBuilder()
.withUrl("/hub", { skipNegotiation: true }) // transport not pinned
.build();
// after
new signalR.HubConnectionBuilder()
.withUrl("/hub", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.build(); Defensive patterns
Strategy: validation
Validate before calling
function assertSkipNegotiationValid(options) {
if (options.skipNegotiation && options.transport !== signalR.HttpTransportType.WebSockets) {
throw new Error("skipNegotiation requires transport: HttpTransportType.WebSockets");
}
} Type guard
function canSkipNegotiation(options) {
return options.skipNegotiation === true &&
options.transport === signalR.HttpTransportType.WebSockets;
} Try / catch
try {
await connection.start();
} catch (err) {
if (err.message.includes("Negotiation can only be skipped")) {
console.error("Add transport: HttpTransportType.WebSockets when skipNegotiation is true");
} else { throw err; }
} Prevention
- Never set skipNegotiation: true without also pinning transport to WebSockets.
- Remember the server also needs negotiation disabled policy or matching config.
- Document in your connection factory why skipNegotiation is used.
- Prefer the default negotiate flow unless you specifically need to avoid it.
When it happens
Trigger: Calling withUrl(url, { skipNegotiation: true }) while options.transport is HttpTransportType.ServerSentEvents, HttpTransportType.LongPolling, left as default (auto-negotiate), or set to a non-WebSocket combination.
Common situations: Developers enabling skipNegotiation to work around sticky-session/load-balancer issues without pinning transport: WebSockets; Azure App Service or environments where SSE is the negotiated transport; copied config from a WebSockets-only sample applied to an auto transport.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- The HubConnection url must be a valid url.
- Negotiate redirection limit exceeded.
- 'WebSocket' is not supported in your environment.
- The 'OrchardCore.SignalR.Azure' and…
- A feature is missing a mandatory 'Id' property in the Module
AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13).
Data as JSON: /api/errors/7af410b37a1f929e.
Report an issue: GitHub.
Appendix: source
Thrown at src/OrchardCore.Modules/OrchardCore.SignalR/wwwroot/Scripts/signalr.js:2834
}
}
async _startInternal(transferFormat) {
// Store the original base url and the access token factory since they may change
// as part of negotiating
let url = this.baseUrl;
this._accessTokenFactory = this._options.accessTokenFactory;
this._httpClient._accessTokenFactory = this._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 = 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 === "Disconnecting" /* ConnectionState.Disconnecting */ || this._connectionState === "Disconnected" /* ConnectionState.Disconnected */) {
throw new AbortError("The connection was stopped during negotiation.");
}
if (negotiateResponse.error) {
throw new Error(negotiateResponse.error);
}
if (negotiateResponse.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.");
}
if (negotiateResponse.url) {View on GitHub (pinned to 4306c0717f)