Security News
Fluent Assertions Faces Backlash After Abandoning Open Source Licensing
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.
nestjs-cls
Advanced tools
A continuation-local storage module compatible with NestJS's dependency injection.
The 'nestjs-cls' package provides a way to manage context-local storage in NestJS applications. It allows you to store and retrieve data that is scoped to the current request, making it useful for tasks like logging, tracing, and managing user sessions.
Context Management
This feature allows you to set and get values within the context of a request. The 'ClsService' is used to manage context-local storage, making it easy to store and retrieve data that is specific to the current request.
const { ClsService } = require('nestjs-cls');
@Injectable()
export class MyService {
constructor(private readonly cls: ClsService) {}
doSomething() {
this.cls.set('key', 'value');
const value = this.cls.get('key');
console.log(value); // Outputs: 'value'
}
}
Middleware Integration
This feature demonstrates how to integrate 'ClsMiddleware' into your NestJS application. By applying this middleware, you ensure that context-local storage is available for all routes, making it easy to manage request-specific data throughout your application.
const { ClsMiddleware } = require('nestjs-cls');
@Module({
providers: [ClsMiddleware],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ClsMiddleware)
.forRoutes('*');
}
}
Async Context Management
This feature shows how to manage context-local storage in asynchronous operations. The 'run' method of 'ClsService' ensures that the context is preserved across asynchronous boundaries, allowing you to set and get values within async functions.
const { ClsService } = require('nestjs-cls');
@Injectable()
export class MyService {
constructor(private readonly cls: ClsService) {}
async doSomethingAsync() {
await this.cls.run(async () => {
this.cls.set('key', 'value');
const value = this.cls.get('key');
console.log(value); // Outputs: 'value'
});
}
}
The 'cls-hooked' package provides a way to manage context-local storage using Node.js async_hooks. It is a lower-level library compared to 'nestjs-cls' and requires more manual setup, but it offers similar functionality for managing request-specific data.
The 'async-local-storage' package is another library for managing context-local storage in Node.js applications. It provides a simple API for storing and retrieving data that is scoped to the current request, similar to 'nestjs-cls', but without the NestJS-specific integrations.
The 'continuation-local-storage' package offers context-local storage using the continuation-local-storage API. It is an older library and has been largely replaced by 'cls-hooked', but it still provides similar functionality for managing request-specific data.
A continuation-local storage module compatible with NestJS's dependency injection.
Continuous-local storage allows to store state and propagate it throughout callbacks and promise chains. It allows storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages.
Note: For versions < 1.2, this package used cls-hooked as a peer dependency, now it uses AsyncLocalStorage from Node's
async_hooks
directly. The API stays the same for now but I'll consider making it more friendly for version 2.
npm install nestjs-cls
# or
yarn add nestjs-cls
Note: This module requires additional peer deps, like the nestjs core and common libraries, but it is assumed those are already installed.
Below is an example of storing the client's IP address in an interceptor and retrieving it in a service without explicitly passing it along.
Note: This example assumes you are using HTTP and therefore can use middleware. For usage with non-HTTP transports, keep reading.
// app.module.ts
@Module({
imports: [
// Register the ClsModule and automatically mount the ClsMiddleware
ClsModule.register({
global: true,
middleware: { mount: true }
}),
],
providers: [AppService],
controllers: [AppController],
})
export class TestHttpApp {}
/* user-ip.interceptor.ts */
@Injectable()
export class UserIpInterceptor implements NestInterceptor {
constructor(
// Inject the ClsService into the interceptor to get
// access to the current shared cls context.
private readonly cls: ClsService
)
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Extract the client's ip address from the request...
const request = context.switchToHttp().getRequest();
cosnt userIp = req.connection.remoteAddress;
// ...and store it to the cls context.
this.cls.set('ip', userIp);
return next.handle();
}
}
/* app.controller.ts */
// By mounting the interceptor on the controller, it gets access
// to the same shared cls context that the ClsMiddleware set up.
@UseInterceptors(UserIpInterceptor)
@Injectable()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/hello')
hello() {
return this.appService.sayHello();
}
}
/* app.service.ts */
@Injectable()
export class AppService {
constructor(
// Inject ClsService to be able to retireve data from the cls context.
private readonly cls: ClsService
) {}
sayHello() {
// Here we can extract the value of 'ip' that was
// put into the cls context in the interceptor.
return 'Hello ' + this.cls.get<string>('ip') + '!';
}
}
Continuation-local storage provides a common space for storing and retrieving data throughout the life of a function/callback call chain. In NestJS, this allows for sharing request data across the lifetime of a single request - without the need for request-scoped providers. It also makes it easy to track and log request ids throughout the whole application.
To make CLS work, it is required to set up the CLS context first. This is done by calling cls.run()
(or cls.enter()
) somewhere in the app. Once that is set up, anything that is called within the same callback chain has access to the same storage with cls.set()
and cls.get()
.
Since in NestJS, HTTP middleware is the first thing to run when a request arrives, it is an ideal place to initialise the cls context. This package provides ClsMidmidleware
that can be mounted to all (or selected) routes inside which the context is set up before the next()
call.
All you have to do is mount it to routes in which you want to use CLS, or pass middleware: { mount: true }
to the ClsModule.register
options which automatically mounts it to all routes.
Once that is set up, the ClsService
will have access to a common storage in all Guards, Interceptors, Pipes, Controllers, Services and Exception Filters that are called within that route.
Sometimes, you might want to only use CLS on certain routes. In that case, you can bind the ClsMiddleware manually in the module:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
apply(ClsMiddleware).forRoutes(AppController);
}
}
Sometimes, however, that won't be enough, because the middleware could be mounted too late and you won't be able to use it in other middlewares (as is the case of GQL resolvers). In that case, you can mount it directly in the bootstrap method:
function bootstrap() {
const app = await NestFactory.create(AppModule);
// create and mount the middleware manually here
app.use(
new ClsMiddleware({
/* useEnterWith: true */
}).use,
);
await app.listen(3000);
}
Please note: If you bind the middleware using
app.use()
, it will not respect middleware settings passed toClsModule.forRoot()
, so you will have to provide them yourself in the constructor.
For all other transports that don't use middleware, this package provides a ClsGuard
to set up the CLS context. While it is not a "guard" per-se, it's the second best place to set up the CLS context, since it would be too late to do it in an interceptor.
To use it, pass its configuration to the guard
property to the ClsModule.register
options:
ClsModule.register({
guard: { generateId: true, mount: true }
}),
If you need any other guards to use the ClsService
, it's preferable mount ClsGuard
manually as the first guard in the root module:
@Module({
//...
providers: [
{
provide: APP_GUARD,
useClass: ClsGuard,
},
],
})
export class AppModule {}
Please note: using the
ClsGuard
comes with some security considerations!
Note: A guard might not be the best place to initiate the CLS context for all transports. I'm looking into providing alternative options for specific platforms.
The injectable ClsService
provides the following API to manipulate the cls context:
set
<T>(key: string, value: T): T
get
<T>(key: string): T
getId
(): string;
cls.get(CLS_ID)
)getStore
(): any
enter
(): void;
run
(callback: () => T): T;
isActive
(): boolean
The ClsModule.register()
method takes the following options:
ClsModuleOptions
namespaceName
: string
global:
boolean
(default false
)ClsModule.forFeature()
in other modules.middleware:
ClsMiddlewareOptions
guard:
ClsGuardOptions
The ClsMiddleware
takes the following options (either set up in ClsModuleOptions
or directly when instantiating it manually):
ClsMiddlewareOptions
mount
: boolean
(default false
)generateId
: bolean
(default false
)idGenerator
: (req: Request) => string | Promise<string>
Request
object as an argument and (synchronously or asynchronously) returns a string. The default implementation uses Math.random()
to generate a string of 8 characters.setup
: (cls: ClsService, req: Request) => void | Promise<void>;
saveReq
: boolean
(default true
)CLS_REQ
key.saveRes
: boolean
(default false
)CLS_RES
keyuseEnterWith
: boolean
(default false
)true
to set up the context using a call to AsyncLocalStorage#enterWith
instead of wrapping the next()
call with the safer AsyncLocalStorage#run
. Most of the time this should not be necessary, but some frameworks are known to lose the context with run
.The ClsGuard
takes the following options:
ClsGuardOptions
mount
: boolean
(default false
)generateId
: bolean
(default false
)idGenerator
: (context: ExecutionContext) => string | Promise<string>
ExecutionContext
object as an argument and (synchronously or asynchronously) returns a string. The default implementation uses Math.random()
to generate a string of 8 characters.setup
: (cls: ClsService, context: ExecutionContext) => void | Promise<void>;
Because of a shared storage, CLS is an ideal tool for tracking request (correlation) IDs for the purpose of logging. This package provides an option to automatically generate request IDs in the middleware/guard, if you pass { generateId: true }
to the middleware/guard options. By default, the generated ID is a string based on Math.random()
, but you can provide a custom function in the idGenerator
option.
This function receives the Request
(or ExecutionContext
in case a ClsGuard
is used) as the first parameter, which can be used in the generation process and should return a string ID that will be stored in the CLS for later use.
Below is an example of retrieving the request ID from the request header with a fallback to an autogenerated one.
ClsModule.register({
middleware: {
mount: true,
generateId: true
idGenerator: (req: Request) =>
req.headers['X-Request-Id'] ?? uuid();
}
})
The ID is stored under the CLS_ID
constant in the context. ClsService
provides a shorthand method getId
to quickly retrieve it anywhere. It can be for example used in a custom logger:
// my.logger.ts
@Injectable()
class MyLogger {
constructor(private readonly cls: ClsService) {}
log(message: string) {
console.log(`<${this.cls.getId()}> ${message}`);
}
// [...]
}
// my.service.ts
@Injectable()
class MyService {
constructor(private readonly logger: MyLogger);
hello() {
this.logger.log('Hello');
// -> logs for ex.: "<44c2d8ff-49a6-4244-869f-75a2df11517a> Hello"
}
}
The CLS middleware/guard provide some default functionality, but sometimes you might want to store more things in the context by default. This can be of course done in a custom enhancer bound after, but for this scenario the ClsMiddleware/ClsGuard
options expose the setup
function, which will be executed in the middleware/guard after the CLS context is set up.
The function receives the ClsService
and the Request
(or ExecutionContext
) object, and can be asynchronous.
ClsModule.register({
middleware: {
mount: true,
setup: (cls, req) => {
// put some additional default info in the CLS
cls.set('AUTH', { authenticated: false });
},
},
});
While this package aims to be compatible with NestJS's DI, it is also possible to access the CLS context outside of it. For that, it provides the static ClsServiceManager
class that exposes the getClsService()
method.
function helper() {
const cls = ClsServiceManager.getClsService();
// you now have access to the shared storage
console.log(cls.getId());
}
Please note: Only use this feature where absolutely necessary. Using this technique instead of dependency injection will make it difficult to mock the ClsService and your code will become harder to test.
It is often discussed whether AsyncLocalStorage
is safe to use for concurrent requests (because of a possible context leak) and whether the context could be lost throughout the duration of a request.
The ClsMiddleware
by default uses the safe run()
method, so it should not leak context, however, that only works for REST Controllers
.
GraphQL Resolvers
, cause the context to be lost and therefore require using the less safe enterWith()
method. The same applies to using ClsGuard
to set up the context, since there's no callback to wrap with the run()
call (so the context would be not available outside of the guard otherwise).
This has one consequence that should be taken into account:
When the
enterWith
method is used, any consequent requests get access to the context of the previous one until the request hits theenterWith
call.
That means, when using ClsMiddleware
with the useEnterWith
option, or ClsGuard
to set up context, be sure to mount them as early in the request lifetime as possible and do not use any other enhancers that rely on ClsService
before them. For ClsGuard
, that means you should probably manually mount it in AppModule
if you require any other guard to run after it.
This package is 100% compatible with Nest-supported REST controllers when you use the ClsMiddleware
with the mount
option.
For GraphQL, the ClsMiddleware
needs to be mounted manually with app.use(...)
in order to correctly set up the context for resolvers. Additionally, you have to pass useEnterWith: true
to the ClsMiddleware
options, because the context gets lost otherwise.
Use the ClsGuard
to set up context with any other platform. This is still experimental, as there are no test and I can't guarantee it will work with your platform of choice.
Warning: Namespace support is currently experimental and has no tests. While the API is mostly stable now, it can still change any time.
The default CLS namespace that the ClsService
provides should be enough for most application, but should you need it, this package provides a way to use multiple CLS namespaces in order to be fully compatible with cls-hooked
.
Note: Since cls-hooked was ditched in version 1.2, it is no longer necessary to strive for compatibility with it. Still, the namespace support was there and there's no reason to remove it.
To use custom namespace provider, use ClsModule.forFeature('my-namespace')
.
@Module({
imports: [ClsModule.forFeature('hello-namespace')],
providers: [HelloService],
controllers: [HelloController],
})
export class HelloModule {}
This creates a namespaces ClsService
provider that you can inject using @InjectCls
@Injectable()
class HelloService {
constructor(
@InjectCls('hello-namespace')
private readonly myCls: ClsService,
) {}
sayHello() {
return this.myCls.get('hi');
}
}
Note:
@InjectCls('x')
is equivalent to@Inject(getNamespaceToken('x'))
. If you don't pass an argument to@InjectCls()
, the default ClsService will be injected and is equivalent to omitting the decorator altogether.
@Injectable()
export class HelloController {
constructor(
@InjectCls('hello-namespace')
private readonly myCls: ClsService,
private readonly helloService: HelloService,
);
@Get('/hello')
hello2() {
// seting up cls context manually
return this.myCls.run(() => {
this.myCls.set('hi', 'Hello');
return this.helloService.sayHello();
});
}
}
FAQs
A continuation-local storage module compatible with NestJS's dependency injection.
The npm package nestjs-cls receives a total of 196,388 weekly downloads. As such, nestjs-cls popularity was classified as popular.
We found that nestjs-cls demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.
Research
Security News
Socket researchers uncover the risks of a malicious Python package targeting Discord developers.
Security News
The UK is proposing a bold ban on ransomware payments by public entities to disrupt cybercrime, protect critical services, and lead global cybersecurity efforts.