I'm using SignalR Core, and for architecture reasons the SignalR server stuff (Hubs and the like) are in a different domain from my main project.
Client has this code:
function connectToHub(url) {
var connection = new signalR.HubConnectionBuilder()
.withUrl(url, { headers: { "cookie": document.cookie } }).build();
connection.on("NotificationUpdated", function (count) {
console.log(count);
});
connection.start().then(function () {
console.log('started');
}).catch(function (err) {
return console.error(err.toString());
});
}
It connects to server fine, but OnConnectedAsync in my hub gets no cookies or any other credentials (Context.User.Identity.Claims is empty)
Here's OnConnectedAsync code
public async override Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, ((ClaimsIdentity)Context.User.Identity)
.Claims.FirstOrDefault(c => c.Type.Contains("email"))?.Value);
await base.OnConnectedAsync();
}
If I put hub in the same domain as client, it works fine, but if I place it in different domain, client stops attaching cookies.
How to get the client to attach cookies?
(Alternatively, if there are different ways of mapping connectionId with User Claims, I would be fine with that too)
Related
So, I am trying to do a SIMPLE task with SignalR right now. Basically, I have a console app that will have .NET SignalR client, and a .NET 6 Web API with SignalR server.
I am just trying to do a simple flow:
Client sends request to SignalR Hub
Hub processes request, and returns data back to .NET client
I have no user interaction at all, that's why I'm not using JS, as this will not be in the browser.
Here is my current setup for the SignalR Server
OnPremAgentClientHub in Web API:
using Microsoft.AspNetCore.SignalR;
namespace HttpLongPollingServer.Hubs
{
public sealed class OnPremClientHub : Hub
{
public async Task GetOnPremAgentStatus(string clientIp)
{
bool isReachable = OnPremAgentData.IsAgentPingable(clientIp)
await Clients.Caller.SendAsync("OnPremAgentStatusReceived", isReachable);
}
}
}
The relevant code from Program.cs in Web API for SignalR setup:
app.MapHub<OnPremClientHub>("/clienthub", options =>
{
options.Transports = HttpTransportType.LongPolling;
});
Yes, I want to force HTTP Long polling.
Here is my setup in the console app for SignalR client
Program.cs
var hubConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7184/clienthub")
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.AddConsole();
// This will set ALL logging to Debug level
logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();
await hubConnection.StartAsync();
await LongPollingTest.TestHttpSignalRLongPolling(hubConnection);
The TestHttpSignalRLongPolling that calls the SignalR Hub methods:
public static async Task TestHttpSignalRLongPolling(HubConnection hubConnection)
{
await hubConnection.InvokeAsync("GetOnPremAgentStatus", arg1: "192.168.19.128");
hubConnection.On("OnPremAgentStatusReceived", (bool isReachable) => {
if (isReachable)
Console.WriteLine("Agent is reachable");
else
Console.WriteLine("Agent is not reachable");
});
}
Now, I can get the client to invoke the GetOnPremAgentStatus. However, when the client goes to do the hubConnection.On()... I get the following Warning and Error
Microsoft.AspNetCore.SignalR.Client.HubConnection[14]
Failed to find handler for 'OnPremAgentStatusReceived' method.
fail: Microsoft.AspNetCore.SignalR.Client.HubConnection[57]
Failed to bind arguments received in invocation '(null)' of 'OnPremAgentStatusReceived'.
System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 0.
at Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol.BindTypes(Utf8JsonReader& reader, IReadOnlyList`1 paramTypes)
I'm banging my head against a wall. I feel it's something simple I'm missing, I just don't know what it is.
Any tips or help is appreciated!
Alright, so the answer was staring me straight in the face in the Microsoft docs. The problem was that I was trying to call a callback on an unregistred handler that is located in my Server Hub in my client code. The fix was easy. I simply moved my connection.On to Program.cs after making my Hub Connection, but before starting the connection.
Updated Program.cs looks like this:
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using HttpLongPollingClientConsole;
ServiceProvider service = new ServiceCollection()
.AddLogging((loggingBuilder) => loggingBuilder
.SetMinimumLevel(LogLevel.Debug)
.AddConsole()
.AddDebug())
.BuildServiceProvider();
var hubConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7184/clienthub", options =>
{
options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
})
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.AddConsole();
// This will set ALL logging to Debug level
logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();
// Register the handler here!!
hubConnection.On<bool>("OnPremAgentStatusReceived", (isReachable) => {
if (isReachable)
Console.WriteLine("Agent is reachable");
else
Console.WriteLine("Agent is not reachable");
});
hubConnection.StartAsync().Wait();
await LongPollingTest.TestHttpSignalRLongPolling(hubConnection);
Console.ReadKey();
This is explained in the Microsoft Docs very well.
Now, from my Debug output of the SignalR client I can see the Handler being registered properly.
dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[40]
Registering handler for client method 'OnPremAgentStatusReceived'.
I am struggling with configuring and using signalR with .net core mvc 6. The purpose for the signalR hub is to send messages to js clients after invoking method in C# controller (js client is React application configured in MVC as ClientApp).
I enabled debugging for both client signalR instance and asp.net
here are the logs from ASP.NET:
SPA proxy is ready. Redirecting to https://localhost:44440.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionManager[1]
New connection ctA6QHwS4fvVGcufYvtlAA created.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[10]
Sending negotiation response.
dbug: Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionDispatcher[4]
Establishing new connection.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionHandler[5]
OnConnectedAsync started.
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubProtocolResolver[2]
Found protocol implementation for requested protocol: json.
dbug: Microsoft.AspNetCore.SignalR.HubConnectionContext[1]
Completed connection handshake. Using HubProtocol 'json'.
connected!! ctA6QHwS4fvVGcufYvtlAA
and corresponding to them logs with js client:
Utils.ts:194 [2022-02-03T18:40:17.568Z] Debug: Starting HubConnection.
Utils.ts:194 [2022-02-03T18:40:17.568Z] Debug: Starting connection with transfer format 'Text'.
Utils.ts:194 [2022-02-03T18:40:17.576Z] Debug: Sending negotiation request: https://localhost:44440/hubs/order/negotiate?negotiateVersion=1.
Utils.ts:194 [2022-02-03T18:40:21.741Z] Debug: Skipping transport 'WebSockets' because it was disabled by the client.
Utils.ts:194 [2022-02-03T18:40:21.742Z] Debug: Selecting transport 'ServerSentEvents'.
Utils.ts:194 [2022-02-03T18:40:21.742Z] Trace: (SSE transport) Connecting.
Utils.ts:190 [2022-02-03T18:40:25.857Z] Information: SSE connected to https://localhost:44440/hubs/order?id=fxqgKpJnF5Dq5MX-RCfXcg
Utils.ts:194 [2022-02-03T18:40:25.857Z] Debug: The HttpConnection connected successfully.
Utils.ts:194 [2022-02-03T18:40:25.857Z] Debug: Sending handshake request.
Utils.ts:194 [2022-02-03T18:40:25.858Z] Trace: (SSE transport) sending data. String data of length 32.
Utils.ts:194 [2022-02-03T18:40:29.969Z] Trace: (SSE transport) request complete. Response status: 200.
Utils.ts:190 [2022-02-03T18:40:29.978Z] Information: Using HubProtocol 'json'.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HttpConnection.stopConnection(undefined) called while in state Disconnecting.
index.js:1 [2022-02-03T18:40:59.997Z] Error: Connection disconnected with error 'Error: Server timeout elapsed without receiving a message from the server.'.
console.<computed> # index.js:1
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HubConnection.connectionClosed(Error: Server timeout elapsed without receiving a message from the server.) called while in state Connecting.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: Hub handshake failed with error 'Error: Server timeout elapsed without receiving a message from the server.' during start(). Stopping HubConnection.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: Call to HttpConnection.stop(Error: Server timeout elapsed without receiving a message from the server.) ignored because the connection is already in the disconnected state.
Utils.ts:194 [2022-02-03T18:40:59.997Z] Debug: HubConnection failed to start successfully because of error 'Error: Server timeout elapsed without receiving a message from the server.'.
here is sample code from js client application:
console.log("hub attached");
const hubConnection = new HubConnectionBuilder()
.withUrl(OrderHubUrl, {
transport: HttpTransportType.ServerSentEvents,
accessTokenFactory: () => user.accessToken ?? "",
})
.withAutomaticReconnect()
.configureLogging(LogLevel.Trace)
.build();
this.dispatcher.state.saveState("hubConnection", hubConnection);
const startConnection = async () => {
try {
await hubConnection.start();
console.log("connected");
} catch (e) {
this.dispatcher.dispatch(Event.ShowModal, {
actionName: "OK",
header: "Error",
content: e,
});
}
};
hubConnection.on("ReceiveMessage", (user, message) => {
console.log("message received");
console.log(user);
console.log(message);
});
hubConnection.onreconnecting((e) => console.log("reconnecting", e));
hubConnection.onreconnected((e) => console.log("reconnected", e));
startConnection();
}
as you can see from logs on the top, signalR js client is not able to pass through start method. Instead after some time it throws an error message.
below is my Program.cs file, where i configured signalR (i am suspecting that maybe something wrong is here)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using OrderMaker.Authentication.Helpers;
using OrderMaker.Authentication.Services;
using OrderMaker.Entities;
using OrderMaker.Modules.Common.Services;
using OrderMaker.Modules.Order.Hubs;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSignalR(c =>
{
c.EnableDetailedErrors = true;
c.ClientTimeoutInterval = TimeSpan.MaxValue;
c.KeepAliveInterval = TimeSpan.MaxValue;
});
ConfigureConfiguration(builder.Configuration);
ConfigureServices(builder.Services, builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCors(
x => x
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");
app.MapHub<OrderHub>("/hubs/order");
app.Run();
void ConfigureConfiguration(ConfigurationManager configuration)
{
using var client = new OrderMakerContext();
client.Database.EnsureCreated();
}
void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddControllers();
// configure strongly typed settings objects
var appSettingsSection = configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["order_maker_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}
);
services.AddScoped<IUserService, UserService>();
services.AddSingleton<ILogService, LogService>();
}
definition of my hub:
public class OrderHub : Hub
{
public async Task SendNotification(List<OrderModel> message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
Console.WriteLine("connected!! " + Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
Console.WriteLine("disconnected!!");
}
}
And sample controller from where i want to send message to clients:
private readonly IHubContext<OrderHub> _orderHubContext;
public OrdersController(IHubContext<OrderHub> orderHubContext)
{
_orderHubContext = orderHubContext;
}
[Route("api/[controller]/")]
[HttpPost]
//[Authorize(Roles = $"{Role.Admin},{Role.Manager},{Role.Employee}")]
public async Task<IActionResult> Post([FromBody] List<OrderModel> model)
{
/// some code for creating entities etc
db.SaveChanges();
/// notification to all clients that something new was added
Console.Write(_orderHubContext.Clients.All.ToString());
await _orderHubContext.Clients.All.SendAsync("ReceiveMessage", "hi there");
}
return Ok(new MessageResponse("Order added succesfully."));
}
basically i am lost, i spend two days already figuring out what may cause the issues, but i just can't make this thing working. I would really appreciate any sugestions or help. I tried to disable firewall, use different browser, etc without any success. Connection to hub is being made, c# app see that new connection but js client simply stuck in start() method for a while and throws an error message 'Error: Server timeout elapsed without receiving a message from the server.'.
Update: when i explicitly set type of transport in js clint to LongPolling hub is working as intended, but this is not ideal solution.
--- Update ---
All of those issues are happening only on local machine. I tried to check my luck and deploy to production app with transport fixed to SSE and it works without any issuess, as well as WebSocket transport. The only clue i have is that on localhost app is using kestrel and on server when i am hosting my app is using IIS.
You could try to set the serverTimeout and KeepAliveInterval in your program.cs:
builder.Services.AddSignalR(c =>
{
c.EnableDetailedErrors = true;
c.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
c.KeepAliveInterval = TimeSpan.FromSeconds(15);
});
Reason:
If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing KeepAliveInterval, change the ServerTimeout or serverTimeoutInMilliseconds setting on the client. The recommended ServerTimeout or serverTimeoutInMilliseconds value is double the KeepAliveInterval value.
So,you'd better not set the value of KeepAliveInterval as max value.
This is related to the SPA Proxy that .NET is setting up.
The message I get is as follows:
Utils.ts:193
[2022-09-29T17:09:19.866Z] Error: Failed to complete negotiation with the server: Error: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /hub/negotiate</pre>
</body>
</html>
: Status code '404' Either this is not a SignalR endpoint or there is a proxy blocking the connection.
What happens, likely, is that the proxy routes traffic from your spa to the port specified here in your csproj file
eg.
https://localhost:44423
You should not initiate a websocket to this port this is -not- proxied.
In my case, the dotnet served port is in reality https://localhost:7088 (properties/launchsettings.json)
So modify your Cors settings... like below, if the spaproxy e.g. is at 44423 and the .net service port at 7088.
services.AddCors(options =>
{
options.AddPolicy("ClientPermission", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins("https://localhost:44423", "https://localhost:7088")
.AllowCredentials();
});
});
Next is your js code looks like this e.g.:
const newConnection = new HubConnectionBuilder()
.withUrl('https://localhost:7088/hub/myhub')
.withAutomaticReconnect()
.build();
For me this solved it.
I have implemented an yarp.reverse proxy server with the code below:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpProxy();
services.AddCors(options =>
{
options.AddPolicy("customPolicy", builder =>
{
builder.AllowAnyOrigin();
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpProxy httpProxy)
{
// Configure our own HttpMessageInvoker for outbound calls for proxy operations
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler()
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseCookies = false
});
// Setup our own request transform class
var transformer = new CustomTransformer(); // or HttpTransformer.Default;
var requestOptions = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(100) };
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.Map("/{**catch-all}", async httpContext =>
{
httpContext.Request.Headers["Connection"] = "upgrade";
await httpProxy.ProxyAsync(httpContext, "http://192.168.178.80:5000", httpClient, requestOptions, transformer);
var errorFeature = httpContext.Features.Get<IProxyErrorFeature>();
save_log(httpContext.Request.Path, "/", "http://192.168.178.80:5000" + httpContext.Request.Path, "3");
// Check if the proxy operation was successful
if (errorFeature != null)
{
var error = errorFeature.Error;
var exception = errorFeature.Exception;
}
});
});
}
And in another app a SignalR server following this example: https://learn.microsoft.com/en-GB/aspnet/core/tutorials/signalr?view=aspnetcore-5.0&tabs=visual-studio
The proxy server works and forwards the request to the signalR server. But the signalR Client is not able to connect to the signalR Server. I always get a Connection disconnected with error
Error: Server timeout elapsed without receiving a message from the server.
in the Java Script console.
But the SSE is connected as you can see in the following browser status report:
signalr.js:2156 [2021-03-25T13:19:29.970Z] Information: SSE connected to https://localhost:44318/chatHub?id=IqKD6P0NsUY9Is6OSrMusQ
The problem seems to be the Proxy Server because if I call the site directly it works. Has somebody any idea what's wrong with my Proxy and how I can solve it?
In my solution, I have 2 projects : SignalRServer, WebClient which are both MVC application.
In my server, I've used SignalR 2 Hub and in client project I've used jquery SignalR library to connect to hub server by passing hosted server url as hub connection url.
Everything is working as well and client can receive messages from server.
The problem : In client project I have added authentication and users must enter their username and password to view the page which messages are going to be viewed.
Now I want to get the signed in username which has connected to SignalR Hub Server and send related messages to each user using this username.
How do I know which user has connected to server while the authentication is processing in client project separated from server project ?
JQuery client hub:
var PbxHub = $.connection.pbxhub;
$.connection.hub.url = "http://localhost:10437/signalr";
$.connection.hub.logging = true;
$.connection.hub.start({
jsonp: true,
withCredentials: false
}).done(function () {
console.log('Hub is connected.');
});
$.connection.hub.disconnected(function () {
console.log('Hub is disconnected.');
setTimeout(function () {
$.connection.hub.start();
}, 1500); // Restart connection after 1500 miliseconds.
});
Client Startup:
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Home/Login"),
});
}
Server Startup:
public void Configuration(IAppBuilder app)
{
app.Map("/signalr", map =>
{
var hubConfiguration = new HubConfiguration
{
EnableJavaScriptProxies = true,
EnableJSONP = true,
EnableDetailedErrors = true
};
map.UseCors(CorsOptions.AllowAll);
map.RunSignalR(hubConfiguration);
});
}
Appreciated for any idea.
As described by Microsoft documentation at here https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1#cookie-authentication
In a browser-based app, cookie authentication allows your existing
user credentials to automatically flow to SignalR connections. When
using the browser client, no additional configuration is needed. If
the user is logged in to your app, the SignalR connection
automatically inherits this authentication.
So, by using same DBContext which used in client app , I could access to connectionId of users which connected to hub and either insert records to database after sending them messages.
if (_context.Extension.Any(ex=>ex.Code.ToString() == deviceName))
{
extensionID = _context.Extension.SingleOrDefault(ex => ex.Code.ToString() == deviceName).ID.ToString();
}
else { return; }
asterHub.Clients.User(extensionID).SendState(
e.Device_state.Name,
string.Format("{0}:{1}:{2}",
pc.GetHour(DateTime.Now),
pc.GetMinute(DateTime.Now),
pc.GetSecond(DateTime.Now)),
state,
stateCode);
I'm trying to have a WebSocket server up and running on ASP.NET Core. I created an empty web project dotnet new web changed the Program.cs to:
public static void Main(string[] args) {
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => {
webBuilder.UseStartup<Startup>();
})
.Build()
.Run();
}
And Startup.cs's ConfigureServices method to:
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
services.AddWebSockets();
}
And Configure method to:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapConnectionHandler<WebSocketHandler>("/ws");
});
}
And my WebSocketHandler's OnConnectedAsync method I've the following:
public override async Task OnConnectedAsync(ConnectionContext connection)
{
var context = connection.GetHttpContext();
var endpoint = $"{connection.RemoteEndPoint}";
if (!context.WebSockets.IsWebSocketRequest) {
connection.Abort();
_logger.LogCritical($"Request from {endpoint} endpoint aborted.");
return;
}
var websocket = await context.WebSockets.AcceptWebSocketAsync();
_logger.LogInformation($"WebSocket request from {endpoint} endpoint accepted!");
}
The problem arises when I try to connect to APP_URL/ws and each time the server closes the connection as soon as it receives the request. Here are the logs: https://pastebin.com/raw/34yu7thw
If I place a Task.Delay(-1) at the end of OnConnectedAsync method, it keeps the connection open but drops incoming connections.
I have searched MSDocs and haven't been able to find much documentation on how to use MapConnectionHandler<T>.
Would it be safe for me to have a while loop which receives messages from multiple clients in OnConnectedAsync?
Is this not the right way to handle websocket connections?
Is MapConnectionHandler<T> transient?
I'm really confused and can't figure out it's behavior.
I've implemented a WebSocket server in ASP.NET core using these docs and it worked out quite well for me: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-3.1
The main idea is that after you accept a request with AcceptWebSocketAsync() you take the returned WebSocket object and use it to send and receive. Typically you would create a loop, calling ReceiveAsync until some condition is met (you determine the session is done when you receive a certain message, or the client disconnects, etc). As the docs state, When using a WebSocket, you must keep the middleware pipeline running for the duration of the connection. So if you're passing that WebSocket connection off to a background worker to perform send/receive on, you need to keep that pipeline open for the duration of your interactions with that client. Then when you're done you signal to the middleware that the connection is finished and it can unwind.
I suspect not keeping the connection open and looping is your issue. I haven't used MapConnectionHandler for WebSockets before, so this might not work, but it's possible the above strategy will be helpful to you, or follow the docs with a background worker like so:
app.Use(async (context, next) => {
var socket = await context.WebSockets.AcceptWebSocketAsync();
var socketFinishedTcs = new TaskCompletionSource<object>();
BackgroundSocketProcessor.AddSocket(socket, socketFinishedTcs);
await socketFinishedTcs.Task;
});
So, I accomplished my goal. Complete use-case is here: https://github.com/Yucked/Rhapsody/blob/beta/src/Controllers/WebSocketHandler.cs
This approach uses System.IO.Pipelines. I don't have the old source code as I scrapped it and can't figure out why connections were being dropped before even after keeping the pipeline open but hey it works now!
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapConnectionHandler<WebSocketHandler>("/player/{playerId}");
});
public override async Task OnConnectedAsync(ConnectionContext connection) {
var httpContext = connection.GetHttpContext();
if (!httpContext.WebSockets.IsWebSocketRequest) {
await httpContext.Response.CompleteAsync();
return;
}
await httpContext.WebSockets.AcceptWebSocketAsync()
.ContinueWith(async task => {
var webSocket = await task;
await HandleConnectionAsync(webSocket);
});
}
private async Task HandleConnectionAsync(WebSocket websocket) {
try {
do {
var memory = writer.GetMemory(BUFFER_SIZE);
var receiveResult = await webSocket.ReceiveAsync(memory, CancellationToken.None);
if (!receiveResult.EndOfMessage) {
writer.Advance(receiveResult.Count);
continue;
}
await writer.FlushAsync();
} while (webSocket.State == WebSocketState.Open);
}
catch (Exception exception) {
await writer.CompleteAsync(exception);
}
}