ASP.NET Core 109 SignalR 9: LongPolling fallback fails with 400 Bad Request on Handshake and breakpoints in Hub are never hi

felipe borges 0 Puntos de reputación
2026-07-31T15:07:28.61+00:00

Estoy intentando conectar un cliente frontend (Vue.js) a un Hub de SignalR 9 en ASP.NET Core 10. El cliente intenta conectar mediante WebSockets, falla con un código 302, y al caer en el transporte de respaldo (LongPolling), el servidor responde con un 400 bad request durante el intercambio de protocolos (Handshake).

Lo extraño es que los puntos de interrupción dentro de la clase del Hub nunca se detienen, lo que indica que la petición es rechazada por algún componente del pipeline antes de llegar al Hub. Las cookies de sesión sí se están enviando correctamente en los headers de la petición.

Entorno

Backend: ASP.NET Core 9.0 (SignalR v9.0.6)

Frontend: Vue.js con @microsoft/signalr

mi programs esta estructurado asi

using GraphQL.Client;

using Microsoft.AspNetCore.Authentication.Cookies;

using Microsoft.AspNetCore.Builder;

using Microsoft.AspNetCore.Hosting;

using Microsoft.AspNetCore.Http;

using Microsoft.AspNetCore.Http.Connections;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Localization;

using Microsoft.AspNetCore.Localization.Routing;

using Microsoft.EntityFrameworkCore;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

using Microsoft.Extensions.Hosting;

using Microsoft.Extensions.Logging;

using Microsoft.Extensions.Options;

using System;

using System.Globalization;

using System.Reflection;

using System.Threading.Tasks;

using TuFactoring.CustomProviders;

using TuFactoring.Data;

using TuFactoring.Resources;

using TuFactoring.Services;

using TuFactoring.Utilities;

using TuFactoring.WebSocket;

using TuFactoringGraphql;

using TuFactoringModels;

InitialConfigurations.SetPort();

var builder = WebApplication.CreateBuilder(args);

var configuration = builder.Configuration;

builder.WebHost.UseUrls("http://0.0.0.0:" + InitialConfigurations.GetPort());

#region CORS & Politica de Cookies

