Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typescript] share session data across express and socket.io #3890

Closed
el-zorab opened this issue Apr 19, 2021 · 15 comments
Closed

[typescript] share session data across express and socket.io #3890

el-zorab opened this issue Apr 19, 2021 · 15 comments
Labels
documentation Improvements or additions to documentation

Comments

@el-zorab
Copy link

Describe the bug
I recently moved from JavaScript to TypeScript and I'm having a difficult time trying to share session data across express and socket.io.

To Reproduce
Socket.IO server version: 4.0.1

Server
main.ts:

import http from 'http';
import express from 'express';
import expressSession from 'express-session';
import { Server } from 'socket.io';

const session = expressSession({
    // session config...
});

const app = express();

app.use(session);

const httpServer = http.createServer(app);

const io = new Server(httpServer);

// *need to achieve session sharing here*

// rest socket.io configuration...

httpServer.listen(80, () => console.log('http listening'));

types.ts, to expand (I doubt this is the appropriate word) the session object by augmenting the module:

import 'expresss-session';

declare module 'express-session' {
    interface SessionData {
        username: string
    }
};

The above allows me to use req.session.username in app callbacks.

Expected behavior
I want to be able to access express session data from a Socket object.

Platform:
Windows 10, Node v14.16.0, TypeScript v4.2.4

Additional context
In JS, this is what I used:

io.use((socket, next) => session(socket.request, socket.request.res || {}, next));

I tried two options to achieve the same in TS:

  1. I changed the above line by little but I encountered two errors:
io.use((socket: Socket, next) => session(socket.request, socket.request.res || {}, next));
  • Property 'res' does not exist on type 'IncomingMessage', which I could fix by changing socket.request.res || {} to {}. Not sure if it was mandatory or even needed as I can't test it.
  • Argument of type 'IncomingMessage' is not assignable to parameter of type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>', which I could not fix at all.
  1. I tried using a library from NPM like so:
import sharedSession from 'express-socket.io-session';

// same code...

io.use(sharedSession(session, { autoSave: true }));

// same code...

No errors, until I try getting the value of socket.handshake.session.username (see docs for usage), which, of course, raises an error: Property 'session' does not exist on type 'Handshake'.

@el-zorab el-zorab added the bug Something isn't working label Apr 19, 2021
@el-zorab
Copy link
Author

I found a workaround for this issue which does not require any external library.

Configuring

Configure the socket.io server like so:

const session = ... // define your express session
const io = ... // define your socket.io server
const wrapper = (middleware: any) => (socket: Socket, next: any) => middleware(socket.request, {}, next);
io.use(wrapper(session));

I keep my TypeScript settings really tightened so I needed that wrapper function to make TypeScript shut up for once.
It really depends on your tsconfig and this might work too for others:

io.use((socket: Socket, next: any) => session(socket.request, {}, next));

If it doesn't, just stick with the first option.

The following needs to be added to types.ts; modify the names of the interfaces as you like, EXCEPT SessionData, KEEP IT THE SAME:

import type { IncomingMessage } from 'http';
import type { SessionData } from 'express-session';
import type { Socket } from 'socket.io';

declare module 'express-session' {
    interface SessionData {
        username: string
        // ... the rest of the variables you intent to store in the session object
    }
};

interface SessionIncomingMessage extends IncomingMessage {
    session: SessionData
};

export interface SessionSocket extends Socket {
    request: SessionIncomingMessage
};

Usage

import type { SessionSocket } from './types';

function doSomethingWithSocket(socket: SessionSocket) {
    console.log('username is: ' + socket.request.session.username);
};

io.on('connection', (defaultSocket: Socket) => {
    const socket = <SessionSocket> defaultSocket;
    const username = socket.request.session.username; // retrieve any variable from the session
    doSomethingWithSocket(socket);
});

@barroudjo
Copy link

Actually there is a simpler solution (maybe not cleaner depending on your point of view):
Add .d.ts file somewhere in your project where you know it will be loaded (if necessary add its location in the typeRoots property of your tsconfig.json) and write in there:

declare module 'socket.io' {
  interface Socket {
    request: SessionIncomingMessage
  }
}

@el-zorab
Copy link
Author

el-zorab commented May 4, 2021

Did you test this? TypeScript doesn't seem to like it. Subsequent property declarations must have the same type. Property 'request' must be of type 'IncomingMessage', but here has type 'SessionIncomingMessage'.

I am still forced to use the method which I have shown above. Thanks for sharing anyways, maybe it'll help someone else!

@barroudjo
Copy link

Damnit you're right ! I didn't test this because I wrote this from an example where I was not changing a property of Socket, but adding one, which declaration merging allows. However, in your case you are changing the type of an existing property on Socket, and declaration merging forbids this...
I'm trying hard to come up with a workaround, but there just might not be one...

@barroudjo
Copy link

OK just to say that there is a workaround in your case, but know that it is a little bit filthy...

What you can do is augment the IncomingMessage interface from the http module:

declare module 'node:http' {
  interface IncomingMessage {
    session: string
  }
}

Since socket.io uses that definition, you will have session on it. But also every type definition that uses IncomingMessage from the http module will have it, which is maybe not something you want, or maybe it is.

@el-zorab
Copy link
Author

