0
votes

I am trying to get basic signalR connectivity going between my react application and a server component written in C#.

I have bootstrapped the signalR client using the JQuery SignalR library and invoking as middleware. On start up, it should try and call the only server method available, which should then cause the hub to be constructed and the method called. However the constructor for the hub doesn't get called, nor does the invoked method. Based on the client connection properties, it is clearly finding the hub and connecting as the onStateChanged() method gets called with "Connected" as the new state, and I can view the connection object in chrome dev tools. However when invoking the server method, it simply gives me the following error in the javascript console (nothing happens on the server process at all)

jquery-1.12.0.min.js:4 POST https://localhost:3830/signalr/send?transport=serverSentEvents&clientProtocol=1.5&connectionToken=AQAAANCMnd8BFdERjHoAwE%2FCl%2BsBAAAAuw4b0U8E80ynfN6Njj7iGAAAAAACAAAAAAADZgAAwAAAABAAAAC2F%2BIFF73jLL%2FLtWBjkBafAAAAAASAAACgAAAAEAAAAIKqx8wP6asrIYRF24wgF%2FwwAAAAJLyt3OCdy%2FIXbOYlu1vSNKLkF%2Fs19QM9yVQb%2FhpfVxavZSxG7uauwc3vXSfZYDFrFAAAAMzCxkLJZxDevB1J8Wqst8IUk0Al&connectionData=%5B%7B%22name%22%3A%22logtailhub%22%7D%5D 500 (Internal Server Error)

The more detailed SignalR error is

