Search code examples
c#signalrasp.net-core-3.1signalr.clientasp.net-core-signalr

SignalR error 404 on negotiate with asp.net application and subfolder


I am using ASP.NET Core 3.1, with SignalR 3.1.9:

  • Microsoft.AspNetCore.SignalR.Common 3.1.9
  • Microsoft.AspNetCore.SignalR.Core 1.1.0
  • Microsoft.AspNetCore.SignalR.Protocols.Json 3.1.9

I am using the Javscript client v3.1.9 (libman.json file):

    {
      "provider": "unpkg",
      "library": "@microsoft/signalr@3.1.9",
      "destination": "wwwroot/lib/signalr/",
      "files": [
        "dist/browser/signalr.js",
        "dist/browser/signalr.min.js"
      ]
    }

On my webserver, the root (example.com) is used by Wordpress for the front-end. Wordpress allows requests to /core with some modifications in the web.config, and the website loads 100% correctly.

To host my .NET Core app, I created an Application that points to the sub-folder core (example.com/core). I don't know if it is important, but all my controllers are under the area "app" (example.com/core/app).

I declared a new Hub:

    public class NotificationsHub : Hub
    {
        private readonly IMainDataService _data;
        private readonly ILogger<NotificationsHub> _logger;

        public NotificationsHub(IMainDataService data, ILoggerFactory loggerFactory)
        {
            this._data = data;
            this._logger = loggerFactory.CreateLogger<NotificationsHub>();
        }

        public async Task SendFriendNotification(Guid newFriendId, Guid currentUserId)
        {
            var currentUser = await this._data.GetUserByIdAsync(currentUserId);
            var notificationRecipientId = newFriendId.ToString();
            await Clients.User(notificationRecipientId).SendAsync("FriendRequestReceived", newFriendId, currentUserId, currentUser.FirstName, currentUser.LastName);
        }

This is my Startup.cs (shortened to the essential):

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
                options.ConsentCookie.Expiration = TimeSpan.FromDays(365);
            });

            services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C");

            services.AddControllersWithViews()
                .AddMvcLocalization()
                .AddMicrosoftIdentityUI();

            services.AddRazorPages();
            services.AddSignalR();
            services.AddRouting();

            services.AddOptions();

            services.Configure<OpenIdConnectOptions>(Configuration.GetSection("AzureAdB2C"));

            services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

            services.AddDistributedMemoryCache();

            services.AddSession(options =>
            {
                options.Cookie.IsEssential = true;
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ...

            if ((env.IsDevelopment() || env.IsStaging() || env.IsProduction()) && !env.IsEnvironment("Localhost"))
            {
                app.UsePathBase("/core");
            }

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();

                endpoints.MapControllerRoute(
                    name: "areas",
                    pattern: "{area:exists}/{controller=Map}/{action=Index}/{id?}"
                );
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{area=App}/{controller=Map}/{action=Index}"
                );
                endpoints.MapControllerRoute(
                    name: "profile",
                    pattern: "{area=App}/{controller=Profile}/{action=Index}/{id?}"
                );

                endpoints.MapRazorPages();

                endpoints.MapDbLocalizationAdminUI();
                endpoints.MapDbLocalizationClientsideProvider();

                if (env.EnvironmentName == "Localhost")
                {
                    endpoints.MapHub<NotificationsHub>("/notificationshub");
                }
                else
                {
                    endpoints.MapHub<NotificationsHub>("/core/notificationshub");
                }
            });
        }

In local, as I am directly running my .NET Core app at the root, there is no problem. But when I deploy to Dev/Staging/Prod, where the sub-folder /core is used, I always get an error 404 on the following URL: https://www.example.com/core/notificationshub/negotiate?negotiateVersion=1

I also tried:

In both cases, I get a 404 exception. The 404 is always followed by the following line in the F12 console:

Error: Failed to complete negotiation with the server: Error: Not Found
Error: Failed to start the connection: Error: Not Found
Error: Not Found

The 3rd line above points to my JavaScript client:

"use strict";

var currentUrl = window.location.href;

var basePath = '';
if (currentUrl.includes("/core") == true) {
    basePath = '/core'
}

var connection = new signalR.HubConnectionBuilder().withUrl(basePath + "/notificationshub").build();

connection.start().catch(function (err) {
    return console.error(err.toString());
});

The error is on the line connection.start().

I tried to modified the previous line with ../ before the path, but it didn't help:

let connection = new signalR.HubConnectionBuilder().withUrl("../" + basePath + "/notificationshub").build();

Any idea if I am missing something? It seems straightforward to have SignalR working on the root URL, but not if there is a sub-folder.


Solution

  • It looks like you're mapping your app to "/core" and then mapping your hub to "/core/hub". So your hub is actually at "/core/core/hub".