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

  1. Fix or replace the ISecretWriter implementation so StageAsync always returns a new staged reference distinct from the live binding
  2. Ensure the value passed to StageAsync is the new user-supplied value (request.Value), not the stored value read back from the connection
  3. If your writer intentionally dedupes identical values, change the flow to short-circuit before staging instead of returning the live reference
  4. 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

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


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)