I have an angular application that uses signalR. It is hosted in a .NET core 3.1 application. Since upgrading to Angular 9, signalR no longer works. Everything was working fine in Angular 8. Since update, I've been getting errors like
404 Error: Failed to complete negotiation with the server
Failed to start the connection
I am using MessagePackHubProtocol. In the package.json
file I have the following packages:
"@microsoft/signalr": "3.1.2",
"@microsoft/signalr-protocol-msgpack":"3.1.2"
In .NET core I have packages:
Microsoft.AspNetCore.SignalR.Common 3.1.2 Microsoft.AspNetCore.SignalR.Protocols.MessagePack 3.1.2
The Typescript version I'm using is v3.7.5. I have SSL enabled.
I tried adding CORS as stated in this post and I also tried adding the following options to my client application.
{
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
}
Unfortunately, neither worked.
Startup.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Server.IISIntegration;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using System.IO;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public static IConfiguration Configuration
{
get; set;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppConfig>(options =>
{
Configuration.GetSection("AppConfig").Bind(options);
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
services.AddSignalR(o =>
{
o.EnableDetailedErrors = true;
o.MaximumReceiveMessageSize = 1000000;
}).AddMessagePackProtocol();
services.AddControllersWithViews()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.DictionaryKeyPolicy = null;
});
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpContext();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), @"Uploads")),
RequestPath = new PathString("/Uploads")
});
app.Use(async (context, next) =>
{
context.Response.Headers.Remove("X-XSS-Protection");
context.Response.Headers.Add("X-Xss-Protection", "1");
context.Response.Headers.Remove("X-Frame-Options");
context.Response.Headers.Add("X-Frame-Options", "DENY");
context.Response.Headers.Remove("Referrer-Policy");
context.Response.Headers.Add("Referrer-Policy", "no-referrer");
context.Response.Headers.Remove("X-Content-Type-Options");
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
//context.Response.Headers.Remove("Content-Security-Policy");
//context.Response.Headers.Add(
// "Content-Security-Policy",
// "default-src 'self'; " +
// "img-src 'self' myblobacc.blob.core.windows.net; " +
// "font-src 'self'; " +
// "style-src 'self'; " +
// "script-src 'self' 'nonce-KIBdfgEKjb34ueiw567bfkshbvfi4KhtIUE3IWF' 'nonce-rewgljnOIBU3iu2btli4tbllwwe'; " +
// "frame-src 'self';" +
// "connect-src 'self';");
await next();
});
if(!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthentication();
WebSocketOptions webSocketOptions = new WebSocketOptions();
webSocketOptions.AllowedOrigins.Add("*");
app.UseWebSockets(webSocketOptions);
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<AppHub>("/appHub");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if(env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
app.UseCookiePolicy();
}
}
My angular signalR service:
import { Location } from '@angular/common';
import { EventEmitter, Injectable, OnDestroy } from '@angular/core';
import { HubConnection, HubConnectionBuilder, HubConnectionState, IHubProtocol, LogLevel } from '@microsoft/signalr';
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack';
import { Subscription } from 'rxjs';
@Injectable()
export class AppSignalRService implements OnDestroy
{
public connectionEstablished = new EventEmitter<Boolean>();
public startConnectionTimeoutDelay: number = 3000;
public protocol: IHubProtocol = new MessagePackHubProtocol();
public autoReconnect: boolean = true;
private _userId: number = 0;
private _connectionIsEstablished: boolean = false;
private _hubConnection: HubConnection;
private connectedSubscription: Subscription;
constructor(public location: Location)
{
}
createConnection(huburl: string, userId: number)
{
this._userId = userId;
if (!this._hubConnection && this._userId > 0)
{
let hubConnectionBuilder: HubConnectionBuilder = new HubConnectionBuilder();
hubConnectionBuilder.withUrl(this.location.path.name + huburl);
hubConnectionBuilder.withHubProtocol(this.protocol);
hubConnectionBuilder.configureLogging(LogLevel.Information);
if (this.autoReconnect)
{
hubConnectionBuilder.withAutomaticReconnect([0, 1000, 1000, 1000, 1000, 1000, 2000, 5000, 10000, 20000, 30000, null]);
}
this._hubConnection = hubConnectionBuilder.build();
this.hubConnection.onclose((msg) =>
{
console.log(msg.message);
this.startConnection();
});
this.hubConnection.onreconnected((connectionId: string) =>
{
this.hubConnection.invoke("RegisterConnection", this._userId)
});
}
}
startConnection()
{
if (this._hubConnection.state == HubConnectionState.Disconnected)
{
this._hubConnection
.start()
.then(() =>
{
this._connectionIsEstablished = true;
console.log('Hub connection started');
this.connectionEstablished.emit(true);
this.hubConnection.invoke("RegisterConnection", this._userId);
})
.catch(err =>
{
this._connectionIsEstablished = false;
console.log('Error while establishing connection, retrying...');
setTimeout(() =>
{
this.startConnection();
}, this.startConnectionTimeoutDelay);
});
}
}
// ...args: any[] allows you to pass any number of arguments
run(method: string, ...args: any[])
{
switch (this.hubConnection.state)
{
case HubConnectionState.Connected: ;
this.hubConnection.invoke(method, ...args);
break;
case HubConnectionState.Connecting:
this.connectedSubscription = this.connectionEstablished.subscribe((data: any) =>
{
this.hubConnection.invoke(method, ...args);
this.connectedSubscription.unsubscribe();
});
break;
default:
this.hubConnection.start()
.then(() =>
{
this.hubConnection.invoke(method, args)
this.hubConnection.invoke("RegisterConnection", this._userId);
})
.catch(err => console.error(err.toString()));
break;
}
}
get connectionIsEstablished(): boolean
{
return this._connectionIsEstablished;
}
get hubConnection(): HubConnection
{
return this._hubConnection;
}
ngOnDestroy()
{
if (this.connectedSubscription)
{
// prevent memory leak when component destroyed
this.connectedSubscription.unsubscribe();
}
}
}
My package.json:
{
"name": "mic",
"private": true,
"version": "1.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --live-reload=false",
"build": "ng build",
"build:ssr": "ng run MIC:server:dev",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"postcss": {},
"dependencies": {
"@angular/animations": "9.0.2",
"@angular/cdk": "9.0.1",
"@angular/common": "9.0.2",
"@angular/compiler": "9.0.2",
"@angular/core": "9.0.2",
"@angular/flex-layout": "9.0.0-beta.29",
"@angular/forms": "9.0.2",
"@angular/localize": "9.0.2",
"@angular/material": "9.0.1",
"@angular/material-moment-adapter": "9.0.1",
"@angular/platform-browser": "9.0.2",
"@angular/platform-browser-dynamic": "9.0.2",
"@angular/platform-server": "9.0.2",
"@angular/router": "9.0.2",
"@angular-devkit/schematics": "9.0.3",
"@fortawesome/angular-fontawesome": "0.6.0",
"@fortawesome/fontawesome-svg-core": "1.2.27",
"@fortawesome/free-regular-svg-icons": "5.12.1",
"@fortawesome/free-solid-svg-icons": "5.12.1",
"@microsoft/signalr": "3.1.2",
"@microsoft/signalr-protocol-msgpack": "3.1.2",
"@ng-bootstrap/ng-bootstrap": "6.0.0",
"@ngmodule/material-carousel": "0.6.0",
"@ng-select/ng-select": "3.7.2",
"@nguniversal/module-map-ngfactory-loader": "9.0.0-next.9",
"@swimlane/ngx-charts": "13.0.2",
"angular2-notifications": "3.0.0",
"angular-dual-listbox": "5.0.1",
"aspnet-prerendering": "^3.0.1",
"bootstrap": "4.4.1",
"classlist.js": "1.1.20150312",
"core-js": "3.6.4",
"date-fns": "2.9.0",
"hammerjs": "2.0.8",
"hammer-timejs": "1.1.0",
"hash-sum": "2.0.0",
"immutable": "4.0.0-rc.12",
"intl": "1.2.5",
"jquery": "3.4.1",
"jwt-decode": "2.2.0",
"material-design-icons": "3.0.1",
"moment": "^2.24.0",
"ng-validator": "2.0.0",
"ngx-material-timepicker": "^5.3.0",
"oidc-client": "1.10.1",
"popper.js": "1.16.1",
"primeicons": "2.0.0",
"primeng": "9.0.0-rc.4",
"rxjs": "6.5.4",
"tippy.js": "5.2.1",
"tslib": "^1.10.0",
"web-animations-js": "^2.3.2",
"zone.js": "0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.900.3",
"@angular/cli": "9.0.3",
"@angular/compiler-cli": "9.0.2",
"@angular/language-service": "9.0.2",
"@intervolga/optimize-cssnano-plugin": "1.0.6",
"@types/jasmine": "3.5.3",
"@types/jasminewd2": "2.0.8",
"@types/jwt-decode": "2.2.1",
"@types/node": "13.7.4",
"autoprefixer": "9.7.4",
"codelyzer": "5.2.1",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
"karma": "4.4.1",
"karma-chrome-launcher": "3.1.0",
"karma-coverage-istanbul-reporter": "2.1.1",
"karma-jasmine": "3.1.1",
"karma-jasmine-html-reporter": "1.5.2",
"karma-scss-preprocessor": "4.0.0",
"typescript": "3.7.5"
},
"optionalDependencies": {
"node-sass": "4.13.1",
"protractor": "6.0.0",
"tslint": "6.0.0",
"ts-node": "8.6.2"
}
}
I managed to find what was wrong and get signalR working again. In my tsconfig.json file I had the following value in the "compilerOptions" section:
"target": "es2015"
which I changed to:
target": "es5"
This is more a workaround as I would like to use es2015 but for the moment, everything is working and I am happy. If anyone knows how to get it working with es2015, please let me know.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With