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

Cls support for websockets #8

Closed
nileger opened this issue Oct 30, 2021 · 6 comments
Closed

Cls support for websockets #8

nileger opened this issue Oct 30, 2021 · 6 comments
Assignees
Labels
documentation Improvements or additions to documentation question Further information is requested solved The question was answered or the problem was solved

Comments

@nileger
Copy link

nileger commented Oct 30, 2021

Problem statement

  • Summary: At the moment, there seems to be no way to get cls working for websocket communication. It would be great to have cls support for both, http and ws, within one application.
  • Background: I have a tenant interceptor for my websocket gateway that reads the tenant from the namespace of the websocket object. This information shall be used in the interceptor to do cls.set('tenant', tenant). This information will then be used by the custom logger to add tenant information for each log statement created in websocket context.

Solution

I'm not familiar with this repository, neither with cls nor the different NestJs communication protocols. Therefore, at the moment, I can't provide an ideas. From the perspective of a consumer of nestjs-cls, it would be great to extend the existing functionality in such a way, that cls for websockt context can be added by setting a property or by an additional import in the module that provides the websocket gateway.

Minimum reproduction

@Papooch Papooch self-assigned this Oct 30, 2021
@Papooch Papooch added the question Further information is requested label Oct 30, 2021
@Papooch
Copy link
Owner

Papooch commented Oct 30, 2021

Okay, so I've found the issue.

The problem is that Websocket Gateways don't respect globally bound enhancers (neither with app.useGlobal<Enhancer> nor with the APP_<ENHACER> provider (which is what nestjs-cls uses with the mount: true option)), which kind of makes sense now, since they're bound to the Express or Fastify apps.

To make CLS also work with Websockets, all you have to do is explicitly bind the Cls<Enhancer> to the Websocket Gateway like so:

@WebSocketGateway()
@UseInterceptors(/* interceptor */ ClsInterceptor, WsTenantInterceptor)
export class WebsocketGateway {
   // ...
}

or

@WebSocketGateway()
@UseGuards(/* guard */ ClsGuard)
@UseInterceptors(WsTenantInterceptor)
export class WebsocketGateway {
   // ...
}

(btw, you will still need to configure the ClsGuard or ClsInterceptor in ClsModule.register())

I will update the README to reflect that, but Websockets are currently supported :)

Papooch added a commit that referenced this issue Oct 31, 2021
@Papooch Papooch closed this as completed Nov 2, 2021
@Papooch Papooch added the documentation Improvements or additions to documentation label Nov 2, 2021
@peisenmann
Copy link

@Papooch The docs further don't address that even if you set everything up the way you've described, handleConnection is not covered by Interceptors, Guards, or Middleware.
It's not a NestJS CLS problem, it's just something of a shortcoming in Nest:

It does appear this might be something they support in the future nestjs/nest#882

A simple solution I've found for now is to inject the ClsService into my gateway, then do:

  async handleConnection(socket: Socket, incoming: IncomingMessage) {
    // handleConnection is not covered by NestJS Middleware, Guards, or Interceptors
    this.clsService.run(async () => {
       // Do stuff here
       const authToken = getAuthTokenFromIncomingMessage(incoming);
       this.clsService.set('request.authToken', authToken);
    }
 }

The rest of the incoming messages are covered by the interceptor as you mentioned. It may be obvious, but of course the CLS values won't persist between web socket messages since there's not any asynchronous context left to contain them. So, I've been storing the values I care about in a map I added onto the socket. Then, in the interceptor, I grab them off the socket and copy them into the real cls. My app has a mixture of HTTP and WS, and this lets my app only have to check one place, the cls, for these values which I think keeps it cleaner.

@Papooch
Copy link
Owner

Papooch commented Sep 29, 2022

@peisenmann Thanks for the writeup, this might definitely help someone implementing the same!

As for the need to wrap the call to handleConnection in CLS, that's something that will be addressed in #19 in the future.

@wormen
Copy link

wormen commented Mar 26, 2023

maybe it will help someone, I did it this way, in appmo AppModule I imported it globally

@Module({
	imports: [
		ClsModule.forRoot({
			global: true,
			guard: {
				mount: true,
				generateId: true,
				idGenerator: () => uuidv4(),
				setup: clsSetupHelper
			},
			interceptor: {
				mount: true,
				generateId: true,
				idGenerator: () => uuidv4(),
				setup: clsSetupHelper
			},
			middleware: {
				mount: true,
				generateId: true,
				useEnterWith: true,
				idGenerator: (req: Request) => req.headers['X-Request-Id'] ?? uuidv4(),
				setup: clsSetupHelper
			}
		})
  ]
})
export class AppModule implements NestModule {
	configure(consumer: MiddlewareConsumer): any {
		consumer.apply(ClsMiddleware).forRoutes('*');
	}
}

clsSetupHelper

export const clsSetupHelper = (cls: ClsService, context: ExecutionContext) => {
	try {
		let xUser: Record<string, any> | null = null;

		if (typeof context.getType !== 'function') {
			xUser = context.headers['x-user'] ?? null;
		} else if (context.getType() === 'http') {
			const request = context.switchToHttp().getRequest();
			xUser = request.headers['x-user'] ?? null;
		} else if (context.getType() === 'ws') {
			const request = context.switchToWs().getClient();
			xUser = request.handshake.headers['x-user'] ?? null;
		}

		cls.set('xUser', xUser);
	} catch (e) {
		console.error('clsSetupHelper ==>', e);
	}
};

works everywhere except handleConnection

@WebSocketGateway({
	cors: {
		origin: shareOrigin()
	}
})
@UseGuards(AuthGuard)
@UseInterceptors(ClsInterceptor)
export class AppWebsocketGateway implements OnGatewayConnection, OnGatewayDisconnect
{
	@WebSocketServer()
	server: Server;
}

@Papooch Papooch added the solved The question was answered or the problem was solved label Jul 10, 2023
@arko7n
Copy link

arko7n commented Dec 13, 2024

Facing trouble implementing this. I have a NestJS project that uses websockets. I want to use cls inside the handleConnection method. I am expecting a websocket connection level context to be created, and not message level. I want to share a client context across all services used by the connection and throughout the connection (not recreate it for every message for performance reasons).

Based on @Papooch's comment I was hoping this is fixed:

As for the need to wrap the call to handleConnection in CLS, that's something that will be addressed in #19 in the future.

I tried using this on the app module:

ClsModule.forRoot({
  global: true,
  middleware: { mount: true },
  interceptor: { mount: true },
}),

This on the websocket gateway:

@UseInterceptors(ClsInterceptor)
@WebSocketGateway({
...

And:

@UseCls()
async handleConnection(clientWs: WebSocket) {
  this.cls.run(() => {
  // code that tries to use CLS

But I'm still getting the error:

No CLS context available, please make sure that a ClsMiddleware/Guard/Interceptor has set up the context, or wrap any calls that depend on CLS with "ClsService#run"

@Papooch
Copy link
Owner

Papooch commented Dec 17, 2024

@arko7n Are you getting that error inside of the run callback? Where/when exactly is the error thrown?

It's hard to understand what the problem is based on just these code snippets. Please create a new issue and add as many details as possible, so I can try to assist you.

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 question Further information is requested solved The question was answered or the problem was solved
Projects
None yet
Development

No branches or pull requests

5 participants