Skip to content

fix(core): isolate nested transient instances in static context#16362

Open
veeceey wants to merge 2 commits intonestjs:masterfrom
veeceey:fix/issue-16257-transient-isolation-static-context
Open

fix(core): isolate nested transient instances in static context#16362
veeceey wants to merge 2 commits intonestjs:masterfrom
veeceey:fix/issue-16257-transient-isolation-static-context

Conversation

@veeceey
Copy link

@veeceey veeceey commented Feb 14, 2026

Description

Fixes #16257

When multiple DEFAULT-scoped providers inject the same TRANSIENT -> TRANSIENT chain, the deeply nested transient instances are incorrectly shared. For example:

@Injectable()
class ServiceA {
  constructor(public readonly logger: TransientLogger) {}
}

@Injectable()
class ServiceB {
  constructor(public readonly logger: TransientLogger) {}
}

@Injectable({ scope: Scope.TRANSIENT })
class TransientLogger {
  constructor(public readonly nested: NestedTransient) {}
}

@Injectable({ scope: Scope.TRANSIENT })
class NestedTransient {}

In this scenario, serviceA.logger.nested and serviceB.logger.nested point to the same NestedTransient instance, even though both TransientLogger instances are correctly different.

Root cause

The transient map uses the inquirer's wrapper ID as the key. In STATIC context, getEffectiveInquirer() has a contextId !== STATIC_CONTEXT guard that prevents it from returning the parent inquirer for nested TRANSIENT chains. This means the intermediate TransientLogger wrapper's ID is used as the key regardless of which DEFAULT parent (ServiceA vs ServiceB) initiated the chain — so the nested instance gets shared.

Fix

Introduces getEffectiveInquirerId() which returns a composite key (parentInquirer.id:inquirer.id) for nested TRANSIENT chains in STATIC context. This ensures each resolution path through a different DEFAULT parent produces its own isolated nested instance, without affecting the existing behavior for non-nested cases or REQUEST-scoped contexts.

Also updates TestingInjector.resolveComponentHost() to forward the new inquirerIdOverride parameter.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the CONTRIBUTING document
  • My commit message follows the conventional commits format
  • New unit tests added for nested transient isolation scenarios
  • New integration tests added for DEFAULT -> TRANSIENT -> TRANSIENT chains
  • All existing tests pass (1990 unit tests, all transient scope integration tests)

When multiple DEFAULT-scoped providers inject the same TRANSIENT chain
(e.g., ServiceA -> TransientLogger -> NestedTransient and ServiceB ->
TransientLogger -> NestedTransient), the nested transient instances were
incorrectly shared because the transient map key (inquirerId) is per-class,
not per-instance. In STATIC context, getEffectiveInquirer returned the
intermediate TRANSIENT wrapper's id, which is the same regardless of
which DEFAULT parent initiated the chain.

This fix introduces a composite key (parentInquirer.id:inquirer.id) for
nested TRANSIENT chains in STATIC context via getEffectiveInquirerId().
The composite key differentiates resolution paths so each DEFAULT parent
gets its own isolated nested transient instances.

Also updates TestingInjector.resolveComponentHost() to forward the new
inquirerIdOverride parameter to the parent class.

Closes nestjs#16257
@coveralls
Copy link

coveralls commented Feb 14, 2026

Pull Request Test Coverage Report for Build 4594ff5f-f403-4927-91ad-e3405f57f30c

Details

  • 10 of 10 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall first build on fix/issue-16257-transient-isolation-static-context at 89.836%

Totals Coverage Status
Change from base Build 35a249bb-b90d-4d09-aaec-feede0cafbd4: 89.8%
Covered Lines: 7469
Relevant Lines: 8314

💛 - Coveralls

@veeceey
Copy link
Author

veeceey commented Mar 15, 2026

friendly ping - any chance someone could take a look at this? the transient scope isolation issue is kind of subtle but it bites people

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested TRANSIENT providers not isolated in STATIC context when multiple DEFAULT parents inject same chain

2 participants