builder.Services.AddCors(options =>

{

options.AddPolicy("CorsDevPolicy", policy =>

{

    policy.SetIsOriginAllowed(origin => true)

          .AllowAnyHeader()

          .AllowAnyMethod()

          .AllowCredentials(); 

});
```});

builder.Services.Configure<CookiePolicyOptions>(options =>

{

```sql
options.CheckConsentNeeded = context => false;

options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
```});

#endregion

#region Identity & Autenticación

builder.Services.AddIdentity<User, Role>(config =>

{

```ruby
config.Password.RequiredLength = 8;

config.Password.RequireDigit = true;

config.Password.RequireUppercase = true;

config.Password.RequireLowercase = true;

config.Password.RequireNonAlphanumeric = true;
```})

.AddDefaultTokenProviders()

.AddClaimsPrincipalFactory<CustomClaimsStore>();

builder.Services.AddTransient<IUserStore<User>, CustomUserStore>();

builder.Services.AddTransient<IRoleStore<Role>, CustomRoleStore>();

builder.Services.ConfigureApplicationCookie(ops =>

{

```powershell
InitialConfigurations.SetTimeSesion(480);

InitialConfigurations.SetFinancing();

InitialConfigurations.SetWallet();

ops.Cookie.Path = "/";

ops.Cookie.IsEssential = true;

ops.Cookie.HttpOnly = false;

ops.Cookie.SameSite = SameSiteMode.Lax;

ops.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;

ops.ExpireTimeSpan = TimeSpan.FromMinutes(InitialConfigurations.GetTimeSesion());

ops.LoginPath = $"/{CultureInfo.CurrentCulture.Name}/Identity/Account/Login";

ops.AccessDeniedPath = $"/{CultureInfo.CurrentCulture.Name}/Identity/Account/Logout";

ops.SlidingExpiration = true;

// Evitar redirecciones 302 en llamadas de SignalR

ops.Events.OnRedirectToLogin = context =>

{

    var path = context.Request.Path.Value ?? string.Empty;

    var pathBase = context.Request.PathBase.Value ?? string.Empty;

    if (path.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||

        pathBase.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||

        path.Contains("/wsUser", StringComparison.OrdinalIgnoreCase) ||

        pathBase.Contains("/wsUser", StringComparison.OrdinalIgnoreCase))

    {

        context.Response.StatusCode = StatusCodes.Status401Unauthorized;

        return Task.CompletedTask;

    }

    context.Response.Redirect(context.RedirectUri);

    return Task.CompletedTask;

};

ops.Events.OnRedirectToAccessDenied = context =>

{

    var path = context.Request.Path.Value ?? string.Empty;

    var pathBase = context.Request.PathBase.Value ?? string.Empty;

    if (path.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||

        pathBase.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) ||

        path.Contains("/wsUser", StringComparison.OrdinalIgnoreCase) ||

        pathBase.Contains("/wsUser", StringComparison.OrdinalIgnoreCase))

    {

        context.Response.StatusCode = StatusCodes.Status403Forbidden;

        return Task.CompletedTask;

    }

    context.Response.Redirect(context.RedirectUri);

    return Task.CompletedTask;

};
```});

#endregion

#region Localización, Memoria y Sesión

builder.Services.AddSingleton<CultureLocalizer>();

builder.Services.Configure<RequestLocalizationOptions>(options =>

{

```javascript
var supportedCultures = new[]

{

    new CultureInfo("es-VE"),

    new CultureInfo("es-AR"),

    new CultureInfo("es-DO"),

    new CultureInfo("en")

};

options.DefaultRequestCulture = new RequestCulture("es-VE");

options.SupportedCultures = supportedCultures;

options.SupportedUICultures = supportedCultures;

options.FallBackToParentCultures = true;

options.FallBackToParentUICultures = true;

options.RequestCultureProviders.Clear();

options.RequestCultureProviders.Add(new CustomRequestCultureProvider(context =>

{

    var path = context.Request.Path.Value?.ToLower() ?? string.Empty;

    if (path.Contains("/js/") || path.Contains("/css/") || path.Contains("/lib/") || path.Contains("/images/") || path.Contains("/wsSubastas") || path.Contains("/wsUser"))

    {

        return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult("es-VE"));

    }

    return Task.FromResult<ProviderCultureResult?>(null);

}));

options.RequestCultureProviders.Add(new RouteDataRequestCultureProvider());

options.RequestCultureProviders.Add(new CookieRequestCultureProvider());

options.RequestCultureProviders.Add(new AcceptLanguageHeaderRequestCultureProvider());
```});

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

builder.Services.AddDistributedMemoryCache();

builder.Services.AddMemoryCache();

builder.Services.AddSession(options =>

{

```sql
options.IdleTimeout = TimeSpan.FromHours(8);

options.Cookie.HttpOnly = true;

options.Cookie.IsEssential = true;
```});

#endregion

#region Políticas de Autorización

builder.Services.AddAuthorization(config =>

{

```javascript
#region Profile

config.AddPolicy("PolicyProfile", policy => policy.RequireRole("OFFICER", "SEGMENTATION", "EXECUTIVE", "TREASURER", "GUEST", "PAYER", "OPERATIONSTREASURER", "ACCOUNTANT", "ADMINISTRATOR", "LEGAL", "MANAGER"));

config.AddPolicy("PolicyProfileChange", policy => policy.RequireRole("ADMINISTRATOR", "LEGAL", "MANAGER"));

config.AddPolicy("PolicyContracts", policy => policy.RequireRole("LEGAL"));

config.AddPolicy("PolicySecurity", policy => policy.RequireRole("ADMINISTRATOR", "LEGAL", "MANAGER"));

#endregion

#region Debtor

config.AddPolicy("RequiredDebtor", policy => policy.RequireRole("DEBTOR"));

config.AddPolicy("PolicyLoadInvoiceDebtor", policy => policy.RequireRole("MANAGER", "PAYER"));

config.AddPolicy("PolicyEditInvoiceDebtor", policy => policy.RequireRole("MANAGER", "PAYER"));

config.AddPolicy("PolicyPostedDebtor", policy => policy.RequireRole("MANAGER", "TREASURER"));

