Skip to content

ServiceClient returns HTML login page instead of auth error when cached token expires in long-running service #536

@amahalams

Description

@amahalams

Summary

When a ServiceClient instance is cached and reused in a long-running ASP.NET Core service, calls to RetrieveAsync can fail with a ProtocolException because the Dataverse endpoint returns an HTML sign-in page (text/html) instead of the expected SOAP/XML response (text/xml).

No clean authentication exception is thrown (e.g., ExpiredSecurityTokenException, SecurityTokenValidationException, or MessageSecurityException), making it impossible for callers to distinguish an auth failure from a protocol error without inspecting the exception message for HTML content.

Environment

  • Microsoft.PowerPlatform.Dataverse.Client version: 1.1.22 (also reviewed changelog through 1.2.10 no related fix noted)
  • Runtime: .NET 9.0 / ASP.NET Core on Linux containers (Kubernetes)
  • Authentication: S2S (certificate-based, via MSAL)

Steps to Reproduce

  1. Create a ServiceClient with valid S2S credentials (certificate or client secret)
  2. Cache the ServiceClient instance for reuse across requests (standard pattern for multi-tenant long-running services)
  3. Wait for the internal MSAL token to expire (typically 60-90 minutes, depending on AAD policy)
  4. Call ServiceClient.RetrieveAsync() on the cached instance
  5. The call fails no token refresh occurs before the SOAP request is sent

Expected Behavior

The SDK should either:

a) Transparently refresh the token before making the SOAP call. MSAL's in-memory token cache should handle silent token renewal via AcquireTokenSilent, but this does not appear to happen when the ServiceClient is cached and reused across a long time window.

b) Throw a specific, documented authentication exception so callers can detect the auth failure and take corrective action (e.g., evict the cached client and create a fresh one). The appropriate exceptions would be:

  • ExpiredSecurityTokenException
  • SecurityTokenValidationException
  • MessageSecurityException

These exceptions are already listed in the official documentation as exceptions that callers should handle:
https://learn.microsoft.com/en-us/power-apps/developer/data-platform/org-service/handle-exceptions-code

Actual Behavior

A ProtocolException is thrown from System.Private.ServiceModel:

The content type text/html; charset=utf-8 of the response message does not match 
the content type of the binding (text/xml; charset=utf-8). If using a custom encoder, 
be sure that the IsContentTypeSupported method is implemented properly.

The first 1024 bytes of the response contain Microsoft's AAD sign-in page:

<title>Sign in to your account</title>
<meta name="PageID" content="ConvergedSignIn" />

Root Cause Analysis

The failure sequence is:

  1. ServiceClient.RetrieveAsync() sends a SOAP request to the Dataverse Organization Service endpoint with an expired/invalid bearer token
  2. The Dataverse gateway responds with HTTP 302 redirect to login.microsoftonline.com
  3. The WCF HTTP transport layer follows the redirect automatically
  4. The WCF client receives the HTML login page from AAD
  5. The WCF XML parser expects text/xml content but receives text/html
  6. A ProtocolException is thrown the authentication failure is completely masked as a content-type mismatch

Key observations:

  • The SDK's built-in retry logic handles 429 (throttling) and concurrency errors, but does NOT handle auth redirects
  • The WCF channel does not intercept 302 responses to check if they indicate an auth failure
  • No POST to login.microsoftonline.com/.../oauth2/v2.0/token occurs between the cache hit and the Dataverse SOAP call, confirming no token refresh was attempted

Stack Trace (redacted)

at Microsoft.PowerPlatform.Dataverse.Client.ServiceClient.RetrieveAsync(
    String entityName, Guid id, ColumnSet columnSet, CancellationToken cancellationToken)
at [caller code redacted]

Workaround

On the caller side, we detect ProtocolException messages containing text/html and treat them as transient/retryable errors. Before retrying, we evict the cached ServiceClient so a fresh instance is created with new credentials on the next attempt.

Request

If this behavior cannot be fixed in the SDK (due to WCF architectural constraints), we request that it be explicitly documented as a known limitation, including:

  1. A note in the exception handling documentation that ProtocolException with text/html content indicates an authentication redirect, not a true protocol error
  2. Recommended patterns for long-running services that cache ServiceClient instances (e.g., periodic eviction, or wrapping calls with auth-redirect detection)
  3. Guidance on the expected token lifetime and whether ServiceClient is designed to be cached indefinitely or should be periodically recreated

Responsible Team

The ServiceClient is owned by the Microsoft Dataverse SDK team (microsoft/PowerPlatform-DataverseServiceClient). The WCF SOAP transport layer is owned by the .NET WCF team (dotnet/wcf), but the fix should come from the Dataverse SDK since they control the auth flow and retry logic.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions