Skip to content

Transparently retry once on 'unsecured or incorrectly secured fault' (WcfSoapWithOpenIdConnect only) #273

Description

@ddemeyer

Problem

Despite the fixes in #201 and #219, a SOAP proxy call over WcfSoapWithOpenIdConnect can still fault on its first use after some as-yet-unconfirmed trigger:

An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail.

The existing recovery mechanism (from #219) only kicks in on the next call to the same channel: WCF flips the faulted channel's CommunicationState, and Get*25Channel()''s null/faulted check rebuilds it - but only when that method is entered again. So the user sees one hard failure, then has to re-run the same cmdlet for it to succeed. Confirmed this is single-channel behavior, not a cross-channel token issue - Find-IshBaseline and Get-IshPublicationOutputContent both go through the same cached Baseline25 channel, and the working call happened first.

Proposal

Wrap each returned channel in a thin retry proxy (System.Reflection.DispatchProxy) that catches CommunicationException/FaultException on the actual call, forces a rebuild via the existing Get*25Channel() rebuild logic, and retries exactly once - so a single cmdlet invocation self-heals instead of requiring a second call. Scope: InfoShareWcfSoapWithOpenIdConnectConnection.cs only (WcfSoapWithWsTrust stays untouched, per precedent in #201/#219).

Before

// InfoShareWcfSoapWithOpenIdConnectConnection.cs
public Application25ServiceReference.Application GetApplication25Channel()
{
    ...
    if (/* null / faulted / closing / closed */)
    {
        ...rebuild _applicationClient and _applicationServiceReference...
    }
    return _applicationServiceReference;
}

After

public Application25ServiceReference.Application GetApplication25Channel()
{
    ...
    if (/* null / faulted / closing / closed */)
    {
        ...rebuild _applicationClient and _applicationServiceReference... // unchanged
    }
    return RetryOnFaultProxy<Application25ServiceReference.Application>.Wrap(
        _applicationServiceReference, GetApplication25Channel);
}

New shared class (single-sourced, added once):

internal sealed class RetryOnFaultProxy<T> : DispatchProxy where T : class
{
    private T _target;
    private Func<T> _rebuild;

    public static T Wrap(T target, Func<T> rebuild)
    {
        var proxy = Create<T, RetryOnFaultProxy<T>>() as RetryOnFaultProxy<T>;
        proxy._target = target;
        proxy._rebuild = rebuild;
        return proxy as T;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        try
        {
            return targetMethod.Invoke(_target, args);
        }
        catch (TargetInvocationException tie) when (tie.InnerException is CommunicationException || tie.InnerException is FaultException)
        {
            _target = _rebuild();          // re-enters the existing, unchanged rebuild logic
            return targetMethod.Invoke(_target, args);   // one retry only, then propagate
        }
    }
}

Repeated for all ~19 Get*25Channel() methods (Annotation, Application, DocumentObj, Folder, User, UserRole, UserGroup, ListOfValues, PublicationOutput, OutputFormat, Settings, EDT, EventMonitor, Baseline, MetadataBinding, Search, TranslationJob, TranslationTemplate, BackgroundTask) - one-line change per method, both #if NET48/#else arms.

Positive side effects

  • No cmdlet files touched; change fully contained in Connection/, matching the layer''s own "keep changes inside this folder" rule.
  • Cmdlet scripts/automation no longer need their own retry-on-transient-fault handling for this specific error - one less thing external callers have to work around.
  • Faulted-channel rebuild logic itself is untouched/not duplicated - the retry proxy just re-enters the existing method.

Risks / open questions

  • New net48-only NuGet dependency: System.Reflection.DispatchProxy (built into BCL on net6.0/net10.0, not referenced yet on net48).
  • Retried call happens transparently - if the underlying fault is not transient (e.g. genuinely invalid credentials, revoked ClientSecret), the cmdlet now takes ~2x as long to fail instead of failing fast. Acceptable trade-off but worth calling out.
  • The proxy wraps only the plain service-contract interface (e.g. Application), not ICommunicationObject/IDisposable - confirmed no code outside Connection/ casts to those on the returned channel, so this is safe, but any future code that tries to cast the returned proxy to ICommunicationObject would break; worth a code comment warning against that.
  • Only handles the synchronous contract methods actually used by cmdlets; the generated *Async() methods on the same interfaces are not exercised by ISHRemote today and are not specifically designed for in this change.
  • One retry masks the underlying "why did this fault in the first place" question - root cause of the original fault remains unconfirmed (see Recover and Refresh IShSession Access Token to avoid FaultException ‘An unsecured or incorrectly secured fault was received from the other party.’ #201/Recover from 'The communication object, ..., cannot be used for communication because it is in the Faulted state.' plus improve debug information #219 history); this issue only makes the symptom invisible to the end user, it doesn''t fix the trigger.
  • Not yet validated against a live server / compiled - feasibility reasoned from reading the generated interfaces and existing casts only.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions