Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Since upgrading to Angular 9 Signalr doesn't work

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"
  }
}
like image 582
LanceM Avatar asked Oct 16 '22 05:10

LanceM


1 Answers

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.

like image 181
LanceM Avatar answered Oct 18 '22 23:10

LanceM