fullstackhero/dotnet-starter-kit · warning · CustomException
end current impersonation before starting a new one
Error message
end current impersonation before starting a new one
What it means
Impersonation sessions cannot nest. If the caller's claims already contain an ActorSubject claim — meaning they are currently impersonating someone — the handler throws CustomException('end current impersonation before starting a new one', BadRequest).
Solutions
- Call the end-impersonation endpoint first, then start the new session
- Use the original (pre-impersonation) token when starting a new impersonation
- Make the client guard: only show 'start impersonation' when not currently impersonating
Example fix
// before await api.startImpersonation(target); // 400 while impersonating // after if (isImpersonating) await api.endImpersonation(); await api.startImpersonation(target);
Defensive patterns
Strategy: validation
Validate before calling
if (claims.some(c => c.type === 'actor_subject' || c.type === 'ActorSubject')) {
throw new Error('already impersonating; end the current session first');
} Type guard
const isImpersonating = (claims: {type: string}[]) => claims.some(c => c.type.toLowerCase().includes('actorsubject')); Try / catch
try { await api.startImpersonation(req); }
catch (e) { if (e.status === 400 && /end current impersonation/.test(e.message)) { await api.endImpersonation(); return retryOnce(); } throw e; } Prevention
- Track impersonation state in the client and hide 'start' while active
- Always start impersonation from the original token, never the impersonated one
- End the session on logout/token refresh
- In automation, pair every start with an end in cleanup
When it happens
Trigger: Calling start-impersonation while already holding an active impersonation session (ActorSubject claim present in the JWT).
Common situations: A support agent forgets to end the current session and starts another; a SPA holding a stale impersonated token retries the start call; automation loops issuing start without stop.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- current session is not an impersonation session
- cannot impersonate yourself
- A category cannot be its own parent.
- Setting this parent would create a cycle.
- Cannot DM yourself.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/cc2d9a92bb740af3.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs:85
&& !string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))
{
throw new ForbiddenException("cross-tenant impersonation is restricted to platform operators");
}
// Prevent self-impersonation (pointless, confuses the audit trail). Caller error → explicit 4xx,
// not the 500 CustomException defaults to.
if (string.Equals(actorUserId, request.TargetUserId, StringComparison.Ordinal)
&& string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))
{
throw new CustomException("cannot impersonate yourself", errors: null, System.Net.HttpStatusCode.BadRequest);
}
// Prevent nesting: if the caller is already impersonating, require end-impersonation first.
var callerClaims = _currentUser.GetUserClaims();
if (callerClaims is not null
&& callerClaims.Any(c => c.Type == ClaimConstants.ActorSubject))
{
throw new CustomException(
"end current impersonation before starting a new one",
errors: null,
System.Net.HttpStatusCode.BadRequest);
}
var targetClaimsResult = await _identityService
.BuildClaimsForUserAsync(request.TargetUserId, request.TargetTenantId, cancellationToken);
if (targetClaimsResult is null)
{
throw new NotFoundException("target user not found");
}
var (subject, claims) = targetClaimsResult.Value;
var targetUserName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value
?? claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Name)?.Value;
// Strip the auto-generated jti from BuildClaimsForUserAsync and inject our own, so the persistedView on GitHub (pinned to 3f2959e683)