typescriptvue.jsasp.net-coreasp.net-core-signalr

I'm trying to connect to SignalR on my backend, but it does not work


I'm using Vue + ts + @microsoft/signalr in my project. I want to develop a chat with SignalR. I'm also using C# + ASP.NET Core on the backend + docker.

So, I have written the following code on my backend:

using DomovoiBackend.API.Auth.ServiceDecorators;
using DomovoiBackend.API.Controllers;
using DomovoiBackend.API.JsonInheritance;
using DomovoiBackend.Application;
using DomovoiBackend.Application.Services.CounterAgentServices.Interfaces;
using DomovoiBackend.Persistence;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSignalR();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1",
        new OpenApiInfo()
        {
            Title = "DomovoiBackend.API",
            Version = "v1"
        });
    options.UseAllOfToExtendReferenceSchemas();
    options.UseAllOfForInheritance();
    options.UseOneOfForPolymorphism();
});

var inheritanceConfigurations =
    new AssemblyInheritanceConfiguration()
        .CreateAllConfigurations();

builder.Services.AddControllers().AddNewtonsoftJson(
    options =>
    {
        foreach(var inheritanceConfiguration in inheritanceConfigurations)
            options.SerializerSettings.Converters.Add(inheritanceConfiguration);
    });

builder.Services.AddApplicationLayer()
    .AddMappers()
    .AddPersistence(builder.Configuration)
    .CreateDatabase()
    .FillDatabase();

builder.Services.Decorate<ICounterAgentService, AuthCounterAgentService>();

builder.Services.AddHttpContextAccessor();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddAuthorization();
builder.Services.AddSession();

builder.Services.AddCors();

builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
        options.SlidingExpiration = true;
        options.LoginPath = "/CounterAgent/Login";
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Strict;
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AuthenticatedCounterAgent", policy => policy.RequireClaim("CounterAgentId"));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.UseSession();

app.UseCors(policyBuilder =>
{
    policyBuilder.WithOrigins("http://localhost:5173")
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials()
        .SetIsOriginAllowedToAllowWildcardSubdomains()
        .SetIsOriginAllowed((host) => true);
});

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapHub<ChatHub>("/chat");

app.Run();

public partial class Program;

ChatHub class:

 [Authorize]
 public class ChatHub : Hub
 {
     [Authorize]
     public async Task Send(string message, string idReceiver)
     {
         var nameSender = Context.User?.Identity?.Name;
         if (Clients.User(idReceiver) == null)
             throw new UnknownUser($"User with id \"{idReceiver}\" does not exist in the system");
         await Clients.User(idReceiver).SendAsync("Receive", message, nameSender);
         await Clients.Caller.SendAsync("NotifySendMethod", "Ok");
     }
 }

and frontend:

import  { HttpTransportType, HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { inject, singleton } from "tsyringe";
import { Chat } from "../../application/useCases/chat";
import { Message } from "../../domain/chat/message";
import { MessageStatus } from "../../domain/enums/chatEnum";
import { CounterAgent } from "../../domain/counteragents/counteragent";
import { CounteragentViewModel } from "../../viewModel/CounteragentViewModel";
import { ICouterAgentMapper } from "../../mappers/interfaces/couteragentMapperInterface";

@singleton()
export class ChatService {

    private readonly _connection: HubConnection;
    private _listener!: Chat;
    
    public constructor(@inject("chatURL") private readonly _baseURL: string,
        @inject("ICouterAgentMapper") private readonly _userMapper: ICouterAgentMapper) {
        this._connection = new HubConnectionBuilder()
            .withUrl(this._baseURL, {
                headers: { 
                    'Access-Control-Allow-Origin': '*',
                 },
                withCredentials: true,
                transport: HttpTransportType.WebSockets,
            })
            .build();
        this._connection.on("Receive", (text: string, idSender: string) => this.receiveMessage(text, idSender));
    }

    public start(idCompanion :string, user: CounteragentViewModel): void {
        let counteragent = this._userMapper.mapViewModelToCouterAget(user);
        this._listener = new Chat(this, true, idCompanion, counteragent);
        this._connection.start();
    }

    public close(): void {
        this._connection.stop()
    }

    public get state(): string {
        return this._connection.state.toString();
    }

    public get messages(): Message[] {
        return this._listener.messages;
    }

    public async sendMessage(text: string): Promise<void> {
        let message = new Message("", text, this._listener.idCompanion, this._listener.user.id!);
        try {
            let response = await this._connection.invoke("Send", message.text, message.recieverId);
            message.status = MessageStatus.Send;            
        }
        catch(e){
            message.status = MessageStatus.NotSend;
            throw(e);
        }
        finally {
            this._listener.addMessage(message);
        }
    }

    public receiveMessage(text: string, idSender: string): void {
        let message = new Message("", text, idSender, this._listener.user.id!);
        message.status = MessageStatus.Recieve;
        this._listener.addMessage(message);
    } 
}

I use ChatService in ChatView.vue:

//...
 export default defineComponent({
        components: { Header},
        data() {
            return {
                store: store,
                chatService: {} as ChatService, 
                //...
            };
        },
        computed: {
        },
        mounted() {
        //...
            this.load_data();
        },
        methods: {
            load_data(){
                let chatService = container.resolve(ChatService);
                this.chatService = chatService;
                chatService.start(this.user.id, this.store.state.user!);
                this.messages = this.chatService.messages;
            },
        //...
        },
    })
    </script>

After load_data() method and connection.start() i get this message:

Access to fetch at 'http://localhost:8181/chat/negotiate?negotiateVersion=1' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

@microsoft_signalr.js?v=12bba02c:2110 Uncaught (in promise) Error: Failed to complete negotiation with the server: TypeError: Failed to fetch at HttpConnection._getNegotiationResponse (@microsoft_signalr.js?v=12bba02c:2110:29)

ChatService.ts:35 [2024-06-03T18:17:15.726Z] Error: Failed to start the connection: Error: Failed to complete negotiation with the server: TypeError: Failed to fetch

ChatService.ts:35 [2024-06-03T18:17:15.725Z] Error: Failed

to complete negotiation with the server: TypeError: Failed to fetch

I have used different cors policy settings on backend and different headers in connectionBuilder.WithUrl(...), but it does not help me.


Solution

  • You are using SetIsOriginAllowed and WithOrigins together. It will face cors issue, for more details, you can check this thread.

    Change your CORS setting like below.

    app.UseCors(policyBuilder =>
    {
        policyBuilder.WithOrigins("http://localhost:5173")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
            //.SetIsOriginAllowedToAllowWildcardSubdomains()
            //.SetIsOriginAllowed((host) => true);
    });
    

    And I also find some middleware order issue in your code.

    var app = builder.Build();
        
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    //app.UseAuthentication();
    //app.UseAuthorization();
    //app.UseSession();
    
    app.UseCors(policyBuilder =>
    {
        policyBuilder.WithOrigins("http://localhost:5173")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
            //.SetIsOriginAllowedToAllowWildcardSubdomains()
            //.SetIsOriginAllowed((host) => true);
    });
    
    app.UseAuthentication();
    app.UseAuthorization();
    // move it to here
    app.UseSession();
    
    app.MapControllers();
    
    
    app.MapHub<ChatHub>("/chat");
    
    app.Run();