Jan Kowalski
Jan Kowalski

Reputation: 45

SignalR js client is not able to start connection, even if logs are showing that connection is being made (only LongPooling works)

I am struggling with configuring and using signalR with .net core mvc 6. The purpose for the signalR hub is to send messages to js clients after invoking method in C# controller (js client is React application configured in MVC as ClientApp).

I enabled debugging for both client signalR instance and asp.net

here are the logs from ASP.NET:

      SPA proxy is ready. Redirecting to https://localhost:44440.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionManager[1]
      New connection ctA6QHwS4fvVGcufYvtlAA created.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[10]
      Sending negotiation response.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[4]
      Establishing new connection.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionHandler[5]
      OnConnectedAsync started.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubProtocolResolver[2]
      Found protocol implementation for requested protocol: json.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionContext[1]
      Completed connection handshake. Using HubProtocol 'json'.
connected!! ctA6QHwS4fvVGcufYvtlAA

and corresponding to them logs with js client:

Utils.ts:194 [2022-02-03T18:40:17.568Z] Debug: Starting HubConnection.
Utils.ts:194 [2022-02-03T18:40:17.568Z] Debug: Starting connection with transfer format 'Text'.
Utils.ts:194 [2022-02-03T18:40:17.576Z] Debug: Sending negotiation request: https://localhost:44440/hubs/order/negotiate?negotiateVersion=1.
Utils.ts:194 [2022-02-03T18:40:21.741Z] Debug: Skipping transport 'WebSockets' because it was disabled by the client.
Utils.ts:194 [2022-02-03T18:40:21.742Z] Debug: Selecting transport 'ServerSentEvents'.
Utils.ts:194 [2022-02-03T18:40:21.742Z] Trace: (SSE transport) Connecting.
Utils.ts:190 [2022-02-03T18:40:25.857Z] Information: SSE connected to https://localhost:44440/hubs/order?id=fxqgKpJnF5Dq5MX-RCfXcg
Utils.ts:194 [2022-02-03T18:40:25.857Z] Debug: The HttpConnection connected successfully.
Utils.ts:194 [2022-02-03T18:40:25.857Z] Debug: Sending handshake request.
Utils.ts:194 [2022-02-03T18:40:25.858Z] Trace: (SSE transport) sending data. String data of length 32.
Utils.ts:194 [2022-02-03T18:40:29.969Z] Trace: (SSE transport) request complete. Response status: 200.
Utils.ts:190 [2022-02-03T18:40:29.978Z] Information: Using HubProtocol 'json'.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HttpConnection.stopConnection(undefined) called while in state Disconnecting.
index.js:1 [2022-02-03T18:40:59.997Z] Error: Connection disconnected with error 'Error: Server timeout elapsed without receiving a message from the server.'.
console.<computed> @ index.js:1
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HubConnection.connectionClosed(Error: Server timeout elapsed without receiving a message from the server.) called while in state Connecting.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: Hub handshake failed with error 'Error: Server timeout elapsed without receiving a message from the server.' during start(). Stopping HubConnection.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: Call to HttpConnection.stop(Error: Server timeout elapsed without receiving a message from the server.) ignored because the connection is already in the disconnected state.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HubConnection failed to start successfully because of error 'Error: Server timeout elapsed without receiving a message from the server.'.

here is sample code from js client application:

    console.log("hub attached");

    const hubConnection = new HubConnectionBuilder()
      .withUrl(OrderHubUrl, {
        transport: HttpTransportType.ServerSentEvents,
        accessTokenFactory: () => user.accessToken ?? "",
      })
      .withAutomaticReconnect()
      .configureLogging(LogLevel.Trace)
      .build();

    this.dispatcher.state.saveState("hubConnection", hubConnection);

    const startConnection = async () => {
      try {
        await hubConnection.start();
        console.log("connected");
      } catch (e) {
        this.dispatcher.dispatch(Event.ShowModal, {
          actionName: "OK",
          header: "Error",
          content: e,
        });
      }
    };
    hubConnection.on("ReceiveMessage", (user, message) => {
      console.log("message received");
      console.log(user);
      console.log(message);
    });

    hubConnection.onreconnecting((e) => console.log("reconnecting", e));
    hubConnection.onreconnected((e) => console.log("reconnected", e));
    startConnection();
  } 

as you can see from logs on the top, signalR js client is not able to pass through start method. Instead after some time it throws an error message.

below is my Program.cs file, where i configured signalR (i am suspecting that maybe something wrong is here)

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using OrderMaker.Authentication.Helpers;
using OrderMaker.Authentication.Services;
using OrderMaker.Entities;
using OrderMaker.Modules.Common.Services;
using OrderMaker.Modules.Order.Hubs;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddSignalR(c =>
{
    c.EnableDetailedErrors = true;
    c.ClientTimeoutInterval = TimeSpan.MaxValue;
    c.KeepAliveInterval = TimeSpan.MaxValue;
});

ConfigureConfiguration(builder.Configuration);
ConfigureServices(builder.Services, builder.Configuration);

var app = builder.Build();

app.UseAuthentication();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    // 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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCors(
          x => x
          .AllowAnyMethod()
          .AllowAnyHeader()
          .AllowCredentials());

