websocket state 3 while sending message after reconnect - node.js

I encountered odd problem when working with websocket. I am not sure exactly what is the reason for whats happening.
I have this simple websocket client that is implemented into nestjs service
import { Injectable } from '#nestjs/common';
import * as WebSocket from 'ws';
import { timer } from 'rxjs';
#Injectable()
export class ImportFeedsGateway {
private ws: WebSocket;
constructor() {
this.connect();
}
private connect() {
this.ws = new WebSocket(process.env.URL);
this.ws.on('open', () => {
this.ws.send(Math.random()); // this message is always send properly and this.ws state is 1 when logged after reconnect
});
this.ws.on('message', (message) => {
...
});
this.ws.on('error', (message) => {
this.ws.close();
});
this.ws.on('close', (message) => {
timer(1000).subscribe(() => {
this.connect();
});
});
}
sendMessage(message) {
this.ws.send(message); // this message is never send after reconnect and this.ws state is 3 when logged
}
}
So the problem is that after reconnect Math.random() message is always properly sent while attempt to use sendMessage method results in fail because this.ws state always is 3.
It seems like this.ws points to different places depending on if its accessed from outside of class (via sendMessage method) or from inside (via on('open', callback). Inside callback its always current this.ws while sendMessage accesses old this.ws in closed state. Its like this.ws reference is not wired properly.
Does anyone know why is that and how to fix it?
---- EDIT
after transforming sendMessage method to its arrow version, its working correctly, but its more of puzzle now why
sendMessage = (message) => {
this.ws.send(message); // with arrow version this.ws is always in state 1 and its working correctly
}

Related

Messages not reaching handler while using WebSocket and nestjs

I am trying to implement WebSocket into nestjs app and I have problems with messages not reaching my handler in nestjs.
I am able to connect both ends and send message from nestjs to client but not the other way.
this is my nestjs code: (please note that i am not using socket.io i am implementing ws as WebSocket
import {
OnGatewayInit,
WebSocketGateway,
WebSocketServer,
} from '#nestjs/websockets';
import { Logger } from '#nestjs/common';
#WebSocketGateway(5015)
export class ExportFeedsGateway implements OnGatewayInit {
#WebSocketServer() wss: any;
private logger: Logger = new Logger('ExportFeedsGateway');
afterInit(server: any): any {
this.logger.log('Export Feeds Initialized');
}
handleConnection(client) {
client.send('message from server'); // this message is properly send and read on the client side
}
handleMessage(message) {
console.log('message', message); // this is never logged in nest
}
}
and some client code:
const WebSocket = require( 'ws');
...
this.ws = new WebSocket('ws://localhost:5015');
this.ws.on('open', () => {
this.ws.send('message from client') // this message never reaches server
});
...
Question is what is the problem with nestjs messages handler that it doesnt pick up messages?
You're missing the #SubscribeMessage('message') that you'll need for Nest to register the handler to the websocket server. handleConnection is a bit of a special case method as defined inthe lifecycle methods. Also, when sending the message from the ws library, use the form: ws.send('{"event": "message", "data": whateverDataObjectYouWant }', callback). This way, Nest can parse the event and properly route it

Client never receives messages from signalr hub SendAsync call from controller .NET Core in production

This works locally without any issues however when deployed to azure app service my client never receives any message when calling REST endpoint in controller.
My App Service has Web sockets enabled on the server via the configuration settings in azure.
Referencing docs for how to use the hubcontext outside of the hub: https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-6.0
I have a successful opened a connection:
[2022-04-01T19:34:40.915Z] Information: WebSocket connected to wss://staging-env.com/hubs/notification?id=omz2rGgyuXw01QV6Vs6PbA.
I receive test broadcast we call from the client successfully:
Testing notification hub broadcasting 4/1/2022 7:34:40 PM
No error messages or information regarding a closed connection - everything pinging correctly in app logs.
I have a controller:
public class NotificationHubController : ControllerBase
{
private readonly IHubContext<NotificationHub> _hub;
private readonly ILogger<NotificationHubController> _logger;
public NotificationHubController(
ILogger<NotificationHubController> logger,
IHubContext<NotificationHub> hub
)
{
_logger = logger;
_hub = hub;
}
[HttpPost]
public async Task<IActionResult> SendNotification(NotificationDto dto)
{
_logger.LogInformation("Broadcasting notification update started");
await _hub.Clients.All.SendAsync("NotificationUpdate", dto); <-------- no errors reported
_logger.LogInformation("Broadcasting notification update resolved");
return Ok();
}
}
Hub:
using Microsoft.AspNetCore.SignalR;
namespace NMA.Api.Service.Hubs
{
public class NotificationHub: Hub
{
public Task NotifyConnection()
{
return Clients.All.SendAsync("TestBrodcasting", $"Testing notification hub
broadcasting {DateTime.Now.ToLocalTime()}");
}
}
}
Startup:
...
services.AddSignalR(hubOptions =>
{
hubOptions.EnableDetailedErrors = true;
});
...
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<NotificationHub>("/hubs/notification");
endpoints.MapControllers();
});
Client:
(react js hook)
export const useNotificationHub = ({ onNotificationRecievedCallback }: NotificationHubProps) => {
const [connection, setConnection] = useState<HubConnection | null>(null);
useEffect(() => {
const establishConnection = async () => {
const newConnection = new HubConnectionBuilder()
.withUrl(`${process.env.REACT_APP_NMA_HUB_URL}/notification`)
.withAutomaticReconnect()
.build();
await newConnection.start();
newConnection.invoke("NotifyConnection").catch(function (err) {
console.error(err.toString());
});
newConnection.on("TestBrodcasting", function (time) {
console.log(time);
});
newConnection.on('NotificationUpdate', (response: NotificationAPIResponse) => {
console.log("Notification Recieved: ", response);
onNotificationRecievedCallback(response);
});
newConnection.onclose(() => {
console.log("Notification connection hub closed");
})
setConnection(newConnection);
}
establishConnection();
}, [onNotificationRecievedCallback]);
return {
}
}
I make the request via Insomnia to post a notification via the endpoint. I get a successful HTTP status return. My logs tell me that the that the call was made and it was successful. Yet not data received in the client.
I'm not sure what to do from here. I need to be able to invoke client calls from REST endpoints but this doesn't seem to work and I've exhausted my ability to use google to find alternatives.
I believe you are experiencing the issue because you are starting the connection and then setting up the on and onclose callbacks last. You should rather set up all of your callbacks first, and then starting the connection should be the last thing you do.
EG:
// BUILD THE CONNECTION FIRST
const newConnection = new HubConnectionBuilder()
.withUrl(`${process.env.REACT_APP_NMA_HUB_URL}/notification`)
.withAutomaticReconnect()
.build();
// SET UP CALLBACKS SECOND
newConnection.on("TestBrodcasting", function (time) {
console.log(time);
});
newConnection.on('NotificationUpdate', (response: NotificationAPIResponse) => {
console.log("Notification Recieved: ", response);
onNotificationRecievedCallback(response);
});
newConnection.onclose(() => {
console.log("Notification connection hub closed");
})
// START CONNECTION LAST
await newConnection.start();
// NOW YOU CAN INVOKE YOUR METHOD
newConnection.invoke("NotifyConnection").catch(function (err) {
console.error(err.toString());
});

Socket.io server ping timeout on some connections

I'm having issues with using the socket.io JS client and the server where websocket connections ping timeout at random times. I've searched high and low through SO and the rest of the interwebs.
Some context on the project - this is an Electron/ReactJS application that has two separate windows, where the socket.io client connects on application load, provided the user is logged in. If a user logs in after application load, the same function to connect to the socket is called:
const protocol = Env.isDev() ? 'http' : 'https'
const socketServer = `${protocol}://${LocalConfig.servers.websocket}:80`
const socketReceiverId = createReceiverId()
/**
* Initiates the websocket connection
*/
export function init() {
// Get authourizer ID
const authorizerId = getAuthId()
// only allow in the renderer and we have an authorizer id
// and if the socket connection is null. Socket will not be
// null if this method is called after HMR
if (Env.isRenderer() && authorizerId && !getCurrentSocket()) {
const socket = sockio(`${socketServer}/notification`, {
timeout: 5000,
reconnection: false,
transportOptions: {
polling: {
extraHeaders: {
'origin': `gui://${process.env.PLATFORM}`,
'x-receiver-token': socketReceiverId,
'x-authorization': authorizerId
}
}
}
})
socket.on('s:connected', onConnected) // called when connection made (custom event with extra data
.on('n:push', onNotification) // called when a notification is received
.on('s:close', onServerClose) // called when server shuts down
.on('disconnect', onDisconnect) // called on disconnect
.on('error', console.warn)
}
}
I had to leave reconnection to false because I need to manage reconnections manually, because if a user logs out, we won't be reconnecting, but will reconnect after successful login
I'm also using polling so I can send extra headers, including origin, authorization and the receiver-token, which is a random UUID that's stored in LocalStorage to uniquely identify the application instance, and will be the room that we join, so notifications get sent to the proper device, whether it's this app or mobile (which will all use the same websocket interface)
When the application loads initially and the websocket connection is attempted, 90% of the time, both windows connect successfully and the push notifications come through - the other 10% of the time, only one window can connect, while the other won't (even though through logging on the client, I can see that the onConnected callback is called). Eventually, both will connect successfully, but ideally, they should connect instantly at the first attempt.
What really screws things up is when the server reboots (which will be something that will happen from time to time, as new code gets pushed to the server and the service is restarted). When the server disconnects however, a message is emitted to all clients currently connected where the custom reconnect circuit is invoked.
This is where the main issues start - whether it's manual reconnection, or automatic, the polling keeps failing with a ping timeout error, even though the onConnected callback is called.
Eventually, the connections all settle and connect - sometimes after a minute or two, sometimes as much as 10 minutes to a half hour. It's all exceedingly random.
Note that everything right now is on my local machine running Ubuntu 18.04 LTS, so there should be near zero latency.
I pasted the verbose debug output here:
https://pastebin.com/TR6qPe0R
Here is the main server code:
import process from 'process'
import http from 'http'
import sockio from 'socket.io'
import { BindAll } from 'lodash-decorators'
import { Handlers } from './Handlers'
import { Receiver } from './Receiver'
import { RedisPool } from './RedisPool'
import { Log } from './Logger'
interface NamespaceGroup {
ns: SocketIO.Namespace
receiver: Receiver
}
#BindAll()
class Server {
private _port: number
private _socketServer: sockio.Server
private _pollingServer: http.Server
private _namespaces: NamespaceGroup[] = []
constructor() {
this._port = parseInt(process.env['PORT']) || 18000
this._pollingServer = http.createServer(Handlers.handleHttpRequest)
}
startServer() {
this._pollingServer.listen(this._port, this.onServerStart)
process.on('SIGINT', this.close)
process.on('SIGUSR2', this.close)
}
private async onServerStart() {
Log.info(`Server started on port ${this._port}`)
this._socketServer = sockio(this._pollingServer)
this._socketServer.origins(Handlers.handleOrigin)
this._socketServer.use(Handlers.handleAuthorization)
const nsNotify = this._socketServer.of('/notification')
.on('connection', Handlers.handleNotificationConnection)
this._namespaces.push({
ns: nsNotify,
receiver: new Receiver(nsNotify)
})
}
private close() {
Log.info('Closing server. Killing process')
for (const nsGroup of this._namespaces) {
nsGroup.ns.emit('s:close')
}
this._socketServer.close()
RedisPool.closeAll()
process.exit(0)
}
}
const server = new Server()
export default server
And the handlers:
import { IncomingMessage, ServerResponse } from 'http'
import { Socket } from 'socket.io'
import { Queries } from './Queries'
import { Log } from './Logger'
import { OutgoingMessage } from './Receiver'
export namespace Handlers {
const allowedOrigins = [
'gui://win32',
'gui://darwin',
'gui://linux',
'ast://android',
'ast://ios',
'mob://android',
'mob://ios',
]
export function handleOrigin(origin: string, callback: (error: string | null, success: boolean) => void) {
if (allowedOrigins.indexOf(origin) == -1) {
callback('Invalid Origin: ' + origin, false)
} else {
callback(null, true)
}
}
export async function handleAuthorization(socket: Socket, next: (err?: any) => void) {
const auth = socket.handshake.headers['x-authorization']
const receiverToken = socket.handshake.headers['x-receiver-token']
if (!auth) {
next(new Error('Authorization not provided'))
socket.disconnect()
} else {
try {
const credentials = await Queries.findClientCredentials(auth)
if (!!credentials) {
await Queries.saveCurrentToken(auth, receiverToken)
next()
} else {
Log.warn('Client rejected [auth:%s] (Invalid Authorization)', auth)
next(new Error('Invalid Authorization: ' + auth))
socket.disconnect()
}
} catch (err) {
next(err)
Log.error('Client auth error', err)
socket.disconnect()
}
}
}
export async function handleNotificationConnection(socket: Socket) {
const auth = socket.handshake.headers['x-authorization']
const receiverToken = socket.handshake.headers['x-receiver-token']
const namespace = socket.nsp
const ns = namespace.name
// Emit connection message
socket.emit('s:connected', ns, socket.id) // lets the client know connection is OK
// Join room specific to an application instance
socket.join(receiverToken)
Log.debug('Connect: Client connected to %s [auth:%s] - %s (%d active sockets)',
ns, auth, socket.id, socket.adapter.rooms[receiverToken].length)
// Listens to the c:close (client request close) event
socket.on('c:close', async reason => {
socket.leave(receiverToken)
socket.disconnect(true)
Log.debug('Disconnect: (%s) %s : %s', reason, receiverToken, socket.id)
})
}
// Default HTTP handler for the main Node HTTP server
export function handleHttpRequest(request: IncomingMessage, response: ServerResponse) {
response.statusCode = 200
response.setHeader('Content-Type', 'text/plain')
response.end(`${request.method} ${request.url}\n`)
}
}
The rest of the code on the server side works as it should, such as RedisPool and the Receiver class instances. The issue is with connectivity, so I only included the Server class and Handlers namespace.
And on the client, the relevant code (same namespace as the init function above):
// Flag to tell us if the disconnection
// was instigated via the client after a
// log out, so we know if we need to
// reconnect
let isManualDisconnect = false
/**
* Manually disconnect from the websocket server
* normally used when user logs out
*/
export function disconnect() {
const socket = getCurrentSocket()
isManualDisconnect = true // lets us know not to attempt reconnects
socket.emit('c:close', 'manual client disconnect', socket.id)
}
/**
* Handle a successful connection to the websocket server
* #param ns the namespace connected to (should always be /notification)
* #param socketId the socket id (SID)
*/
function onConnected(ns: string, socketId: string) {
console.log('Connection to %s Accepted. Receiver ID: %s', ns, socketId)
isManualDisconnect = false
}
/**
* Handles the disconnection from the socket server
* #param reason the reason for the disconnect
*/
function onDisconnect(reason: DisconnectReason) {
console.log('Disconnected (%s) Manual:', reason, isManualDisconnect)
if (!isManualDisconnect) {
if (getAuthId())
getCurrentSocket().connect()
}
}
/**
* Handles a socket being disconnected when the
* socket server shuts down
*/
function onServerClose() {
console.log('Socket server interrupt')
window.setTimeout(reconnect, 5000)
}
/**
* Attempts a reconnection every 5 seconds until
* a conneciton is re-established
*/
function reconnect() {
const socket = getCurrentSocket()
console.log('Attempting reconnect')
if (socket.disconnected) {
console.log('Socket confirmed disconnected. Opening...')
socket.open()
} else {
window.setTimeout(reconnect, 5000)
}
}
/**
* Gets the current authorization id from local storage
*/
function getAuthId(): string {
return window.localStorage.getItem('auth')
}
function getCurrentSocket(): SocketIOClient.Socket {
const manager = sockio.managers[socketServer]
if (manager)
return manager.nsps['/notification']
}
/**
* Gets the current receiver id (application instance id)
*/
export function getReceiverId(): string {
return socketReceiverId
}
Finally, the NGINX config for the socket server:
server {
listen 80;
server_name [server name here];
location / {
proxy_pass "http://127.0.0.1:18000";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
}
}
I realize the code examples are verbose, but I've tinkered for over a day trying to find the issue, but with no solutions. Another issue is that nearly all examples for socket.io involve chat applications that don't cover authorization, origins, or how to properly disconnect, and in which order.
Socket.io client and server versions are the same (2.2.0)

How can I notify connection error to callers in nodejs?

I am using nodejs for websocket connection(https://github.com/websockets/ws). I defines a connect method to return Promise to caller. This is the code:
connect = () => {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url, {});
this.ws.on("open", () => {
console.log("connected:", this.ws.readyState);
resolve();
});
this.ws.on("close", function close() {
console.log("disconnected");
// how to throw error exception here?
});
} catch (err) {
reject(err);
}
});
};
the caller will call this method and try to reconnect if the connection failed.
connect().then(() => {
...
}).catch(() => {
// do reconnect
})
it works fine if it fails to connect on the first time. But what if the connection drops. How can I let caller knows that there is an error happens?
One solution I can think of is to create an event emitter to notify caller that we need to reconnect. But if the code is used in many places in my project, I have to pass the event emitter to many places. Is there any better way to do that?
It's unclear for me which npm package you're using but if it's ws you can refer to this part of its README and adapt to your use case if necessary:
https://github.com/websockets/ws/blob/master/README.md#how-to-detect-and-close-broken-connections

