elsa-workflows/elsa-core · error · InvalidOperationException
A managed secret writer returned the live secret reference…
Error message
A managed secret writer returned the live secret reference instead of a fresh staged reference.
What it means
When updating a connection's secret, the endpoint stages a fresh value via the managed secret writer and then verifies the staged binding does not equal the connection's existing live binding (same ResolverType AND same Reference). If the writer handed back a binding identical to the live one, it failed to create a new staged version, so the update would be a no-op masquerading as a rotation — this InvalidOperationException guards that invariant.
Solutions
- Fix or replace the ISecretWriter implementation so StageAsync always returns a new staged reference distinct from the live binding
- Ensure the value passed to StageAsync is the new user-supplied value (request.Value), not the stored value read back from the connection
- If your writer intentionally dedupes identical values, change the flow to short-circuit before staging instead of returning the live reference
- Use the production managed secret writer rather than a fake/mock that echoes bindings
Example fix
// before: custom writer returns existing binding for unchanged values
public ValueTask<SecretBinding> StageAsync(...) => new(connection.SecretBindings[field]); // wrong
// after
public ValueTask<SecretBinding> StageAsync(StageRequest request, CancellationToken ct)
=> new(new SecretBinding { ResolverType = ResolverType, Reference = CreateStagedReference(request) }); // fresh reference Defensive patterns
Strategy: try-catch
Validate before calling
// before calling the endpoint, ensure the writer stages distinct references
var staged = await writer.StageAsync(stageRequest, ct);
if (connection.SecretBindings.TryGetValue(field, out var live) &&
live.ResolverType == staged.ResolverType && live.Reference == staged.Reference)
throw new InvalidOperationException("Writer returned the live binding; fix the ISecretWriter."); Type guard
bool IsFreshReference(SecretBinding staged, SecretBinding? live) =>
live is null || !string.Equals(live.ResolverType, staged.ResolverType, StringComparison.Ordinal) ||
!string.Equals(live.Reference, staged.Reference, StringComparison.Ordinal); Try / catch
try { await UpdateConnectionSecretAsync(request, ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("live secret reference"))
{ logger.LogError(ex, "Secret writer did not stage a fresh reference for field {Field}.", fieldName); } Prevention
- Unit-test custom ISecretWriter implementations to assert StageAsync always returns a new reference
- Never feed a stored value back into StageAsync; always pass the new user input
- Use integration tests that rotate a connection secret end-to-end
- Keep mocks out of production paths; fakes that echo bindings will trip this invariant
When it happens
Trigger: Posting a secret update to the connection management endpoint where writer.StageAsync returns a binding whose ResolverType and Reference exactly match the connection's existing SecretBindings[fieldName] — i.e. a custom/buggy ISecretWriter that returns the live binding instead of staging a fresh secret version.
Common situations: Custom secret writer implementations that cache and return the existing binding for unchanged values; a writer bug where staging is skipped when the value appears unchanged; test doubles (fakes) for ISecretWriter that echo back the stored binding.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- The secret field name must contain a letter or digit.
- Only managed secret bindings can remove managed secret…
- The configured secret binding could not be resolved.
- The configured secret binding is incompatible with the…
- The configured secret binding is not active.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/c40bb2c218c8f069.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ConnectionManagementEndpoints.cs:388
return;
}
if (effective.Connection.Revision != revision)
{
await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status412PreconditionFailed, "revision_conflict", "The connection has changed; reload it before replacing its secret.", cancellationToken);
return;
}
if (!adapters.TryGet(effective.Connection.AdapterType, out var adapter) || !adapter.Describe().Fields.Any(x => x.IsSecretBinding && string.Equals(x.Name, fieldName, StringComparison.Ordinal)))
{
await ConnectionEndpointSupport.SendErrorAsync(HttpContext, StatusCodes.Status400BadRequest, "undeclared_secret_field", "The adapter does not declare this secret field.", cancellationToken);
return;
}
using var value = new SensitiveString(request.Value);
var stagedBinding = await writer.StageAsync(new(effective.Connection.Id, fieldName, value), cancellationToken);
if (effective.Connection.SecretBindings.TryGetValue(fieldName, out var liveBinding) &&
string.Equals(liveBinding.ResolverType, stagedBinding.ResolverType, StringComparison.Ordinal) &&
string.Equals(liveBinding.Reference, stagedBinding.Reference, StringComparison.Ordinal))
throw new InvalidOperationException("A managed secret writer returned the live secret reference instead of a fresh staged reference.");
var candidate = IdentityProviderConnectionCloner.Clone(effective.Connection);
candidate.SecretBindings[fieldName] = stagedBinding;
ManagementConnectionMutationResult result;
try
{
result = await management.UpdateAsync(candidate.Id, candidate, revision, User, tenantAccessor.TenantId, false, cancellationToken: cancellationToken);
}
catch
{
await CleanupAfterExceptionalFailureAsync();
throw;
}
if (result is not ManagementConnectionMutationResult.Success(var connection))
{
await ManagedSecretBindingCleanup.TryRemoveAsync(writer, stagedBinding, effective.Connection.Id, logger);
await ConnectionEndpointSupport.SendMutationResultAsync(HttpContext, result, management, cancellationToken);
return;
}View on GitHub (pinned to fe9217bdfa)