Skip to content

Commit 1eb80bc

Browse files
Papoochclaude
andcommitted
docs: add caveats for proxy provider bootstrap timing and REQUEST-scoped mixing
- Warn that proxy providers are unavailable until onApplicationBootstrap (BullMQ and similar workers must not consume in onModuleInit) - Danger block: never inject Scope.REQUEST providers into proxy provider factories; shows before/after with CLS_REQ pattern Closes #404, relates to #375 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 068cd16 commit 1eb80bc

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

docs/docs/03_features-and-use-cases/04_usage-outside-of-web-request.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,9 @@ export class CronController {
4343
Special care must be taken in case you're using [Proxy Providers](../03_features-and-use-cases/06_proxy-providers.md#outside-web-request).
4444

4545
:::
46+
47+
:::warning
48+
49+
If you are using Proxy Providers in a background worker (e.g. BullMQ), make sure the worker does not start consuming jobs in `onModuleInit`. Proxy Providers are only available after `onApplicationBootstrap` completes. See the [relevant caveat](./06_proxy-providers.md#proxy-providers-require-full-application-bootstrap) in the Proxy Providers documentation.
50+
51+
:::

docs/docs/03_features-and-use-cases/06_proxy-providers.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,69 @@ ClsModule.forFeatureAsync({
310310

311311
## Caveats
312312

313+
### Proxy Providers require full application bootstrap
314+
315+
Proxy providers (and plugins) are registered during the `onApplicationBootstrap` lifecycle hook. Any code that attempts to access a Proxy Provider _before_ this hook completes will encounter an unresolved proxy and may throw a `ProxyProviderNotResolvedException`.
316+
317+
It goes without saying that any access to any property on a Proxy provider **in the constructor** will always evaluate to `undefined` (or throw in strict mode).
318+
319+
:::warning
320+
321+
This is a common pitfall with background job processors such as **BullMQ workers**, which start consuming messages in `onModuleInit` — before `onApplicationBootstrap` has run. Accessing a Proxy Provider from within a job handler at that point will fail.
322+
323+
To avoid this, ensure the application is fully bootstrapped before your worker begins consuming. Either start consuming in `onApplicationBootstrap` instead of `onModuleInit`, or delay consumption until after `app.listen()` / `app.init()` resolves:
324+
325+
```ts
326+
@Injectable()
327+
export class WorkerService implements OnApplicationBootstrap {
328+
constructor(private readonly worker: Worker) {}
329+
330+
// Use onApplicationBootstrap instead of onModuleInit
331+
// to ensure Proxy Providers are available.
332+
onApplicationBootstrap() {
333+
this.worker.run();
334+
}
335+
}
336+
```
337+
338+
:::
339+
340+
### Do not mix REQUEST-scoped providers with Proxy Providers
341+
342+
:::danger
343+
344+
Never inject a real NestJS `Scope.REQUEST` (or `durable: true`) provider as a dependency of a Proxy Provider or any `ClsModule` plugin.
345+
346+
:::
347+
348+
Proxy Providers are **singletons** from NestJS's DI perspective. When NestJS detects that a singleton depends on a REQUEST-scoped provider, it changes the scope of the singleton to REQUEST as well. This means the Proxy wrapper itself is re-created on every request, which defeats the purpose of Proxy Providers and can cause **cross-request contamination** (e.g. tenant connections leaking between requests).
349+
350+
```ts
351+
// ❌ Wrong: TENANT_CONNECTION depends on a real Scope.REQUEST provider
352+
{
353+
provide: TENANT_CONNECTION,
354+
scope: Scope.REQUEST,
355+
durable: true,
356+
inject: [REQUEST, TenantRegistry],
357+
useFactory: (req: Request, registry: TenantRegistry) =>
358+
registry.getConnection(req.headers['tenant-id']),
359+
}
360+
```
361+
362+
Convert it to a `ClsModule.forFeatureAsync` Proxy Provider using `CLS_REQ` or `ClsService` instead:
363+
364+
```ts
365+
// ✅ Correct: factory is a singleton; request data comes from CLS context
366+
ClsModule.forFeatureAsync({
367+
provide: TENANT_CONNECTION,
368+
inject: [CLS_REQ, TenantRegistry],
369+
useFactory: (req: Request, registry: TenantRegistry) =>
370+
registry.getConnection(req.headers['tenant-id']),
371+
});
372+
```
373+
374+
`CLS_REQ` is itself a Proxy Provider (a singleton that delegates to the per-request value stored in CLS), so the factory above remains a singleton from NestJS's point of view while still resolving the correct request on each access.
375+
313376
### No primitive values
314377

315378
Proxy Factory providers _cannot_ return a _primitive value_ (`string`, `number`, `boolean`, `null`, or `undefined`). Doing so throws a `ProxyProviderInvalidReturnTypeException` at resolution time. This is because the provider itself is the Proxy and it only delegates access once a property or a method is called on it (or if it itself is called in case the factory returns a function).

0 commit comments

Comments
 (0)