Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross origin SignalR connection stops after negotiate

I have an MVC 5 app serving up views, and a Web API 2 app as the service layer (.NET 4.5). The Web API app uses SignalR 2.1.2 to return progress as it's processing POSTs to the service API. The two are deployed to different domains, so I've set up cross origin support as per the asp.net tutorial article.

[assembly: OwinStartup(typeof (Startup))]
namespace MyApp.Service
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Map("/signalr", map =>
            {
                //worry about locking it down to specific origin later
                map.UseCors(CorsOptions.AllowAll);
                map.RunSignalR(new HubConfiguration());
            });
            //now start the WebAPI app
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }
    }
}

WebApiConfig.cs also contains its own CORS declaration.

namespace MyApp.Service
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            //controller invocations will come from the MVC project which is deployed to a
            //different domain, so must enable cross origin resource sharing
            config.EnableCors();
            // Web API routes
            config.MapHttpAttributeRoutes();

            //Snip other controller dependency initialisation
        }
    }
}

I've defined a simple hub class with no server-side API (it's only to allow the server to push to the clients, not for the clients to call into).

namespace MyApp.Service.Hubs
{
    [HubName("testresult")]
    public class TestResultHub : Hub
    {
    }
}

Since I'm going cross-domain AND the hub is not exposing any server side API, I'm not bothering to use a generated JS proxy.

The relevant bits of the JS that set up the signalr hub connection is: (remember this is being served up from the MVC app, which does not have any signalr support (except jquery-signalr-{version}.js of course))

function TestScenarioHandler(signalrHubUrl) {
    var self = this;
//Snip irrelevant bits (mostly Knockout initialisation)

    self.signalrConnectionId = ko.observable();

    var hubConnection = $.hubConnection(signalrHubUrl, { useDefaultPath: false });

    var hubProxy = hubConnection.createHubProxy("testresult");
    hubProxy.on("progress", function(value) {
        console.log("Hooray! Got a new value from the server: " + value);
    });

    hubConnection.start()
        .done(function() {
            self.signalrConnectionId(hubConnection.id);
            console.log("Connected to signalr hub with connection id " + hubConnection.id);
        })
        .fail(function() {
            console.log("Failed to connect to signalr hub at " + hubConnection.url);
        });
}

Going cross-origin like this, Firefox network traffic shows (and I've confirmed Chrome shows the same thing) a GET to

http://****service.azurewebsites.net/signalr/negotiate?clientProtocol=1.5&connectionData=[{"name":"testresult"}]&_=1424419288550

Notice that the name matches the value of the HubName attribute on my hub class.

This GET returns HTTP 200, the response gives me a JSON payload containing a ConnectionId, ConnectionToken, and a bunch of other fields that suggests everything's ok. The HTTP response also has the Access-Control-Allow-Origin: header set to the domain that the GET originated from. All up it looks good, except that's where the traffic stops.

But the JS console prints "Failed to connect to signalr hub at http://****service.azurewebsites.net/signalr"

To verify I'm not doing anything too stupid, I've added signalr support and a basic hub to the MVC app (so no cross origin required), and changed the $.hubConnection() and hubConnection.createProxy() calls accordingly. When I do that, browser traffic shows the same /signalr/negotiate?... GET (obviously not cross origin any more), but then also GETs to /signalr/connect?... and /signalr/start?.... The JS console also prints a success message.

So in summary;

  • CORS is enabled on the service layer, and the signalr /negotiate GET returns 200, what appears to be a valid connection id, and the expected Access-Control-Allow-Origin: header. This suggests to me that the server-side CORS support is behaving itself correctly, but the signalr connection does not succeed.
  • When I reconfigure so the signalr connection is NOT cross origin, everything works as expected.

WTF am I missing or doing wrong?! Some conflict between HttpConfiguration.EnableCors() and IAppBuilder.UseCors(CorsOption) perhaps?

like image 862
tones Avatar asked Feb 20 '15 08:02

tones


People also ask

What causes SignalR to disconnect?

If a server does not become available within the disconnect timeout period, the SignalR connection ends. In this scenario, the Closed event ( disconnected in JavaScript clients) is raised on the client but OnDisconnected is never called on the server.

How many connections SignalR can handle?

In the default mode, the app server creates five server connections with Azure SignalR Service. The app server uses the Azure SignalR Service SDK by default. In the following performance test results, server connections are increased to 15 (or more for broadcasting and sending a message to a big group).

Is SignalR bidirectional?

ASP.NET SignalR is a new library for ASP.NET developers that makes developing real-time web functionality easy. SignalR allows bi-directional communication between server and client.

Is SignalR asynchronous?

SignalR is an asynchronous signaling library for ASP.NET that our team is working on to help build real-time multi-user web application.


2 Answers

Solved it. I had changed the map.UseCors(CorsOptions.AllowAll) to pass in a CorsPolicy object instead, and set SupportsCredentials to false, having read elsewhere that Access-Control-Allow-Origin: * is incompatible with access-control-allow-credentials: true.

private static readonly Lazy<CorsOptions> SignalrCorsOptions = new Lazy<CorsOptions>(() =>
{
    return new CorsOptions
    {
        PolicyProvider = new CorsPolicyProvider
        {
            PolicyResolver = context =>
            {
                var policy = new CorsPolicy();
                policy.AllowAnyOrigin = true;
                policy.AllowAnyMethod = true;
                policy.AllowAnyHeader = true;
                policy.SupportsCredentials = false;
                return Task.FromResult(policy);
            }
        }
    };
});

public void Configuration(IAppBuilder app)
{
    app.Map("/signalr", map =>
    {
        map.UseCors(SignalrCorsOptions.Value);
        map.RunSignalR(new HubConfiguration());
     });
     //now start the WebAPI app
     GlobalConfiguration.Configure(WebApiConfig.Register);
}

Setting SupportCredentials to true results in the Access-Control-Allow-Origin header being rewritten with the actual origin (not *) and access-control-allow-credentials: true in the response.

And now it works.

like image 89
tones Avatar answered Sep 28 '22 09:09

tones


For me following settings did good job

        services.AddCors(c =>
        {
            c.AddPolicy("AllowCCORSOrigin", options => options
                .WithOrigins("http://localhost:3000")
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials()
                ); 
        });
like image 35
Jacek Plesnar Avatar answered Sep 28 '22 08:09

Jacek Plesnar