Socket.io server TransportError - express

I'm facing to an issue with a Socket.io server running throw an Express.js server with Next.js.
The server send error without any client connected. But clients can connect from browser without any issue...
Here is the error on console where the server is running :
Socket error TransportError: xhr poll error
at Polling.onError (/Users/cedricbapst/Projects/gynemanager-frontend-next/node_modules/engine.io-client/build/cjs/transport.js:46:37)
at Request.<anonymous> (/Users/cedricbapst/Projects/gynemanager-frontend-next/node_modules/engine.io-client/build/cjs/transports/polling.js:255:18)
at Request.Emitter.emit (/Users/cedricbapst/Projects/gynemanager-frontend-next/node_modules/#socket.io/component-emitter/index.js:143:20)
at Request.onError (/Users/cedricbapst/Projects/gynemanager-frontend-next/node_modules/engine.io-client/build/cjs/transports/polling.js:356:14)
at Timeout._onTimeout (/Users/cedricbapst/Projects/gynemanager-frontend-next/node_modules/engine.io-client/build/cjs/transports/polling.js:329:30)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7) {
description: 0,
context: XMLHttpRequest {
UNSENT: 0,
OPENED: 1,
HEADERS_RECEIVED: 2,
LOADING: 3,
DONE: 4,
readyState: 4,
onreadystatechange: [Function (anonymous)],
responseText: 'Error: getaddrinfo ENOTFOUND undefined\n' +
' at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:71:26)',
responseXML: '',
status: 0,
statusText: Error: getaddrinfo ENOTFOUND undefined
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:71:26) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'undefined'
},
open: [Function (anonymous)],
setDisableHeaderCheck: [Function (anonymous)],
setRequestHeader: [Function (anonymous)],
getResponseHeader: [Function (anonymous)],
getAllResponseHeaders: [Function (anonymous)],
getRequestHeader: [Function (anonymous)],
send: [Function (anonymous)],
handleError: [Function (anonymous)],
abort: [Function (anonymous)],
addEventListener: [Function (anonymous)],
removeEventListener: [Function (anonymous)],
dispatchEvent: [Function (anonymous)]
},
type: 'TransportError'
}
socket.io-client:manager reconnect attempt error +1ms
socket.io-client:manager will wait 5000ms before reconnect attempt +0ms
Just to be clear, this error is not on client side but on the server, clients from browser have a working connection even with this error from the server. The client does not get any error and works fine.
Here is my express.js server code :
import express, {Express} from 'express';
import * as http from 'http';
import next, {NextApiHandler} from 'next';
import passport from 'passport';
import session, {SessionOptions} from 'express-session';
import {Options} from "session-file-store";
const FileStore = require('session-file-store')(session);
import uid from 'uid-safe';
import bodyParser from 'body-parser';
import routes from './routes';
import SocketServer from "../socketio/SocketServer";
import socketMiddleware from "../socketio/SocketMiddleware";
const port: number = parseInt(process.env.PORT || '3000', 10);
const dev: boolean = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev });
const nextHandler: NextApiHandler = nextApp.getRequestHandler();
nextApp.prepare().then(() => {
const app: Express = express();
const server: http.Server = http.createServer(app);
// Socket io server
const socketServer = new SocketServer(server);
// Session
const fileStoreOptions: Options = {
path: './.next/session' // register session folder in .next folder
};
const sessionConfig: SessionOptions = {
//secret: uid.sync(18),
genid: (req) => {
return 'app_' + uid.sync(18) // use UUIDs for session IDs
},
secret: 'SECRET',
cookie: {
maxAge: 43200 * 1000 // 12h
},
resave: false,
saveUninitialized: true,
store: new FileStore(fileStoreOptions)
};
app.use(bodyParser.json());
app.use(session(sessionConfig));
passport.serializeUser((user: any, done: (a: any, b: string) => void) => done(null, user));
passport.deserializeUser((user: string, done: (a: any, b: any) => void) => done(null, user));
app.use(passport.initialize());
app.use(passport.session());
// Add socketio to request to have available in express routes
app.use(socketMiddleware(socketServer));
app.use('/api/auth', routes.security); // anonymous
app.use('/api/users', routes.users); // authenticated
app.use('/api/patients', routes.patients); // authenticated
app.use('/api/appointments', routes.appointments); // authenticated
app.use('/api/consultations', routes.consultations); // authenticated
app.use('/api/tasks', routes.tasks); // authenticated
app.use('/api/documents', routes.documents); // authenticated
app.use('/', routes.nextjs); // anonymous and authenticated
// Handle all nextjs route
app.all('*', (req: any, res: any) => nextHandler(req, res));
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
And then my SocketServer.ts
import {Server, Socket} from "socket.io";
import * as http from 'http';
import {ServerEvents, ClientEvents} from "./SocketEvents";
class SocketServer {
private io: Server;
public constructor(server: http.Server) {
this.io = new Server(server);
this.initServer();
}
private initServer = (): void => {
this.io.on(ServerEvents.CONNECTION, (socket: Socket) => {
this.initClient(socket);
});
}
private initClient = (socket: Socket): void => {
console.log('New client connected with id', socket.id);
// Emit status message for new connected client
socket.emit(ServerEvents.STATUS, 'Connected on Gynemanager SocketIO server');
socket.on(ClientEvents.USER_CONNECTED, async (socketId, username) => {
const sockets = await this.io.fetchSockets(); // Get all connected clients
// Add username to socket data
sockets.forEach(socket => {
if (socket.id === socketId) socket.data.username = username; // add username to new connected client
})
})
socket.on(ServerEvents.DISCONNECT, (reason: string) => {
console.log('Client disconnected', reason);
})
}
}
export default SocketServer
And then the SocketMiddleware.ts
import {Response, NextFunction} from 'express';
import SocketServer from "./SocketServer";
import {CustomRequest} from "../types/express";
const socketMiddleware = (socketServer: SocketServer) => {
return (req: CustomRequest, res: Response, next: NextFunction) => {
req.socketServer = socketServer;
next();
}
}
export default socketMiddleware;
Any help will be appreciate.

Finally I figure out what was my problem.
I initialised the socket.io connection from _app.tsx for clients because this is the place where I use React context provider to bind my SocketClient.ts to the whole application.
To fix the issue I have to move the connection initialisation from _app.tsx to another page (for my case from the page redirected from login page), and check if connection is already init on the other page where socket is also needed to avoid multi connection and then everything works as expected.

Related

"New" redis session store with express, asynchronous creation?

I the past I could just easily add session management using redis as store in express using express-session like so:
import session from "express-session";
module.exports = (config?: {[p: string]: unknown}) => {
server.use(cookieParser());
// etc etc
server.use(session(sessCfg));
server.use('/', router);
return server;
}
This still work when creating a memory store/default store:
import session from "express-session";
import express = require('express');
const server = express();
function sessCfgGenerator() {
return {
"secret": "fdakjlhgafgdsahjkhldg",
"name": "bondinet.sid",
"cookie": {
"httpOnly": true,
"secure": false
},
"resave": false,
"saveUninitialized": true,
"unset": "destroy",
}
}
module.exports = (config?: {[p: string]: unknown}) => {
server.use(cookieParser());
// etc etc
server.use(session(sessCfgGenerator()));
server.use('/', router);
return server;
}
However nowadays to generate a redis store you have to create the client settings and then asynchronous connect to it.
let redisClient = redis.createClient({
socket: {
reconnectStrategy: function (retries: number) {
if (retries > 1000) {
// End reconnecting with built in error
return new Error("Retry time exhausted");
}
// reconnect after
return Math.min((retries + 1) * 100, 60000);
},
}
});
await redisClient.connect();
store = new RedisStore({
client: redisClient,
prefix: prefix,
});
I've tried to include the code into the above loader. To make sure it awaits for redis to start/connect:
import session from "express-session";
import express = require('express');
const server = express();
async generateRedisStore(prefix) {
let redisClient = redis.createClient({
socket: {
reconnectStrategy: function (retries: number) {
if (retries > 1000) {
// End reconnecting with built in error
return new Error("Retry time exhausted");
}
// reconnect after
return Math.min((retries + 1) * 100, 60000);
},
}
});
await redisClient.connect();
return new RedisStore({
client: redisClient,
prefix: prefix,
});
}
async function sessCfgGenerator() {
const opt = {
"secret": "fdakjlhgafgdsahjkhldg",
"name": "bondinet.sid",
"cookie": {
"httpOnly": true,
"secure": false
},
"resave": false,
"saveUninitialized": true,
"unset": "destroy",
store: await generateRedisStore('');
}
}
module.exports = (config?: {[p: string]: unknown}) => {
server.use(cookieParser());
// etc etc
sessCfgGenerator.then(cfg => {
server.use(session(cfg));
console.log("SESSION STARTED");
});
server.use('/', router);
return server;
}
However when I use the asynchronous version I notice that the session management is actually never "available". Even though the line runs (I see clearly the "SESSION STARTED" in the logs), whenever I test a route the session isn't there (below shows "undefined"):
router.get('/test', function(req, res, next) {
const r = 'respond with a test - ' + (req.session ? "loaded" : "undefined");
res.send(r);
});
What is the solution, and how can I make this work? These are actively used libraries so it must be working right?

Axios get request not returning when express session uses connect-redis store

So I have this weird thing going on where the axios requests I make from the front end do not get the response I'm sending from the server, but it only happens when I set the store in express session.
I am making a post request to the login route like in this function
const logIn = async (formData) => {
const config = {
headers: {
"Content-Type": "application/json",
},
};
const response = await axios.post("/login", formData, config);
// do something with the response
};
When I make the request the server responds with status 200 and a user object. When I check the network tab in the browser devtools, I see that it is getting the correct response and the payload is showing the right user object as well
However axios never returns and is left hanging forever.
Now here's the kicker. This only happens if I set the store on the express session in my app.js
import express from "express";
import router from "./routes/router.js";
import session from "express-session";
import "dotenv/config";
import redis from "redis";
import connectRedis from "connect-redis";
const PORT = process.env.PORT || 5000;
const app = express();
const redisClient = redis.createClient();
redisClient.connect();
redisClient.on("connect", () => {
console.log("redis client connected");
});
const RedisStore = connectRedis(session);
app.use(
session({
name: "qid",
store: new RedisStore({ client: redisClient }), // RIGHT HERE
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365,
httpOnly: true,
sameSite: "lax",
secure: false,
},
saveUninitialized: false,
secret: process.env.SESSION_SECRET,
resave: false,
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use("/", router);
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
As soon as I comment out the line where I set the store, Axios gets the response and everything works perfectly. So I have to assume that the session store is somehow blocking the server response from getting to axios.
Do you have any idea why that might be?
I ran into the exact same issue, but I could finally solve this for my setup by using ioredis instead of redis.
I updated your app.js accordingly
import express from "express";
import router from "./routes/router.js";
import session from "express-session";
import "dotenv/config";
import Redis from 'ioredis';
import connectRedis from "connect-redis";
const PORT = process.env.PORT || 5000;
const app = express();
const redisClient = new Redis();
redisClient.on("connect", () => {
console.log("redis client connected");
});
const RedisStore = connectRedis(session);
app.use(
session({
name: "qid",
store: new RedisStore({ client: redisClient }), // RIGHT HERE
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365,
httpOnly: true,
sameSite: "lax",
secure: false,
},
saveUninitialized: false,
secret: process.env.SESSION_SECRET,
resave: false,
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use("/", router);
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Phoenix Channel can't maintain connection with frontend once the connection is established (Error during WebSocket handshake)

I have a problem with WebSocket connection.
I use Phoenix as my API and Vue + phoenix-socket on the frontend.
My browser console looks like this:
receive: ok feed:1 phx_reply (1) {response: {…}, status: "ok"}
Joined successfully {feed: Array(3)}
WebSocket connection to 'ws://localhost:4000/socket/websocket?vsn=1.0.0' failed: Error during WebSocket handshake: Unexpected response code: 403
push: phoenix heartbeat (2) {}
receive: ok phoenix phx_reply (2) {response: {…}, status: "ok"}
WebSocket connection to 'ws://localhost:4000/socket/websocket?vsn=1.0.0' failed: Error during WebSocket handshake: Unexpected response code: 403
and so goes on...
As you can see the connection can be established and data goes through but then it sends errors.
So I checked Phoenix:
[info] CONNECTED TO TweeterApiWeb.UserSocket in 0┬Ás
Transport: :websocket
Serializer: Phoenix.Socket.V1.JSONSerializer
Parameters: %{"token" => "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0d2VldGVyX2FwaSIsImV4cCI6MTU5NjkxMjAxNiwiaWF0IjoxNTk0NDkyODE2LCJpc3MiOiJ0d2VldGVyX2FwaSIsImp0aSI6IjViYWFlMDRlLTBjMTYtNDEyMi05Y2VlLWZmMzQ2OWM1YWE1YiIsIm5iZiI6MTU5NDQ5MjgxNSwic3ViIjoiMSIsInR5cCI6ImFjY2VzcyJ9.-ZJMyyEBKd0_nHYUBGdaI0qdHn1nuWtpG8sEUHqikBuWTB2sKw9Sk36OsUpXBS5ozRpe2l2VXq8NI58HydIhZA", "vsn" => "1.0.0"}
[debug] QUERY OK source="tweets" db=0.0ms idle=875.0ms
SELECT t0."id", t0."content", t0."comment_count", t0."retweet_count", t0."like_count", t0."profile_id", t0."inserted_at", t0."updated_at" FROM "tweets" AS t0 WHERE (t0."profile_id" = $1) [1]
[info] JOINED feed:1 in 0┬Ás
Parameters: %{}
[info] REFUSED CONNECTION TO TweeterApiWeb.UserSocket in 0┬Ás
Transport: :websocket
Serializer: Phoenix.Socket.V1.JSONSerializer
Parameters: %{"vsn" => "1.0.0"}
It looks like the connection is refused because there is no token in params but I don't really understand why.
I only check the authentication when the socket connects, so the token should be completely unnecessary once the connection is established.
Here is my user_socket.ex:
defmodule TweeterApiWeb.UserSocket do
use Phoenix.Socket
alias TweeterApi.Accounts
alias TweeterApi.Accounts.Guardian
## Channels
channel "feed:*", TweeterApiWeb.FeedChannel
#impl true
def connect(%{"token" => token}, socket, _connect_info) do
case Guardian.resource_from_token(token) do
{:ok, user, _claims} ->
current_profile = Accounts.get_profile_by(user_id: user.id)
{:ok, assign(socket, :current_profile_id, current_profile.id)}
{:error, _reason} ->
:error
end
end
def connect(_params, _socket, _connect_info), do: :error
#impl true
def id(socket), do: "users_socket:#{socket.assigns.current_profile_id}"
end
The channel code:
defmodule TweeterApiWeb.FeedChannel do
use TweeterApiWeb, :channel
alias TweeterApi.Tweets
def join("feed:" <> current_profile_id, _params, socket) do
if String.to_integer(current_profile_id) === socket.assigns.current_profile_id do
current_profile_tweets = Tweets.list_profile_tweets(current_profile_id)
response = %{
feed:
Phoenix.View.render_many(current_profile_tweets, TweeterApiWeb.TweetView, "tweet.json")
}
{:ok, response, socket}
else
{:error, %{reson: "Not authorized"}}
end
end
def terminate(_reason, socket) do
{:ok, socket}
end
end
and Vue.js code:
<script>
import UserProfileSection from '#/components/sections/UserProfileSection.vue'
import TimelineSection from '#/components/sections/TimelineSection.vue'
import FollowPropositionsSection from '#/components/sections/FollowPropositionsSection.vue'
import NewTweetForm from '#/components/sections/NewTweetForm.vue'
import { mapGetters } from 'vuex'
import { Socket } from 'phoenix-socket'
export default {
name: 'AppFeed',
components: {
UserProfileSection,
TimelineSection,
FollowPropositionsSection,
NewTweetForm,
},
data() {
return {
tweets: [],
}
},
computed: {
...mapGetters('auth', ['currentProfileId', 'token']),
...mapGetters('feed', ['tweets'])
},
created() {
},
mounted() {
const WEBSOCKET_URL = 'ws://localhost:4000'
const socket = new Socket(`${WEBSOCKET_URL}/socket`, {
params: { token: this.token },
logger: (kind, msg, data) => {
console.log(`${kind}: ${msg}`, data)
},
})
socket.connect()
this.channel = socket.channel('feed:' + this.currentProfileId, {})
this.channel
.join()
.receive('ok', (resp) => {
console.log('Joined successfully', resp)
console.log(resp)
this.tweets = resp.feed
})
.receive('error', (resp) => {
console.log('Unable to join', resp)
})
}
}
</script>
In a typical phoenix app assign the user's token is assigned to the window in our layout's body.
<script>window.userToken = "<%= assigns[:user_token] %>"</script>
And for the socket creation in assets/js/socket.js
let socket = new Socket("/socket", {
params: {token: window.userToken},
})
I believe that when you create your socket: { token: this.token }, this.token is undefined, so it is not sent in params.
Edit:
If you don't care about the token, don't pattern match it. If there is no token you will jump into your second connect which will pattern match anything and refuse the connection.

POST request freezes after add body-parser

I'm build vue app, and for mine app need api request to server from client, also necessary proxy any request.
It's mine vue.config.js
const producer = require('./src/kafka/producer');
const bodyParser = require('body-parser')
module.exports = {
devServer: {
setup: function (app, server) {
app.use(bodyParser.json())
app.post('/send-message', function (req, res) {
producer.send(req.body)
.then(() => {
res.json({result: true, error: null});
})
.catch((e) => {
res.status(500).json({result: false, error: e});
})
});
},
proxy: {
'/v2/order/by-number': {
target: 'http://address-here'
}
}
}
};
As you can see so i'm use body-parser app.use(bodyParser.json())
After I added it, proxying stopped working for me. Request to /send-message freezes after show me error
Proxy error: Could not proxy request path-here from localhost:8080
to http://address-here
Internet searches have not led to a solution.
For a long time, i find a solution:
Add second param jsonParser to app.post()
See full example
const producer = require('./src/kafka/producer');
const bodyParser = require('body-parser')
const jsonParser = bodyParser.json({limit: '1mb'});
module.exports = {
devServer: {
setup: function (app, server) {
app.post('/send-message', jsonParser, function (req, res) {
producer.send(req.body)
.then(() => {
res.json({result: true, error: null});
})
.catch((e) => {
res.status(500).json({result: false, error: e});
})
});
},
proxy: {
'path': {
target: 'http://address-here'
}
}
}
};

How to control when socket connection request is sent to backend from client?

Using vue-socket.io my frontend is currently firing off a Socket.io connection request to my backend when my app is being built initially or every time I refresh the page. This is problematic for me as my page gets initially built at the landing page. This means that it is sending a socket request without the proper data since the user hasn't logged in yet. Once the user logs in I am unable to send another socket connection request.
socket.js (plugins folder) FRONTEND
import Vue from "vue";
import store from "../store";
import SocketIO from "socket.io-client";
import VueSocketIO from "vue-socket.io";
Vue.use(
new VueSocketIO({
debug: true,
connection: SocketIO("http://localhost:3000", {
query: { token: store.state.token }
}),
vuex: {
store,
actionPrefix: "SOCKET_",
mutationPrefix: "SOCKET_"
}
})
);
socket.js (controllers folder) BACKEND
io.use(function (socket, next) {
console.log('Token ' + socket.handshake.query.token);
if (socket.handshake.query && socket.handshake.query.token) {
jwt.verify(socket.handshake.query.token, 'THIS IS SUPPOSED TO BE PRIVATE', function (err, decoded) {
if (err) return next(new Error('Authentication error'));
socket.decoded = decoded;
next();
});
} else {
next(new Error('Authentication error'));
}
})
.on('connection', function (socket) {
socket.on('JOIN_ROOM', (room) => {
socket.join(room);
});
// CourseQuestions Page
socket.on('POST_QUESTION', (data) => {
I am looking for a way to programmatically send off the socket connection request AGAIN from the frontend once the user has logged in preferable using vue-socket.io.
You should pass { autoConnect: false } as options into new VueSocketIO().
Something like this:
new VueSocketIO(..., { autoConnect: false }, ...)
And then, when you want to open the connection just call this.$socket.open().
GitHub issue. and check out that comment.