Right now you can bind middleware to io.use(middleware);, but this is triggered only when a socket connection is made. Is there a way to intercept it before passing it to an event handle like in expressjs?
In other word....
In express.js you can do:
app.get('/', middleware1, middleware2, function(req, res){
res.end();
});
Can I do the same in socket.io?
socket.on('someEvent', middleware1, middleware2, function(data){
console.log(data); // data is now filtered by 2 previous middlewares
});
As of Socket.io v2.0.4 (found this while inspecting this) you can use a socket middleware like so:
io.on('connection', (socket) => {
socket.use((packet, next) => {
// Handler
next();
});
});
Which means that you can set your code like so:
io.on('connection', (socket) => {
socket.use((packet,next) => {
if (packet[0] === 'someEvent') {
if (authorizationCondition) {
return next(new Error('nope'))
}
}
next();
})
socket.on('someEvent', function(data){
console.log(data);
});
})
You will then be able to catch errors like this with a client-side
io.on('error', err => { ... }) // err will equal "nope"
The npm module socket.io-events can help you. The npm repository is well documented.
var router = require('socket.io-events')();
// handles all events
router.on(function (socket, args, next) {
next();
});
var io = require('socket.io')(3000);
io.use(router);
Related
Yes, I have gone through the documentation, which is very well written:
Socket IO Cheatsheet
Here is the problem: I want to notify the user of a logout when his session from the Express App is being destroyed. Now this is what is happening: When I log out from the session, all other clients (including those who have or have not even logged in) get a message saying they're logged out. Yes, my express app is working fine - they are not getting logged off, but I believe SOCKET IO is sending them the message regardless. I ran the debugger and it turns out that both the clients are distinguishable, too.
Here is my code:
server.js:
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server);
app.set('socketio', io);
io.on('connection', function (socket) {
app.set('current_socket', socket);
console.log('No of clients:', io.engine.clientsCount);
});
userController.js:
exports.userLogout = function(req, res, next) {
const sessionID = req.session.id;
const io = req.app.get('socketio');
const this_socket = req.app.get('current_socket');
req.session.destroy(function (err){
if(err) {
console.error("userLogout failed with error:", err);
return next(err);
}
else {
console.log("this_socket:", this_socket);
console.log("io:", io);
this_socket.emit('userAction', { action: 'logout' });
//other logic to remove old sessions from DB Session Store
//and finally:
res.status(200)
.json({
status: 'success',
api_data: {'loggedIn':false},
message: 'Logout successful.'
});
}
}
}
I even tried this instead:
io.emit('userAction', { action: 'logout' });
but turns out it still emits to all the clients. I am pretty sure there is a mismatch somewhere, just can't figure out where.
You need create room for each session id if you want to send emits to spesific user
io.on('connection', function (socket) {
app.set('current_socket', socket);
var sessionId = socker.request.session.id
//join room
socket.join(sessionId);
});
userController.js:
exports.userLogout = function(req, res, next) {
const sessionID = req.session.id;
const io = req.app.get('socketio');
const this_socket = req.app.get('current_socket');
req.session.destroy(function (err){
if(err) {
console.error("userLogout failed with error:", err);
return next(err);
}
else {
console.log("this_socket:", this_socket);
console.log("io:", io);
this_socket.sockets.in(sessionID).emit('userAction', { action: 'logout' });
//other logic to remove old sessions from DB Session Store
//and finally:
res.status(200)
.json({
status: 'success',
api_data: {'loggedIn':false},
message: 'Logout successful.'
});
}
}
}
You have to define unique socket object for each user. We have many ways to do that.
In simple way, we use user id (unique) as a key to store socket object (Map way: key(userId) - vaule(socketObj)).
Follow the rabbit:
When a user loggedin, client side emits a event (login) to server side, the event include the user id.
Client Side:
// login success
socket.emit('userLoggedIn', {userId: THE_USER_ID})
Server Side:
io.on('connection', function (socket) {
// app.set('current_socket', socket);
console.log('No of clients:', io.engine.clientsCount);
socket.on('userLoggedIn', function(data) => {
app.set(data.userId, socket); // save socket object
})
});
userController.js:
exports.userLogout = function(req, res, next) {
const sessionID = req.session.id;
const userId = MAGIC_GET_USER_ID_FROM_SESSION_ID(sessionID) // who want to logout
const io = req.app.get('socketio');
const this_socket = req.app.get(userId); // get "user socket"
req.session.destroy(function (err){
if(err) {
console.error("userLogout failed with error:", err);
return next(err);
}
else {
console.log("this_socket:", this_socket);
console.log("io:", io);
this_socket.emit('userAction', { action: 'logout' });
//other logic to remove old sessions from DB Session Store
//and finally:
res.status(200)
.json({
status: 'success',
api_data: {'loggedIn':false},
message: 'Logout successful.'
});
}
}
}
I want to keep routes separate from controller.
My route is:
'use strict';
module.exports = function(app) {
var controller = require('../controllers/controller');
app.route('/').get(controller.index);
};
And controller is:
exports.index = function() {
request = new Request(
"MYQUERY",
function(err, rowCount) {
if (err) {
console.log(err);
} else {
console.log(rowCount + ' rows');
}
connection.close();
}
);
request.on('row', function(columns) {
columns.forEach(function(column) {
if (column.value === null) {
console.log('NULL');
} else {
console.log(column.value);
}
});
});
connection.execSql(request);
};
I am able to see the result in the terminal console but I want to return it as JSON to http. I can use the following if I am using controller and routes all together:
router.get('/about', function (req, res) {
res.send('About this wiki');
})
The callback function to .get (or any router request handler) takes at least two arguments: request and response. You can see this with your example:
router.get('/about', function (req, res) {
res.send('About this wiki');
})
You could rewrite this to make the callback a named function rather than an anonymous function:
const aboutHandler = function (req, res) {
res.send('About this wiki');
});
router.get('/about', aboutHandler);
Your controller.index is the same kind of function, so it will take those two arguments. You just have to change your function to take them:
exports.index = function (req, res) {
This will give you access to res, and you can use it as you need to do send the response via res.send or res.json if you build a JSON object by accumulating the row results. You can use request.on('end' ... to know when the query has emitted all its results.
I might be misunderstanding your question, but do you mean res.json(...);?
Is there a way that I can assert the middleware is actually attached to the route in my unit test below?
The server ..
var express = require('express');
var http = require('http');
var app = express();
var server = http.createServer(app);
var PORT = 5000;
var myModule = require('myModule');
var handler = require('handler');
var myMiddleware = myModule.doStuff(server);
app.use('/', myMiddleware);
app.route('/')
.get(function(req, res) {
handler.respond(req, res);
});
if (!module.parent) {
server.listen(PORT, function(err) {
if (err) {
logger.error(err);
return;
}
});
}
module.exports = app;
handler.js contains ..
var handler = {
respond: function(req, res) {
res.sendStatus(200);
}
};
module.exports = handler;
myModule.doStuff is implemented using ..
function doStuff(server) {
const SERVER = server;
return function (req, res, next) {
if (server.connections === 2) {
res.send({
message: 'Overloaded'
});
} else {
next();
}
}
}
I test the routes are setup using ..
describe('/', function() {
it('should test endpoint', function(done) {
sandbox.stub(handler, 'respond', function(request, response) {
response.send('called');
});
request(app).get('/')
.expect('called')
.end(done);
});
});
Is there a way that I can assert the middleware is actually attached
to the route in my unit test?
Well that question actually has two sides to it.
Testing that Express.js works as its supposed to
I would personally consider it a waste of time to test Express.js. It already has it's own test suite and it has been around for ages and therefore you should be able to trust that app.use() works as it is supposed to.
Testing that you are passing the expected middleware to app.use()
This is more relevant and you can lay the groundwork for being able to perform tests by structuring your code this way:
var myMiddleware = {
m1: {
path: 'somePath',
callback: 'someCallback'
},
m2: {
path: 'anotherPath',
callback: 'anotherCallback'
}
}
function mountMiddleware () {}
for (var middleware in myMiddleware) {
app.use(middleware.path, middleware.callback);
}
}
Now you can perform unit testing on the contents of myMiddleware and the workings of mountMiddleware() and thereby assert that all your middleware is correctly passed to app.use() (which you expect to work).
If you can change your middleware to something like this:
function doStuff(server) {
const SERVER = server;
return function (req, res, next) {
req.doStuffRun = true;
if (server.connections === 2) {
res.send({
message: 'Overloaded'
});
} else {
next();
}
}
}
Then you will be able to test for req.doStuffRun in your callbacks.
But here it doesn't seem that it is called because you are not running the actual request but you are calling the handler which does not contain the middleware.
var database = require('database');
var express = require('express');
var app = express();
var cors = require('cors');
app.use(cors());
var bodyParser = require('body-parser');
var urlencodedParser = bodyParser.urlencoded({
extended: false
});
app.post('/dosomething', urlencodedParser, function(req, res) {
if (!req.body.a) {
res.status(500).send(JSON.stringify({
error: 'a not defined'
}));
return;
}
firstAsyncFunction(req.body.a, function(err, result) {
if (err) {
res.status(500).send('firstAsyncFunction was NOT a success!');
} else {
if (result.b) {
secondAsyncFunction(result.b, function(err, data) {
if (err) {
res.status(500).send('secondAsyncFunction was NOT a success!');
return;
}
res.send('EVERYTHING WAS A SUCCESS! ' + data);
});
}
else {
res.status(500).send('result.b is not defined');
}
}
});
});
function firstAsyncFunction(param, callback) {
//Some network call:
// Return either return (callback(null,'success')); or return (callback('error'));
var query = database.createQuery(someOptionsHere);
database.runDatabaseQuery(query, function(err, entities, info) {
if (err) {
return (callback('error'));
}
return (callback(null, 'success'));
});
};
function secondAsyncFunction(param, callback) {
//Some network call:
// Return either return (callback(null,'success')); or return (callback('error'));
var query = database.createQuery(someOptionsHere);
database.runDatabaseQuery(query, function(err, entities, info) {
if (err) {
return (callback('error'));
}
return (callback(null, 'success'));
});
};
var server = app.listen(process.env.PORT || 3000, function() {
var host = server.address().address;
var port = server.address().port;
console.log('App listening at http://%s:%s', host, port);
});
module.exports = app;
I have here a basic express http server. This server has one route, dosomething, which makes two network calls and tells the user if they were a success or not.
This is my entire webserver (this is a bare bones server of my actual server for example purposes). I am now concerned with this server crashing. Reading the docs for express I see there is a default error handler which will catch errors and prevent the server from crashing (http://expressjs.com/en/guide/error-handling.html). I have added the code:
function defaultErrorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
res.status(500);
res.render('error', { error: err });
}
app.use(defaultErrorHandler);
This still crashes my server though. For example. I had a problem with my database returning an improper JSON response and inside of my firstAsyncFunction (not shown in the code) I tried to parse the JSON and it caused an error telling me it was improper JSON and the server crashed and was unable to take requests anymore until I restarted it. I would like to avoid this and have the default error handler send out a generic response back to the user when this occurs. I thought if I specified the defaultErrorHandler and put it inside of app.use that it would capture and handle all errors, but this does not seem to be the case? Inside of my async function for example you can see I am looking if an error was returned and if it was I send an error back to the user, but what if some other error occurs, how can I get express to capture and handle this error for me?
The defaultErrorHandler cannot handle exceptions that are thrown inside asynchronous tasks, such as callbacks.
If you define a route like:
app.get('/a', function(req, res) {
throw new Error('Test');
});
An error will be thrown, and in this case defaultErrorHandler will successfully catch it.
If the same exception occurs in an async manner, like so:
app.get('/a', function(req, res) {
setTimeout(function () {
throw new Error('Test');
}, 1000);
});
The server will crush, because the callback is actually in another context, and exceptions thrown by it will now be caught by the original catcher. This is a very difficult issue to deal with when it comes to callback.
There is more than one solution though. A possible solution will be to wrap every function that is prone to throw error with a try catch statement. This is a bit excessive though.
For example:
app.get('/a', function(req, res) {
setTimeout(function () {
try {
var x = JSON.parse('{');
}
catch (err) {
res.send(err.message);
}
}, 1000);
});
A nicer solution:
A nicer solution, would be to use promises instead, if it's possible, then for example you can declare a single errorHandler function like so:
function errorHandler(error, res) {
res.send(error.message);
}
Then, let's say you have to following function with fetches stuff from the database (I used setTimeout to simulate async behavior):
function getStuffFromDb() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve("{");
}, 100);
});
}
Notice that this function returns an invalid JSON string. Your route will look something like:
app.get('/a', function(req, res) {
getStuffFromDb()
.then(handleStuffFromDb)
.catch(function (error) { errorHandler(error, res) });
});
function handleStuffFromDb(str) {
return JSON.parse(str);
}
This is a very simplified example, but you can add a lot more functionality to it, and (at least theoretically) have a single catch statement which will prevent your server from crushing.
what´s the best way if I want to make a GET request in a route?
api.js
api.route('/guests')
.get(function(req, res) {
Guest.find(function(err, guests) {
if (err)
res.send(err);
res.json(guests);
});
});
routes.js
app.get('/export', requiresLogin, function(req, res) {
/* make a GET request to my api (eg.: 'api/guests') */
/* and save the 'guests' to a variable */
});
First Solution
Instead of calling internal apis, you can define a controller guestCtrl.js and call the function from guestCtrl.js in api.js and routes.js
guestCtrl.js
module.exports = {
getGuests : function(){
Guest.find(function(err, guests) {
if (err)
//handle error
return [];
else
return guests;
});
}
}
api.js
//path of guests.js
var guestCtrl = require('guestCtrl.js');
api.route('/guests').get(function(req, res) {
return guestCtrl.getGuests();
});
routes.js
var guestCtrl = require('guestCtrl.js');
app.get('/export', requiresLogin, function(req, res) {
var guests = guestsCtrl.getGuests();
// do whatever you like to do with guests
});
Second Solution
If you really want to work with internal api, then you can use request module.
e.g.
routes.js
var request = require('request');
app.get('/export', requiresLogin, function(req, res) {
// you can put the hostname and port here
request('http://127.0.0.1:3000/api/guests', function(err, body, response){
var guests = body; // and save the 'guests' to a variable
});
});