how to run StartAsync connection of signalr blazor client in docker image?

Viewed 1318

I created default blazor server side app. Then added Microsoft.AspNetCore.SignalR.Client and ChatHub class. Then edited startup.cs file (add services.AddSignalR() and endpoints.MapHub<ChatHub>("/chatHub")) and index.razor page. Then run by IIS express. it is okey.

Then added docker support and run Docker host. it is not working. Because only don't work hub connection StartAsync method. How to run it? Help me? Thank you very much guys.

Error is:

An unhandled exception occurred while processing the request. SocketException: Cannot assign requested address System.Net.Http.ConnectHelper.ConnectAsync(string host, int port, CancellationToken cancellationToken)

HttpRequestException: Cannot assign requested address System.Net.Http.ConnectHelper.ConnectAsync(string host, int port, CancellationToken cancellationToken)

index.razor code:

@code {
    private HubConnection _hubConnection;

    protected override async Task OnInitializedAsync()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chatHub"))
            .Build();

        _hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            StateHasChanged();
        });

        await _hubConnection.StartAsync(); // **DON'T WORK IN DOCKER HOST.**
    }
}

Docker file:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["BlazorApp1/BlazorApp1.csproj", "BlazorApp1/"]
RUN dotnet restore "BlazorApp1/BlazorApp1.csproj"
COPY . .
WORKDIR "/src/BlazorApp1"
RUN dotnet build "BlazorApp1.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "BlazorApp1.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "BlazorApp1.dll"]
2 Answers

My best guess is your hub client is trying to connect to "the-public-url-out-of-docker/chatHub":

    _hubConnection = new HubConnectionBuilder()
        .WithUrl(NavigationManager.ToAbsoluteUri("/chatHub"))
        .Build();

NavigationManager.ToAbsoluteUri(...) will convert /chatHub to a public url which are exposed to the end user. For example, if you're using reverse proxy, it might be the domain name.

Note there're url at three different levels:

  • the domain name that are exposed to public
  • the host ip & port
  • the container ip & port
  +----------------------------------+
  | HOST  (5000)                     |
  |   +                              |
  |   |Port Mapping---------------+  |
  |   >-->-->|Container (80)      |  |
  |          |                    |  |
  |          +--------------------+  |
  +-----^----------------------------+
        |  reverse proxy
+-------+----------------------------+
| nginx                              |
|  https://www.myexample.com/chatHub
|                                    |
+-------^----------------------------+
        |
        |
        |
        |
+-------+-----------+
|                   |
|   Browser         | (Brazor sees only the public url via NavgiationManager  )
|                   |
+-------------------+

However, when running in docker, the host's network is not accessible from the container's network all the time.

If that's case, there're several approaches that should work:

  1. Avoid using the public url like .WithUrl(NavigationManager.ToAbsoluteUri("/chatHub")). Hard-code it to the container ip&port. For example, if you container listens on 80, it should be http://localhost/chatHub.
  2. Configure a network for docker, or add a --network for when running docker. For more details, see this thread

I had the same issue wherein running through IIS the application worked as expected, but when running through docker SignalR failed to connect to the hub. I fixed this issue by defining my own navigation manager that inherits from NavigationManager and overrides the EnsureInitialized() method to set the base URI.

public class CustomNavigationManager : NavigationManager
{
    protected override void EnsureInitialized()
    {
        var baseUri = Environment.GetEnvironmentVariable("blazor-app-url");
        Initialize(baseUri, baseUri);
    }
}

It also requires adding a container name, and an environment variable to my docker-compose.override.yml file to be used in the CustomNavigationManager.

version: '3.4'

networks:
  mynetwork:
    external: true      

services:
  my-blazor-app:
    networks: 
      - mynetwork
    container_name: blazor-app    
    environment:
      blazor_app_url: http://blazor-app:8080/
    ports:
      - "49101:8080"

CustomNavigationManager can then be registered as a Singleton in startup, injected into relevant components (replacing NavigationManager), and continue to be used as NavigationManager was before, e.g.:

public class WeatherForecastService : IWeatherForecastService
{
    private readonly HttpClient _httpClient;
    private readonly CustomNavigationManager _navigationManager;

    public WeatherForecastService(HttpClient httpClient, CustomNavigationManager navigationManager)
    {
        _httpClient = httpClient;
        _navigationManager = navigationManager;
    }

    public async Task<IEnumerable<WeatherForecast>?> GetWeatherForecast(DateTime startDate)
    {
        var uri = _navigationManager.ToAbsoluteUri($"/weatherforecast/get?startDate={startDate:s}");
        return await _httpClient.GetFromJsonAsync<WeatherForecast[]>(uri);
    }
}

For the application to continue to work when running through IIS express, the environment variable will also need to be defined in launchSettings.json.

"IIS Express": {
  "commandName": "IISExpress",
  "launchBrowser": true,
  "environmentVariables": {
    "blazor_app_url": "http://localhost:5282/"
  }
}
Related