app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");

app.MapHub<OrderHub>("/hubs/order");

app.Run();


void ConfigureConfiguration(ConfigurationManager configuration)
{
    using var client = new OrderMakerContext();
    client.Database.EnsureCreated();
}

void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
    services.AddControllers();
    // configure strongly typed settings objects
    var appSettingsSection = configuration.GetSection("AppSettings");
    services.Configure<AppSettings>(appSettingsSection);
    // configure jwt authentication
    var appSettings = appSettingsSection.Get<AppSettings>();
    var key = Encoding.ASCII.GetBytes(appSettings.Secret);
    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
        x.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                context.Token = context.Request.Cookies["order_maker_token"];
                // If the request is for our hub...
                var path = context.HttpContext.Request.Path;
                var accessToken = context.Request.Query["access_token"];
                if (!string.IsNullOrEmpty(accessToken) &&
                    (path.StartsWithSegments("/hubs/")))
                {
                    // Read the token out of the query string
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    }
    );

    services.AddScoped<IUserService, UserService>();
    services.AddSingleton<ILogService, LogService>();
}


definition of my hub:

public class OrderHub : Hub
    {
        public async Task SendNotification(List<OrderModel> message)
        {
            await Clients.All.SendAsync("ReceiveMessage", message);
        }
        public override async Task OnConnectedAsync()
        {
            await base.OnConnectedAsync();
            Console.WriteLine("connected!! " + Context.ConnectionId);
        }
        public override async Task OnDisconnectedAsync(Exception exception)
        {
            await base.OnDisconnectedAsync(exception);
            Console.WriteLine("disconnected!!");
        }
    }

And sample controller from where i want to send message to clients:

  private readonly IHubContext<OrderHub> _orderHubContext;
        public OrdersController(IHubContext<OrderHub> orderHubContext)
        {
            _orderHubContext = orderHubContext;
        }

       
        [Route("api/[controller]/")]
        [HttpPost]
        //[Authorize(Roles = $"{Role.Admin},{Role.Manager},{Role.Employee}")]
        public async Task<IActionResult> Post([FromBody] List<OrderModel> model)
        {
               /// some code for creating entities etc
               db.SaveChanges();
               /// notification to all clients that something new was added
               Console.Write(_orderHubContext.Clients.All.ToString());
               await _orderHubContext.Clients.All.SendAsync("ReceiveMessage", "hi there");
                }           
            return Ok(new MessageResponse("Order added succesfully."));
    }

basically i am lost, i spend two days already figuring out what may cause the issues, but i just can't make this thing working. I would really appreciate any sugestions or help. I tried to disable firewall, use different browser, etc without any success. Connection to hub is being made, c# app see that new connection but js client simply stuck in start() method for a while and throws an error message 'Error: Server timeout elapsed without receiving a message from the server.'.

Update: when i explicitly set type of transport in js clint to LongPolling hub is working as intended, but this is not ideal solution.

--- Update --- All of those issues are happening only on local machine. I tried to check my luck and deploy to production app with transport fixed to SSE and it works without any issuess, as well as WebSocket transport. The only clue i have is that on localhost app is using kestrel and on server when i am hosting my app is using IIS.

Upvotes: 0

Views: 5907

Answers (2)

user2410689
user2410689

Reputation:

This is related to the SPA Proxy that .NET is setting up.

The message I get is as follows: Utils.ts:193

      [2022-09-29T17:09:19.866Z] Error: Failed to complete negotiation with the server: Error: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /hub/negotiate</pre>
</body>
</html>
: Status code '404' Either this is not a SignalR endpoint or there is a proxy blocking the connection.

What happens, likely, is that the proxy routes traffic from your spa to the port specified here in your csproj file eg. https://localhost:44423 You should not initiate a websocket to this port this is -not- proxied.

In my case, the dotnet served port is in reality https://localhost:7088 (properties/launchsettings.json)

So modify your Cors settings... like below, if the spaproxy e.g. is at 44423 and the .net service port at 7088.

 services.AddCors(options =>
        {
            options.AddPolicy("ClientPermission", policy =>
            {
                policy.AllowAnyHeader()
                    .AllowAnyMethod()
                    .WithOrigins("https://localhost:44423", "https://localhost:7088")
                    .AllowCredentials();
            });
        });

Next is your js code looks like this e.g.:

 const newConnection = new HubConnectionBuilder()
        .withUrl('https://localhost:7088/hub/myhub')
        .withAutomaticReconnect()
        .build();

For me this solved it.

Upvotes: 1

Jason Pan
Jason Pan

Reputation: 22082

You could try to set the serverTimeout and KeepAliveInterval in your program.cs:

builder.Services.AddSignalR(c =>
{
    c.EnableDetailedErrors = true;
    c.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    c.KeepAliveInterval = TimeSpan.FromSeconds(15);
});

Reason:

If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing KeepAliveInterval, change the ServerTimeout or serverTimeoutInMilliseconds setting on the client. The recommended ServerTimeout or serverTimeoutInMilliseconds value is double the KeepAliveInterval value. So,you'd better not set the value of KeepAliveInterval as max value.

Upvotes: 0

Related Questions