config.AddPolicy("PolicyFinancingDebtor", policy => policy.RequireRole("MANAGER", "TREASURER"));

config.AddPolicy("PolicyConfirmingDebtor", policy => policy.RequireRole("MANAGER", "TREASURER"));

#endregion

#region Supplier

config.AddPolicy("RequiredSupplier", policy => policy.RequireRole("SUPPLIER"));

config.AddPolicy("PolicyPagePublishedSupplier", policy => policy.RequireRole("MANAGER", "TREASURER", "PAYER"));

config.AddPolicy("PolicyAcceptOfferBankSupplier", policy => policy.RequireRole("MANAGER", "TREASURER"));

config.AddPolicy("PolicyPublishedInvoiceSupplier", policy => policy.RequireRole("MANAGER", "TREASURER", "PAYER"));

config.AddPolicy("PolicyOverdueInvoiceSupplier", policy => policy.RequireRole("MANAGER", "TREASURER", "PAYER"));

config.AddPolicy("PolicyLoadInvoiceSupplier", policy => policy.RequireRole("MANAGER", "PAYER"));

config.AddPolicy("PolicyEditInvoiceSupplier", policy => policy.RequireRole("MANAGER", "PAYER"));

config.AddPolicy("PolicyPostedSupplier", policy => policy.RequireRole("MANAGER", "TREASURER"));

config.AddPolicy("PolicyAuctionSupplier", policy => policy.RequireRole("LEGAL", "MANAGER", "TREASURER", "PAYER", "GUEST"));

config.AddPolicy("PolicyCloseAuctionSupplier", policy => policy.RequireRole("MANAGER", "TREASURER"));

#endregion

#region Confirmant

config.AddPolicy("RequiredConfirmant", policy => policy.RequireRole("CONFIRMANT"));

config.AddPolicy("PolicyRegisterDebtorConfirmant", policy => policy.RequireRole("ADMINISTRATOR", "LEGAL", "MANAGER", "EXECUTIVE", "EXTERNAL_BACKOFFICE_CONFIRMANT"));

config.AddPolicy("PolicyVerifyConfirmant", policy => policy.RequireRole("MANAGER", "OFFICER"));

config.AddPolicy("PolicyReviewInvoice", policy => policy.RequireRole("MANAGER", "TREASURER"));

config.AddPolicy("PolicyAssignExecutiveConfirmant", policy => policy.RequireRole("MANAGER", "SEGMENTATION"));

config.AddPolicy("PolicyAssignRiskConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE", "EXTERNAL_BACKOFFICE_CONFIRMANT"));

config.AddPolicy("PolicyAssignFinancingConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE"));

config.AddPolicy("PolicyRateConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE"));

config.AddPolicy("PolicyConfirmInvoiceConfirmant", policy => policy.RequireRole("MANAGER", "EXECUTIVE"));

config.AddPolicy("PolicyBuyInvoiceConfirmant", policy => policy.RequireRole("MANAGER", "TREASURER"));

config.AddPolicy("PolicyPaidInvoiceConfirmant", policy => policy.RequireRole("MANAGER", "OPERATIONSTREASURER"));

config.AddPolicy("PolicyPaidConciliedConfirmant", policy => policy.RequireRole("MANAGER", "OPERATIONSTREASURER"));

config.AddPolicy("PolicyPaidInvoiceOverdueConfirmant", policy => policy.RequireRole("MANAGER", "OPERATIONSTREASURER"));

config.AddPolicy("PolicyExternalTreasurerConfirmant", policy => policy.RequireRole("EXTERNAL_TREASURER"));

config.AddPolicy("PolicyExternalBackofficeConfirmant", policy => policy.RequireRole("EXTERNAL_BACKOFFICE_CONFIRMANT"));

#endregion

#region Factor

config.AddPolicy("RequiredFactor", policy => policy.RequireRole("FACTOR"));

config.AddPolicy("PolicyAuctionFactor", policy => policy.RequireRole("TREASURER", "DPERSON"));

config.AddPolicy("PolicyPaidFactor", policy => policy.RequireRole("TREASURER", "DPERSON", "OPERATIONS"));

