Closed nileger closed 3 years ago
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 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 https://github.com/nestjs/nest/issues/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.
@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.
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;
}
Problem statement
http
andws
, within one application.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