Carcigenicate
Carcigenicate

Reputation: 45750

Socket.io reprocesses first client when using dynamic namespaces with middleware

For some reason, if I register a dynamic namespace, the middleware fires, but fires with previously processed sockets that have already been put through the middleware.

Below, I set up a server, register a listener that automatically sets up middleware on new namespaces, register a dynamic namespace that uses a regex, then set up two other clients:

const { io } = require('socket.io-client');
const { Server } = require('socket.io');

const server = new Server(4321, {});

function middleware(socket, next) {
  console.log(socket.nsp.name, socket.handshake.headers.cookie);
}

server.on('new_namespace', (namespace) => {
  namespace.use(middleware);
})

const nsps = server.of(/^\/\w+$/);

const clientOne = io.connect('http://localhost:4321/firstNamespace', {
  extraHeaders: {
    Cookie: "client=one"
  }
});

const clientTwo = io.connect('http://localhost:4321/secondNamespace', {
  extraHeaders: {
    Cookie: "client=two"
  }
});

const clientThree = io.connect('http://localhost:4321/thirdNamespace', {
  extraHeaders: {
    Cookie: "client=three"
  }
});

The bizarre thing is, this prints out:

/firstNamespace client=one
/secondNamespace client=one
/thirdNamespace client=one

Note the client=one cookies. Instead of firing the middleware for each of the clients on different namespaces, the middleware three times on the first client, and not at all for the other two.

If I remove the latter two calls to io.connect and only leave the first, only this prints:

/firstNamespace client=one

So it is tied to the creation of the latter clients, but only the first client is used? I would love an explanation for this.


The actual scenario that lead to this was setting up a test to ensure that the auth middleware is automatically associated with new namespaces. That can be remedied by re-creating the server on every test, but I'd like to know why the above code behaves as it does.


Socket.io server and client are version 4.5.4, running on Node v16.15.0.

Upvotes: 0

Views: 232

Answers (1)

Delapouite
Delapouite

Reputation: 10167

In order to get more insights about what's happening under the hood we can rely on the fact that socket.io packages use the https://www.npmjs.com/package/debug package to expose useful info.

Therefore if we want to focus about what's going on from the client perspective, we can execute the following command (assuming that your snippet above is in index.js):

DEBUG='socket.io-client*' node index.js
  socket.io-client:url parse http://localhost:4321/firstNamespace +0ms
  socket.io-client new io instance for http://localhost:4321/firstNamespace +0ms
  socket.io-client:manager readyState closed +0ms
  socket.io-client:manager opening http://localhost:4321/firstNamespace +0ms
  socket.io-client:manager connect attempt will timeout after 20000 +10ms
  socket.io-client:manager readyState opening +1ms
  socket.io-client:url parse http://localhost:4321/secondNamespace +14ms
  socket.io-client:manager readyState opening +0ms
  socket.io-client:url parse http://localhost:4321/thirdNamespace +0ms
  socket.io-client:manager readyState opening +0ms
  socket.io-client:manager open +18ms
  socket.io-client:manager cleanup +0ms
  socket.io-client:socket transport is open - connecting +0ms
  socket.io-client:manager writing packet {"type":0,"nsp":"/firstNamespace"} +1ms
  socket.io-client:socket transport is open - connecting +1ms
  socket.io-client:manager writing packet {"type":0,"nsp":"/secondNamespace"} +1ms
  socket.io-client:socket transport is open - connecting +0ms
  socket.io-client:manager writing packet {"type":0,"nsp":"/thirdNamespace"} +0ms
NIua-1FB4AVjGHAgAAAB /firstNamespace client=one
jz9NAF39kOH8FZH5AAAC /secondNamespace client=one
BgHBTapkuNAGn_tMAAAD /thirdNamespace client=one

The final 3 lines of this input are from this part:

function middleware(socket, next) {
  console.log(socket.id, socket.nsp.name, socket.handshake.headers.cookie);
}

As you can observe I took the liberty to add the socket.id to confirm the the 3 sockets are indeed different which was not so obvious by looking only at the cookie part.

What's happening related to Cookie is that they are transmitted once in the HTTP headers of the HTTP connection on which the WebSocket protocol is then upgraded. Look for 101 HTTP status code on the MDN documentation for more details about this part.

The 3 consecutive calls to io.connect(…) reuse the same cached Manager as can be seen from the source code : https://github.com/socketio/socket.io-client/blob/main/lib/index.ts#L34-L72

Here's the relevant extract:

  const newConnection =
    opts.forceNew ||
    opts["force new connection"] ||
    false === opts.multiplex ||
    sameNamespace;

  let io: Manager;

  if (newConnection) {
    debug("ignoring socket cache for %s", source);
    io = new Manager(source, opts);
  } else {
    if (!cache[id]) {
      debug("new io instance for %s", source);
      cache[id] = new Manager(source, opts);
    }
    io = cache[id];
  }
  if (parsed.query && !opts.query) {
    opts.query = parsed.queryKey;
  }
  return io.socket(parsed.path, opts);

In conclusion, to obtain 3 distinct client, offering 3 distinct Cookie value, you have to disable this cache by forcing a new underlying connection, with the forceNew option:

const clientOne = io.connect('http://localhost:4321/firstNamespace', {
  forceNew: true,
  extraHeaders: {
    Cookie: "client=one"
  }
});

const clientTwo = io.connect('http://localhost:4321/secondNamespace', {
  forceNew: true,
  extraHeaders: {
    Cookie: "client=two"
  }
});

const clientThree = io.connect('http://localhost:4321/thirdNamespace', {
  forceNew: true,
  extraHeaders: {
    Cookie: "client=three"
  }
});

Upvotes: 1

Related Questions