#endregion

#region Backoffice

config.AddPolicy("RequiredBackoffice", policy => policy.RequireRole("BACKOFFICE"));

config.AddPolicy("PolicyExternalBackoffice", policy => policy.RequireRole("EXTERNAL_BACKOFFICE"));

#endregion
```});

#endregion

#region MVC, Razor Pages & DB Context

builder.Services.AddControllersWithViews()

```javascript
.AddViewLocalization(o => o.ResourcesPath = "Resources")

.AddModelBindingMessagesLocalizer(builder.Services)

.AddDataAnnotationsLocalization(o =>

{

    var type = typeof(ViewResource);

    var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName!);

    o.DataAnnotationLocalizerProvider = (t, factory) => factory.Create("ViewResource", assemblyName.Name!);

});
```builder.Services.AddSignalR(options =>

{

```sql
options.EnableDetailedErrors = true;
```})

.AddJsonProtocol(options =>

{

```sql
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
```});

builder.Services.AddRazorPages(o =>

{

```java
o.Conventions.Add(new CultureTemplateRouteModelConvention());

o.Conventions.AuthorizeAreaFolder("Profile", "/");

o.Conventions.AuthorizeAreaPage("Profile", "/ActualizarEmpresa", "PolicyProfile");

o.Conventions.AuthorizeAreaPage("Profile", "/MantenimientoUsuarios", "PolicySecurity");

o.Conventions.AuthorizeAreaFolder("Debtor", "/", "RequiredDebtor");

o.Conventions.AuthorizeAreaPage("Debtor", "/CargaMasiva", "PolicyLoadInvoiceDebtor");

o.Conventions.AuthorizeAreaPage("Debtor", "/CargaManual", "PolicyEditInvoiceDebtor");

o.Conventions.AuthorizeAreaPage("Debtor", "/PostularFacturas", "PolicyPostedDebtor");

o.Conventions.AuthorizeAreaPage("Debtor", "/FinanciamientoFacturas", "PolicyFinancingDebtor");

o.Conventions.AuthorizeAreaPage("Debtor", "/ConfirmarFacturas", "PolicyConfirmingDebtor");

o.Conventions.AuthorizeAreaFolder("Confirmant", "/", "RequiredConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/Account/Register", "PolicyRegisterDebtorConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/VerificarDatos", "PolicyVerifyConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/Segmentacion", "PolicyAssignExecutiveConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/EjecutivoCuentas", "PolicyAssignRiskConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/ConfirmarFacturas", "PolicyConfirmInvoiceConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/CompraFactura", "PolicyBuyInvoiceConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/PagoFacturas", "PolicyPaidInvoiceConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/FacturasConciliadas", "PolicyPaidConciliedConfirmant");

o.Conventions.AuthorizeAreaPage("Connfirmant", "/FacturasVencidas", "PolicyPaidInvoiceOverdueConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/SupplierSelection", "PolicyExternalTreasurerConfirmant");

o.Conventions.AuthorizeAreaPage("Confirmant", "/PreassignableInvoices", "PolicyExternalTreasurerConfirmant");

o.Conventions.AuthorizeAreaFolder("Supplier", "/", "RequiredSupplier");

o.Conventions.AuthorizeAreaPage("Supplier", "/PublicarFacturas", "PolicyPagePublishedSupplier");

o.Conventions.AuthorizeAreaPage("Supplier", "/FacturasVencimiento", "PolicyOverdueInvoiceSupplier");

o.Conventions.AuthorizeAreaPage("Supplier", "/MercadoFacturas", "PolicyAuctionSupplier");

o.Conventions.AuthorizeAreaPage("Supplier", "/CierreMercado", "PolicyCloseAuctionSupplier");

o.Conventions.AuthorizeAreaFolder("Factor", "/", "RequiredFactor");

o.Conventions.AuthorizeAreaPage("Factor", "/MercadoFacturas", "PolicyAuctionFactor");

o.Conventions.AuthorizeAreaPage("Factor", "/PagoFacturas", "PolicyPaidFactor");

o.Conventions.AuthorizeAreaFolder("Backoffice", "/", "RequiredBackoffice");