Error: Send failed.↵ at Object.error (https://localhost:3830/assets/signalr/jquery.signalR-2.2.0.min.js:8:4512)↵ at Object.transportError (https://localhost:3830/assets/signalr/jquery.signalR-2.2.0.min.js:8:4627)↵ at s (https://localhost:3830/assets/signalr/jquery.signalR-2.2.0.min.js:8:18349)↵ at Object.error (https://localhost:3830/assets/signalr/jquery.signalR-2.2.0.min.js:8:18693)↵ at i (https://localhost:3830/assets/signalr/jquery-1.12.0.min.js:2:27451)↵ at Object.fireWith [as rejectWith] (https://localhost:3830/assets/signalr/jquery-1.12.0.min.js:2:28215)↵ at y (https://localhost:3830/assets/signalr/jquery-1.12.0.min.js:4:22617)↵ at XMLHttpRequest.c (https://localhost:3830/assets/signalr/jquery-1.12.0.min.js:4:26750

In the below code, I break on the line

const sessionHub = connection[HUB_NAME];

Which then lets me view the connection for that particular hub, which is connected and seems fine. But on the following invoke line it fails with the above error.

My client middleware code

import hostlookup from "./hostlookup";
import { LOGTAIL_INCOMING_METHOD_UPDATED, HUB_NAME, SIGNALR_CONNECT, SIGNALR_DISCONNECT, LOGTAIL_ACTION_START, LOGTAIL_ACTION_STOP, LOGTAIL_SERVER_METHOD_STOP, LOGTAIL_SERVER_METHOD_START, SIGNALR_STATE_DISCONNECTED, SIGNALR_STATE_CONNECTING, SIGNALR_STATE_RECONNECTING } from "./constants";
import { getSignalRStatusChangedAction, getSignalRStopAction } from "./actions";
import { getLogTailUpdatedAction } from "../../modules/logtail/actions";

const url = hostlookup().host + '/signalr';
const connection = $.hubConnection(url, {
    useDefaultPath: false
});

export default function createSignalrMiddleware() {
    return (store) => {
        const dispatch = store.dispatch.bind(store);

        const SIGNALR_STATES = {
            0: 'Connecting',
            1: 'Connected',
            2: 'Reconnecting',
            4: 'Disconnected'
        };

        let keepAlive = true;
        let wasConnected = false;
        let currentState = null;

        initialiseListeners(dispatch);

        function onStateChanged(state) {
            if (currentState === state) { return; }

            if (currentState === 2 && state === 1) {
                connection.start().done(() => {
                    // reconnect actions if required
                });
            }

            currentState = state;
            dispatch(getSignalRStatusChangedAction(state));
        }

        connection.stateChanged((state) => {
            const newStateName = SIGNALR_STATES[state.newState];

            if (newStateName === 'Connected') {
                wasConnected = true;
            }

            onStateChanged(state.newState);
        });

        connection.disconnected(() => {
            if (keepAlive) {
                if (wasConnected) {
                    onStateChanged(SIGNALR_STATE_RECONNECTING);
                } else {
                    onStateChanged(SIGNALR_STATE_CONNECTING);
                }

                connection.start().done(() => {
                    // Connect actions if required
                });
            }
        });

        function initialiseListeners(dispatcher) {
            const sessionHub = connection[HUB_NAME] = connection.createHubProxy(HUB_NAME);

            sessionHub.on(LOGTAIL_INCOMING_METHOD_UPDATED,
                (logDataLine) => {
                    dispatch(getLogTailUpdatedAction(logDataLine));
                })

            sessionHub.on('Disconnect', () => dispatcher(getSignalRStopAction()));
        }

        return (next) => (action) => {
            const { type } = action;
            const sessionHub = connection[HUB_NAME];

            switch (type) {
                case SIGNALR_CONNECT:
                    keepAlive = true;
                    onStateChanged(SIGNALR_STATE_CONNECTING);
                    connection.start().done(() => {
                        const sessionHub = connection[HUB_NAME];
                        sessionHub.invoke(LOGTAIL_SERVER_METHOD_START, 'test')
                        // Connect actions if required
                    });
                    return;

                case SIGNALR_DISCONNECT:
                    keepAlive = false;
                    wasConnected = false;
                    onStateChanged(SIGNALR_STATE_DISCONNECTED);
                    connection.stop();
                    return;

                case LOGTAIL_ACTION_START:
                    sessionHub.invoke(LOGTAIL_SERVER_METHOD_START, action.applicationName);
                    return;

                case LOGTAIL_ACTION_STOP:
                    sessionHub.invoke(LOGTAIL_SERVER_METHOD_STOP);
                    return;

                default:
                    return next(action);
            }
        }
    }
}

My server hub code

    public interface ILogTailClient
    {
        void LogDataUpdated(string[] logData);        
    }

    [Authorize]
    public class LogTailHub : Hub<ILogTailClient>
    {
        private readonly IClientConnectionHandler _clientConnectionHandler;
        private readonly IAuthorisedUserCache _userCache;
        private readonly ILogTailService _logTailService;

        public LogTailHub(IClientConnectionHandler clientConnectionHandler, IAuthorisedUserCache userCache, ILogTailService logTailService)
        {
            _clientConnectionHandler = clientConnectionHandler;
            _userCache = userCache;
            _logTailService = logTailService;
        }

        public override Task OnConnected()
        {
            var clientConnection = GetClientConnection();
            if (clientConnection != null)
                _clientConnectionHandler.OnConnected(clientConnection);

            return base.OnConnected();
        }

        public override Task OnReconnected()
        {
            lock (_clientConnectionHandler)
            {
                _clientConnectionHandler.OnDisconnected(Context.ConnectionId);

                var clientConnection = GetClientConnection();
                if (clientConnection != null)
                    _clientConnectionHandler.OnConnected(clientConnection);
            }

            return base.OnReconnected();
        }

        public override async Task OnDisconnected(bool stopCalled)
        {
            _clientConnectionHandler.OnDisconnected(Context.ConnectionId);            
            StopTailing();
            await base.OnDisconnected(stopCalled);
        }

        public void StartTailing(string applicationName)
        {
            _logTailService.StartListening(Context.ConnectionId, applicationName);
        }

        public void StopTailing()
        {
            _logTailService.StopListening(Context.ConnectionId);
        }

        private IClientConnection GetClientConnection()
        {
            if (Context.User != null && _userCache.TryGetUser(Context.User.Identity.Name, out var authorisedUser))
            {
                return new ClientConnection(Context.ConnectionId, authorisedUser);
            }

            return null;
        }
    }

The DI registration for the hub

public class WebNotificationsModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterAssemblyTypes(GetType().Assembly).AsImplementedInterfaces()
                .Except<LogTailHub>()          // Defined below
                .Except<HubContextProvider>();  // Defined as Singleton below

            var hubConfiguration = new HubConfiguration
            {
                EnableJavaScriptProxies = false,
                EnableDetailedErrors = true
            };
            builder.Register(ctx => hubConfiguration);

            builder.Register(ctx => {
                var connectionManager = hubConfiguration.Resolver.Resolve<IConnectionManager>();
                return connectionManager.GetHubContext<LogTailHub, ILogTailClient>();
            }).ExternallyOwned();

            builder.RegisterType<HubContextProvider>().As<IHubContextProvider>().SingleInstance();

            builder.RegisterType<LogTailHub>().ExternallyOwned(); // SignalR hub registration

            var serializer = new JsonSerializer();
            serializer.Converters.Add(new StringEnumConverter());
            builder.RegisterInstance(serializer).As<JsonSerializer>();
        }
    }
1

1 Answers

0
votes

The server side code SignalR is calling is throwing an exception, you need the details of this exception to point you to where your code is falling over. The details should be in your server event log. Alternatively you can (temporarily) turn on detailed errors which will push the exception detail into the JavaScript console.

In Startup.cs:

hubConfiguration.EnableDetailedErrors = true;

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.enabledetailederrors?view=aspnetcore-2.2

If you go this route be sure to turn off detailed errors before deploying.