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
- Create a
ServiceClient with valid S2S credentials (certificate or client secret)
- Cache the
ServiceClient instance for reuse across requests (standard pattern for multi-tenant long-running services)
- Wait for the internal MSAL token to expire (typically 60-90 minutes, depending on AAD policy)
- Call
ServiceClient.RetrieveAsync() on the cached instance
- 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:
ServiceClient.RetrieveAsync() sends a SOAP request to the Dataverse Organization Service endpoint with an expired/invalid bearer token
- The Dataverse gateway responds with HTTP 302 redirect to
login.microsoftonline.com
- The WCF HTTP transport layer follows the redirect automatically
- The WCF client receives the HTML login page from AAD
- The WCF XML parser expects
text/xml content but receives text/html
- 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:
- A note in the exception handling documentation that
ProtocolException with text/html content indicates an authentication redirect, not a true protocol error
- Recommended patterns for long-running services that cache
ServiceClient instances (e.g., periodic eviction, or wrapping calls with auth-redirect detection)
- 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
Summary
When a
ServiceClientinstance is cached and reused in a long-running ASP.NET Core service, calls toRetrieveAsynccan fail with aProtocolExceptionbecause 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, orMessageSecurityException), 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.Clientversion: 1.1.22 (also reviewed changelog through 1.2.10 no related fix noted)Steps to Reproduce
ServiceClientwith valid S2S credentials (certificate or client secret)ServiceClientinstance for reuse across requests (standard pattern for multi-tenant long-running services)ServiceClient.RetrieveAsync()on the cached instanceExpected 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 theServiceClientis 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:
ExpiredSecurityTokenExceptionSecurityTokenValidationExceptionMessageSecurityExceptionThese 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
ProtocolExceptionis thrown fromSystem.Private.ServiceModel:The first 1024 bytes of the response contain Microsoft's AAD sign-in page:
Root Cause Analysis
The failure sequence is:
ServiceClient.RetrieveAsync()sends a SOAP request to the Dataverse Organization Service endpoint with an expired/invalid bearer tokenlogin.microsoftonline.comtext/xmlcontent but receivestext/htmlProtocolExceptionis thrown the authentication failure is completely masked as a content-type mismatchKey observations:
POSTtologin.microsoftonline.com/.../oauth2/v2.0/tokenoccurs between the cache hit and the Dataverse SOAP call, confirming no token refresh was attemptedStack Trace (redacted)
Workaround
On the caller side, we detect
ProtocolExceptionmessages containingtext/htmland treat them as transient/retryable errors. Before retrying, we evict the cachedServiceClientso 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:
ProtocolExceptionwithtext/htmlcontent indicates an authentication redirect, not a true protocol errorServiceClientinstances (e.g., periodic eviction, or wrapping calls with auth-redirect detection)ServiceClientis designed to be cached indefinitely or should be periodically recreatedResponsible Team
The
ServiceClientis 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
ExpiredSecurityTokenExceptionbut the SDK doesn't throw it in this scenario