o.Conventions.AuthorizeAreaPage("Backoffice", "/ExternalBackoffice", "PolicyExternalBackoffice");
```}).AddNewtonsoftJson();

builder.Services.AddDbContext<ApplicationDbContext>(options =>

```yaml
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection")));
```#endregion

#region Servicios de Negocio

builder.Services.AddTransient<IEmailSender, AuthMessageSender>();

builder.Services.AddTransient<ISmsSender, AuthMessageSender>();

builder.Services.AddScoped<IAuthService, AuthService>();

builder.Services.AddScoped<IAuctionService, AuctionService>();

builder.Services.AddScoped<IGlobalService, GlobalService>();

builder.Services.AddScoped<IPeopleService, PeopleService>();

builder.Services.AddScoped<IInvoiceService, InvoiceService>();

builder.Services.AddScoped<IPaymentService, PaymentService>();

builder.Services.AddScoped<IWalletService, WalletService>();

builder.Services.AddSingleton(t => new GraphQLClient(configuration["GraphQlEndpoint"]));

builder.Services.AddScoped<GlobalConsumer>();

builder.Services.AddScoped<AuthConsumer>();

builder.Services.AddScoped<PeopleConsumer>();

builder.Services.AddScoped<InvoiceConsumer>();

builder.Services.AddScoped<AuctionConsumer>();

builder.Services.AddScoped<PaymentConsumer>();

builder.Services.AddScoped<WalletConsumer>();

#endregion

var app = builder.Build();

app.UseStaticFiles();

if (app.Environment.IsDevelopment())

{

```sql
app.UseDeveloperExceptionPage();
```}

else

{

```dockerfile
app.UseExceptionHandler("/Error");
```}

app.UseCors("CorsDevPolicy");

app.UseCookiePolicy();

app.UseSession();

app.UseRouting();

app.UseWhen(context =>

```scala
!context.Request.Path.Value.Contains("/wsSubastas", StringComparison.OrdinalIgnoreCase) &&

!context.Request.Path.Value.Contains("/wsUser", StringComparison.OrdinalIgnoreCase),

appBuilder =>

{

    appBuilder.UseRequestLocalization();

});
```app.UseAuthentication();

app.UseAuthorization();

app.UseWebSockets();

app.Use(async (context, next) =>

{

```vba
if (context.Request.Path.Value?.Contains("wsSubastas") == true && context.Request.Method == "POST")

{

    Console.WriteLine("--> LLEGÓ UN POST DE SIGNALR A WSENSUBASTAS");

}

await next();
```});

app.UseEndpoints(endpoints =>

{

```typescript
endpoints.MapControllers();

endpoints.MapRazorPages();

app.MapHub<SocketSubastas>("/wsSubastas", options =>

{

    options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling;

});

endpoints.MapHub<SocketUser>("/wsUser").AllowAnonymous().DisableAntiforgery();

endpoints.MapControllerRoute(

    name: "default",

    pattern: "{controller=Home}/{action=Index}/{id?}");
```});

app.Run(); 

Mi Sokcet de Subasta 

using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Http;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.SignalR;

using Newtonsoft.Json;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

using TuFactoring.Services;

using TuFactoringModels;

namespace TuFactoring.WebSocket

{

    [AllowAnonymous]

    public class SocketSubastas : Hub

    {

        private readonly IAuctionService _aS;

        private readonly SignInManager<User> _signInManager;

        public SocketSubastas(IAuctionService aS, SignInManager<User> signInManager)

        {

            this._aS = aS;

            this._signInManager = signInManager;

        }

        public async Task Publicar(Offert oferta)

        {

            Publications resultado = new Publications() { Error = new Exception("Invalid offert") };

            resultado = await this._aS.OfferPublication(oferta, oferta.Token);

            if (resultado.Errors != null || resultado.Error != null)

            {

                await Clients.Caller.SendAsync("Publications", resultado);

                return; 

            }

            resultado.Bids = new List<Bids>() { new Bids() { Factor = new People() { Id = oferta.Factor_id } } };

            if (oferta.Factor_id == resultado.Bids[0].Factor_id)

            {

                resultado.isOffered = true;

            }

            await Clients.Group(resultado.Invoice.Country.Value + "").SendAsync("Publications", resultado);

        }

        public async Task Cierre(string identificador)

        {

            await Clients.Group(identificador).SendAsync("Cierre", "close");

        }

        public async Task NotifyFactor(List<Notification> notification, string identificador)

        {

            await Clients.Group(identificador).SendAsync("Notification", notification);

        }

        public override async Task OnConnectedAsync()

        {

            await base.OnConnectedAsync();

        }

        public async Task loginGroup(string group)

        {

            if (String.IsNullOrEmpty(group)) return;

            await Groups.AddToGroupAsync(Context.ConnectionId, group);

        }

    }

}

Mi resourceConection que es el gestiona la conection dinamicamente depnediendo del socket a llamar 

let connectionSignalRActual = null;

let functionPostConnectionActual = function () { };

let rutaActual = "";

async function startSignalrConnection(ruta, functionPostConnection) {

```javascript
console.log("SignalR Version:", signalR.VERSION);