There is one slight (or major) issue to my and probably your solution too.

Any property which is updated using socket.request.session.property = ... will not get updated on the express req.session object. However, any updated property (even after the socket connected) on the express session object will get reflected over the socket session object.

I cannot wrap my head around it and I really do not know what is the cause of it, nor how to fix it.

@barroudjo do you have any idea on how to fix this or what could be the problem of it, so I can help to find a solution?

@el-zorab el-zorab reopened this May 16, 2021
@barroudjo
Copy link

barroudjo commented May 16, 2021

What you're describing here is a JavaScript problem, not a typescript one. So the solution must lie in looking at how the session is implemented on the socket vs how it is implemented in express. Not my expertise, and I don't have the time to delve into this, sorry !

@Rc85
Copy link

Rc85 commented May 18, 2021

Did you find a solution to this? I am having the issue with typing when passing socket.request into my session middleware.

Argument of type 'IncomingMessage' is not assignable to parameter of type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'

That's not a big problem. The problem here is that the session object in socket.request is not updated when declaring properties in req.session. I declare req.session.user with the logged in user and now I don't have access to that in my socket.

@ohdoski
Copy link

ohdoski commented Jun 16, 2021

Is there any progress?

@lxwang42
Copy link

it seems both 3.X and 4.X versions have this issue, downgrade to 2.X everything goes smoothly.

it is bypassed in another thread using a hack.
#3890

@SCdF
Copy link

SCdF commented Aug 13, 2021

@lxwang42 your link is to this ticket, the one you're commenting on.

While I'm here, FWIW I just @ ts-ignore'd the lines where I pull things out of the session, which so far is only in 2-3 places. It's a bit annoying, I'm not sure @Rc85 wasn't able to actually add stuff to their session

@lxwang42
Copy link

Oh yes, you’re right! My bad, sorry for hijacking the ticket

@1111mp
Copy link

1111mp commented Aug 20, 2021

This is my solution:
index.d.ts:

// Must add this line of code
import { Socket } from "socket.io";

declare module "http" {
  interface IncomingHttpHeaders {
    token: string;
    userid: string;
  }
}

declare module "socket.io" {
  interface Socket {
    decoded: {
      userId: number;
    };
  }
}

test.ts:

import { Server, Socket } from "socket.io";

// ...

private auth_middleware = async (
    socket: Socket,
    next: (err?: ExtendedError) => void
  ) => {
    const { token, userid: userId } = socket.handshake.headers;

    if (!token || !userId) return next(new Error("Authentication error"));

    const auth_key = `${process.env.USER_AUTH_KEY}::${userId}`;
    const real_token = await this.redis.instance.hget(auth_key, token);

    if (!real_token) return next(new Error("Authentication error"));

    try {
      const { id } = verify(
        real_token,
        process.env.SECRET_Key!
      ) as UserAttributes;

      socket.decoded = { userId: id };

      return next();
    } catch (err) {
      return next(new Error("Authentication error"));
    }
  };

tsconfig.json add "typeRoots": ["./@types", "./node_modules/@types"]
launch.json for vscode:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "runtimeArgs": ["-r", "@noshot/env", "-r", "ts-node/register"],
      "env": {
        // https://github.com/TypeStrong/ts-node#help-my-types-are-missing
        "TS_NODE_FILES": "true",
        "TS_NODE_PROJECT": "tsconfig.json",
        "NODE_ENV": "development",
        "LOAD_CONFIG": "dev"
      },
      "program": "${workspaceFolder}/run/index.ts",
      "resolveSourceMapLocations": ["!**/node_modules/**", "!node:internal/**"]
    }
  ]
}

Please read module augmentation as described

darrachequesne added a commit to socketio/socket.io-website that referenced this issue Apr 6, 2022
@darrachequesne
Copy link
Member

For future readers:

I've added an example in the documentation: https://socket.io/how-to/use-with-express-session#with-typescript

Please reopen if needed.

@darrachequesne darrachequesne added documentation Improvements or additions to documentation and removed bug Something isn't working labels Apr 6, 2022
@goldjee
Copy link

goldjee commented Jan 23, 2023

Hello, @darrachequesne ,

I have tried to follow that section of documentation and discovered that Node throws build error:
node_modules/@types/express-serve-static-core/index.d.ts:377:18 - error TS2320: Interface 'Request<P, ResBody, ReqBody, ReqQuery, Locals>' cannot simultaneously extend types 'IncomingMessage' and 'Request'. Named property 'session' of types 'IncomingMessage' and 'Request' are not identical.

My setup:

"@types/express": "^4.17.15",
"@types/express-session": "^1.17.5",
"express": "^4.18.2",
"express-session": "^1.17.3",
"socket.io": "^4.5.4",

I have checked conflicted type declarations and noticed that @types/express-session extends Request type declaration like so (comments omitted):

declare global {
    namespace Express {
        interface Request {
            session: session.Session & Partial<session.SessionData>;
            sessionID: string;
            sessionStore: SessionStore;
        }
    }
}

After adding missing type intersection for session property, the project started to build properly. So I suggest to update the documentation with

declare module 'http' {
    interface IncomingMessage {
        cookieHolder?: string;
        session: Session & Partial<session.SessionData>;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

9 participants