Reconnect socket on user login

I work with a setup created by create-react-app and use flux for data management and the application needs to implement socket on the client side (I use socket.io for this purpose).
Currently the socket is initialised in a Socket.js file the following way:
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
let socket = io.connect(remoteUrl + '?role=user');
socket.on('statusChange', (data) => {
return SocketWorker.receiveOrderStatusChange(data);
})
export { socket };
It does work, however the problem is that it only tries to connect to the server once, when the site is loaded. When the user opens the site unauthenticated it does not connect and misses to reconnect, thus the connection is not established and socket events are not received
I have tried to create a class instead and react an API for reconnect on the object, like:
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
function Socket() {
this.socket = io.connect(remoteUrl + '?role=user');
this.reconnect = () => {
this.socket = io.connect(remoteUrl + '?role=user');
}
}
let socket = new Socket();
socket.socket.on('statusChange', (data) => {
return SocketWorker.receiveOrderStatusChange(data);
})
export { socket };
I tried to call the Socket.reconnect() method, although it did not work and connection was not established either. Any idea or alternative solution?
The way I managed to solve this if anyone face the same problem with the Socket.io API:
First, you should encapsulate your Socket into an object created by the constructor, but there is no need to create a reconnect method as the connection is present already (and the auth can be handled through emitted events I will describe below) :
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
function Socket() {
this.socket = io.connect(remoteUrl + '?role=user');
this.socket.on('statusChange', (data) => {
return SocketWorker.receiveOrderStatusChange(data);
})
};
const socket = new Socket();
export { socket };
You can import the socket anywhere within your project:
import {socket} from './Socket';
And you can call:
socket.socket.emit('joinRoleRoom','user');
socket.socket.emit('joinIdRoom', _user._id);
On the server side, you just need to handled these events as follow:
socket.on('joinRoleRoom', (role) => {
socket.join(role)
console.log('Client joined to: ' + role);
});
socket.on('joinIdRoom', (id) => {
console.log('Client joined to: ' + id);
socket.join(id)
});
The socket will join the necessary rooms based on their auth info obtained during the auth process.
The original accepted answer from sznrbrt would work, but beware it has a serious security flaw.
If you do the following an attacker could join a room by just passing the desired user_id and start to receive sensitive user information. It could be private messages between two individual.
socket.socket.emit('joinRoleRoom','user');
socket.socket.emit('joinIdRoom', user_id);
Socket.io has an option to pass extraHeaders. One can use that to pass a token from the client. The server would use the desired authentication algorithm to decrypt the token and get the user_id.
Example:
socket.js
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
const socket = io.connect(remoteUrl + '?role=user');
const socketAuth = () => {
socket.io.opts.extraHeaders = {
'x-auth-token': 'SET_TOKEN',
};
socket.io.opts.transportOptions = {
polling: {
extraHeaders: {
'x-auth-token': 'SET_TOKEN',
},
},
};
socket.io.disconnect();
socket.io.open();
};
export { socket, socketAuth };
client.js
import { socket, socketAuth } from './socket';
//After user logs in
socketAuth();
server.js, using a package socketio-jwt-auth
io.use(jwtAuth.authenticate({
secret: 'SECRET',
succeedWithoutToken: true
}, (payload, done) => {
if (payload && payload.id) {
return done(null, payload.id);
}
return done();
}));

Resources