const rutaAbsoluta = ruta.startsWith('/') ? ruta : `/${ruta}`;

console.log('Conectando a la ruta de SignalR: ' + rutaAbsoluta);

const connection = new signalR.HubConnectionBuilder()

    .withUrl(rutaAbsoluta, {

        transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling,

        withCredentials: true

    })

    .configureLogging(signalR.LogLevel.Debug)

    .withAutomaticReconnect()

    .build();

try {

    await connection.start();

    console.log("SignalR conectado con éxito a:", rutaAbsoluta);

    rutaActual = rutaAbsoluta;

    connectionSignalRActual = connection;

    if (typeof postConnectAux !== "undefined" && functionPostConnection !== postConnectAux) {

        functionPostConnectionActual = functionPostConnection;

    }

    if (typeof functionPostConnection === "function") {

        functionPostConnection(connection);

    }

    connection.onreconnected(() => {

        console.log("SignalR reconectado automáticamente.");

        if (typeof functionPostConnectionActual === "function") {

            functionPostConnectionActual(connection);

        }

    });

    if (typeof closeSignalR === "function") {

        connection.onclose(closeSignalR);

    }

} catch (err) {

    console.error("Error crítico en SignalR al conectar:", err);

}
```}
Tecnologías de desarrolladores | ASP.NET Core | Otros
0 comentarios No hay comentarios

1 respuesta

Ordenar por: Más antiguo
  1. Bruno Caetano de Brito 245 Puntos de reputación
    2026-09-06T17:37:42.14+00:00

    Por el código mostrado, empezaría revisando el orden del middleware y la configuración de los endpoints. El problema parece estar ocurriendo antes de que la solicitud llegue al SocketSubastas, por eso los breakpoints dentro del Hub no se están ejecutando.

    1. Revisar el orden del pipeline

    El primer punto que revisaría es que UseCors se está ejecutando antes de UseRouting:

    app.UseCors("CorsDevPolicy");
    app.UseCookiePolicy();
    app.UseSession();
    app.UseRouting();
    

    Yo lo organizaría de esta manera:

    app.UseStaticFiles();
    
    if (app.Environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }
    
    app.UseRouting();
    
    app.UseCors("CorsDevPolicy");
    
    app.UseCookiePolicy();
    app.UseSession();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseWebSockets();
    
    app.MapControllers();
    app.MapRazorPages();
    
    app.MapHub<SocketSubastas>("/wsSubastas", options =>
    {
        options.Transports =
            HttpTransportType.WebSockets |
            HttpTransportType.LongPolling;
    });
    
    app.MapHub<SocketUser>("/wsUser")
       .AllowAnonymous()
       .DisableAntiforgery();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    
    app.Run();
    

    También evitaría mezclar UseEndpoints con app.MapHub. Con el modelo actual de ASP.NET Core, MapHub puede configurarse directamente mediante el endpoint routing.

    2. Revisar el código de estado 302 del WebSocket

    Como el Hub está marcado con:

    [AllowAnonymous]
    public class SocketSubastas : Hub
    

    no debería ser necesario redirigir al usuario hacia una página de inicio de sesión para este endpoint.

    Por eso, revisaría en DevTools → Network la solicitud que devuelve 302 y, principalmente, el header:

    Location: ...
    

    Si el Location apunta a algo como:

    /Identity/Account/Login
    

    entonces probablemente hay algún middleware o política de autenticación interviniendo antes de que la solicitud llegue al Hub.

    Si apunta a otra URL, por ejemplo una URL HTTPS, otro dominio o alguna ruta diferente, habría que revisar si el redireccionamiento proviene del proxy, reverse proxy o de alguna regla de redirección de la aplicación.

    3. Probar /negotiate por separado

    También recomiendo revisar la secuencia de solicitudes de SignalR en la pestaña Network.

    Primero debería aparecer algo similar a:

    POST /wsSubastas/negotiate
    

    y después la conexión al transporte seleccionado:

    GET /wsSubastas?id=...
    

    Si /negotiate devuelve 200, pero la conexión WebSocket devuelve 302, entonces el problema está ocurriendo durante el establecimiento del transporte WebSocket.

    Si WebSocket falla y el cliente intenta utilizar LongPolling, revisaría la solicitud:

    POST /wsSubastas?id=...
    

    y, especialmente, el cuerpo de la respuesta 400.

    El contenido de esa respuesta puede indicar exactamente qué está rechazando el servidor.

    4. Revisar la configuración de cookies

    También revisaría esta configuración:

    ops.Cookie.SameSite = SameSiteMode.Lax;
    

    Si el frontend Vue y la API están utilizando orígenes diferentes, dependiendo de la configuración de los dominios, podría ser necesario utilizar:

    ops.Cookie.SameSite = SameSiteMode.None;
    ops.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    

    En ese escenario, el cliente también debe enviar las credenciales:

    withCredentials: true
    

    Como mencionaste que la cookie ya aparece en los headers, no la consideraría mi primera sospecha. Primero investigaría el 302 y posteriormente el 400.

    5. Los transportes configurados parecen coherentes

    En el cliente tienes:

    transport:
        signalR.HttpTransportType.WebSockets |
        signalR.HttpTransportType.LongPolling
    

    y en el servidor:

    options.Transports =
        HttpTransportType.WebSockets |
        HttpTransportType.LongPolling;
    

    Por lo tanto, la configuración de los transportes parece coherente entre cliente y servidor.

    6. El breakpoint del Hub es una pista importante

    El hecho de que el breakpoint dentro de:

    public override async Task OnConnectedAsync()
    {
        await base.OnConnectedAsync();
    }
    

    nunca se alcance es una pista importante.

    Eso indica que probablemente la solicitud está siendo rechazada o redirigida antes de llegar al Hub.

    Por eso, en este momento no empezaría investigando métodos como:

    Publicar()
    

    o:

    loginGroup()
    

    Primero intentaría identificar exactamente en qué punto de la solicitud aparece el 302 y dónde se genera el 400.

    7. Orden recomendado para diagnosticarlo

    Yo haría las pruebas en este orden:

    1. Cambiar el orden a UseRouting() → UseCors().
    2. Evitar mezclar UseEndpoints con app.MapHub.
    3. Probar POST /wsSubastas/negotiate.
    4. Revisar el header Location de la respuesta 302.
    5. Revisar el cuerpo de la respuesta 400 de LongPolling.
    6. Confirmar si el frontend y el backend están en el mismo origen o en orígenes diferentes.
    7. Después de eso, revisar cookies, autenticación y configuración del proxy.

    La pista principal es que el breakpoint dentro del Hub no se ejecuta. Esto sugiere que el problema no está inicialmente en la lógica del Hub, sino en alguna etapa anterior del pipeline HTTP o durante la negociación del transporte de SignalR.

    Por eso, antes de modificar la implementación de SocketSubastas, intentaría descubrir exactamente qué componente está generando el 302 y cuál está generando el 400.

    ¿Le ha resultado útil esta respuesta?

    0 comentarios No hay comentarios

Su respuesta

Las respuestas pueden ser marcadas como "Aceptadas" por el autor de la pregunta y "Recomendadas" por los moderadores, lo que ayuda a los usuarios a saber que la respuesta ha resuelto el problema del autor.