DOJay
DOJay

Reputation: 101

Connect to SignalR Hubs using Ocelot API Gateway

I am trying to connect Blazor client to SignalR hub in a simple microservice through the Ocelot API gateway. I am using SSL for all the ASP.NET Core projects.

The gateway works fine when calling https endpoints and I get "Connection Id Required" when I call the signalR hub endpoint directly from the gateway browser (which shows Ocelot routes correctly).

Unfortunately, I get the following error when I try to connect to the hub from the blazor client application

fail: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] requestId: 0HM4U0GLR9ACR:00000001, previousRequestId: no previous request id, message: Exception caught in global error handler, exception message: Only Uris starting with 'ws://' or 'wss://' are supported. (Parameter 'uri'), exception stack: at System.Net.WebSockets.ClientWebSocket.ConnectAsync(Uri uri, CancellationToken cancellationToken) at Ocelot.WebSockets.Middleware.WebSocketsProxyMiddleware.Proxy(HttpContext context, String serverEndpoint) at Ocelot.WebSockets.Middleware.WebSocketsProxyMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext) at Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext) at Ocelot.LoadBalancer.Middleware.LoadBalancingMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext) at Ocelot.Request.Middleware.DownstreamRequestInitialiserMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext) at Ocelot.Multiplexer.MultiplexingMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext) at Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.MiddlewareAnalysis.AnalysisMiddleware.Invoke(HttpContext httpContext)

The following are my code.

Ocelot API Startup file

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {            
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy", policy =>
            {
                policy.SetIsOriginAllowed(x => true).AllowAnyMethod().AllowAnyHeader();
                //policy.SetIsOriginAllowed(x => true).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
            });
        });
        services.AddSignalR();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        app.UseRouting();
        app.UseCors("CorsPolicy");
        app.UseWebSockets();
        app.UseOcelot().Wait();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}

ocelot.json Configuration

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/currency/{everything}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7004
        }
      ],
      "UpstreamPathTemplate": "/api-currency/{everything}",
      "UpstreamHttpMethod": [ "Get" ]
    },
    {
      "DownstreamPathTemplate": "/api/currency/{everything}",
      "ReRouteIsCaseSensitive": false,
      "DownstreamScheme": "wss",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7004
        }
      ],
      "UpstreamPathTemplate": "/api-currencyhub/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:7000",
    "RequestIdKey": "OcRequestId"

  }
}

Microservice API Startup file

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy", policy =>
            {
                policy.SetIsOriginAllowed(x => true).AllowAnyMethod().AllowAnyHeader();
                //policy.SetIsOriginAllowed(x => true).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
                //policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials();
            });
        });
        services.AddSignalR();
        services.AddControllers();
    }

    // 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();
        }

        app.UseRouting();
        app.UseCors("CorsPolicy");
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<CurrencyHub>("/api/currency/maincurrencyhub");
            endpoints.MapControllers();
        });
    }
}

Blazor client Razor Page

@page "/"
@using Microsoft.AspNetCore.SignalR.Client
<h1>Hello, world!</h1>

<h1>Welcome to SignalR with Blazor</h1>
<button class="btn btn-success" @onclick="async () => await ConnectToServer()" disabled="@isConnected">Connect</button>
<button class="btn btn-success" @onclick="async () => await OnGateway()">Gateway</button>
<h3>Connection Status: @connectionStatus</h3>
<div class="row">
    <div class="col-4">
        @foreach (var item in notifications)
        {
            <div class="row">
                <h4>@item</h4>
            </div>
        }
    </div>
</div>

@code {

    //string gatewayUrl = "wss://localhost:7000/api-currency/maincurrencyhub";
    string gatewayUrl = "https://localhost:7000/api-currency/maincurrencyhub";
    HubConnection gatewayConnection = null;

    bool isConnected = false;
    string connectionStatus = "Closed";

    List<string> notifications = new List<string>();

    private async Task ConnectToServer()
    {
        gatewayConnection = new HubConnectionBuilder()
            //.WithUrl(gatewayUrl)

            .WithUrl(gatewayUrl, opt => { opt.SkipNegotiation = true; opt.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; })
            .Build();

        try
        {
            await gatewayConnection.StartAsync();

            connectionStatus = "Connected :-)";

            gatewayConnection.Closed += async (s) =>
            {
                isConnected = false;
                connectionStatus = "Disconnected";
                await gatewayConnection.StartAsync();
                isConnected = true;
            };
            gatewayConnection.On<string>("ReceiveMessage", m =>
            {
                notifications.Add(m);
                StateHasChanged();
            });
        }
        catch (Exception ex)
        {

        }
    }

    async Task OnGateway()
    {
        await gatewayConnection.InvokeAsync("Send", "Na Gode");
    }

}

I have tried to follow Ocelot not passing websockets to microservice without avail. Can some one guide me in the right direction?

Upvotes: 5

Views: 9169

Answers (3)

Linus
Linus

Reputation: 107

This is the Ocelot configuration that finally allowed me to setup client => Ocelot => signalR server communications.

    {
      "DownstreamPathTemplate": "/hub/",
      "DownstreamScheme": "ws",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/hub/",
      "QoSOptions": {
        "TimeoutValue": 320000
      },
      "Priority": 50
    },
    {
      "DownstreamPathTemplate": "/hub/{path}",
      "DownstreamScheme": "ws",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 80
        }
      ],
      "UpstreamPathTemplate": "/hub/{path}",
      "QoSOptions": {
        "TimeoutValue": 320000
      },
      "Priority": 50
    },

The signalR server is configured to listen to /hub.

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<MyHub>("hub");
    });

I hope it helps someone.

Upvotes: 6

Zaha
Zaha

Reputation: 946

I solve the problem between Ocelot and SignalR Hub with NGinx Finally,

My Develepment configuration of Nginx is

server {
  listen 2000; #Entry point and redirect to ocelot to 2005 to start site

  #Ocelot configuration in root
  location / {
    proxy_pass http://localhost.com:2005; 
  }

  #SignalR hub (devicehub)
  location /devicehub {
    proxy_pass http://localhost:2004;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

NOTA: Sorry, I've lost the references for hub configuration (/devicehub) but I can update later.

More detail solution (You can skip if you know about docker/ocelot)

This is the docker-compose file to start nginx

version: '3.4'

services:
# ... 
  nginx:
    image: nginx:latest
    volumes:
        #For windows development doesn't work without c:, but in linux you can change to / with other yaml file
      - c:/data/nginx/default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 2000:2000

To start nginx

docker-compose -f "docker-compose.yml" up -d

or

docker-compose up -d

Best regards

Upvotes: 0

Roman
Roman

Reputation: 1711

In ocelot.json, try to remove the second route and leave just the first one but using "wss" as DownstreamScheme, like this:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/currency/{everything}",
      "DownstreamScheme": "wss",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7004
        }
      ],
      "UpstreamPathTemplate": "/api/currency/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:7000",
    "RequestIdKey": "OcRequestId"

  }
}

Upvotes: 0

Related Questions