Context
More customers run Tridion Docs behind containerized/managed infrastructure on AWS, Azure or GCP, increasingly fronted by a WAF. All three clouds ship managed bot-detection rule groups that inspect User-Agent on every request: AWS WAF Bot Control, Azure Front Door/App Gateway WAF bot manager rules, GCP Cloud Armor adaptive protection/bot management. HttpClient sends no User-Agent unless set explicitly, and ISHRemote never has. No complaints yet, but it's a matter of time as more customers enable these rule groups — cheap to fix now against a live repro, expensive as a reactive support incident later.
Measured against a live environment (ish.example.com/health, AWS WAF fronted):
User-Agent sent |
Result |
| (none) |
200 OK |
ISHRemote/8.3.0 (bare custom token) |
403 Forbidden |
curl/8.0.1, PostmanRuntime/7.36.0, python-requests/2.31.0 |
403 Forbidden |
Mozilla/5.0 (compatible; ISHRemote/8.3.0; +https://github.com/rws/ISHRemote) |
200 OK |
Counter-intuitively, this rule set doesn't punish a missing header — only headers matching known non-browser tool signatures. A bare Product/Version token gets bucketed with curl/python-requests and blocked. Only the RFC-sanctioned crawler self-identification convention (same format Googlebot/Bingbot use) got through. So "always send ISHRemote/x.y.z" is worse than today's behavior, and "never send anything" is unsafe against WAF configs that punish a missing header instead. Both failure directions exist; the fix must be adaptive, not a single hardcoded choice.
Fallback mechanism
Default behavior is unchanged: no User-Agent header sent unless a WAF has already proven that blocks the connection.
New-IshSession already makes exactly one "prove the credentials work" call per protocol before returning the session — a connectionconfiguration.xml download over HttpClient, and (for WcfSoapWithOpenIdConnect) an Application25.GetVersion() SOAP call that exists specifically to confirm the connection is alive. Today either failing with 403 fails New-IshSession outright.
New behavior: on 403 from that first call, retry it once with Mozilla/5.0 (compatible; ISHRemote/{version}; +https://github.com/rws/ISHRemote). On success, that header sticks for the session's lifetime — every subsequent request carries it, and a Write-Verbose logs the fallback and the value in use. On repeat failure, New-IshSession fails exactly as today.
No new parameter, no configuration, nothing for existing scripts to learn.
Non-goals
WcfSoapWithWsTrust is out of scope. Legacy/maintain-only, removed in 16.0.0, doesn't use the endpoint-behavior/message-inspector pattern today unlike WcfSoapWithOpenIdConnect.
- Not a per-call retry. Only the two existing "prove the connection" probe calls in
New-IshSession are guarded, not every subsequent SOAP/OpenAPI call. A WAF rule inconsistent per-operation won't be caught — closing that would mean retrofitting retry logic into per-cmdlet exception handling, which is duplicated across dozens of cmdlet files rather than centralized. Not worth it for a corner case.
Implementation details
Scope: IshSession.cs, InfoShareWcfSoapWithOpenIdConnectConnection.cs, one new file. OpenApiWithOpenIdConnect and its OIDC discovery/token calls are covered for free — they share the same HttpClient as connectionconfiguration.xml.
-
IshSession.cs
- Add a private mutable holder
_userAgentState (class with a single string Value property, default null) — must be a reference type read live by the WCF inspector, not captured once at construction.
LoadConnectionConfiguration(Uri): on !IsSuccessStatusCode and StatusCode == Forbidden, set _userAgentState.Value to the fallback string, set it on _httpClient.DefaultRequestHeaders.UserAgent, WriteVerbose the fallback and value, retry GetAsync once. Only on repeat failure throw the existing ArgumentException.
CreateInfoShareWcfSoapWithOpenIdConnectConnection(): wrap application25Proxy.GetVersion() in try/catch. On a 403-shaped failure — unwrap CommunicationException/ProtocolException → WebException → HttpStatusCode.Forbidden (WCF's exception wrapping isn't fully consistent across .NET targets) — set _userAgentState.Value to the fallback, WriteVerbose the fallback and value, retry GetVersion() once on the same already-open channel (inspector reads mutable state at send time, no channel rebuild needed).
CreateOpenApiWithOpenIdConnectConnection(): no dedicated retry — shares _httpClient, inherits whatever LoadConnectionConfiguration resolved.
-
New file Connection/InfoShareWcfSoapUserAgentClientMessageInspector.cs
InfoShareWcfSoapUserAgentClientMessageInspector : IClientMessageInspector — in BeforeSendRequest, if _userAgentState.Value != null, set HttpRequestMessageProperty.Headers[HttpRequestHeader.UserAgent] on the outgoing message.
InfoShareWcfSoapUserAgentEndpointBehavior : IEndpointBehavior — standard passthrough wiring ApplyClientBehavior to add the inspector.
- Constructed once per
IshSession, holding a reference to the same _userAgentState so a later flip is visible to every channel immediately.
-
InfoShareWcfSoapWithOpenIdConnectConnection.cs
- Mechanical: add one
.EndpointBehaviors.Add(userAgentBehavior) next to each of the ~18 existing .EndpointBehaviors.Add(bearerCredentials) call sites, so the header that got the probe through also rides along on every real cmdlet call afterward (confirmed this WAF evaluates per-request, not per-session/cookie).
-
Version string: reuse IshSession.ClientIshVersion/ClientVersion (already computed from assembly file version) for the fallback string.
Acceptance criteria
Context
More customers run Tridion Docs behind containerized/managed infrastructure on AWS, Azure or GCP, increasingly fronted by a WAF. All three clouds ship managed bot-detection rule groups that inspect
User-Agenton every request: AWS WAF Bot Control, Azure Front Door/App Gateway WAF bot manager rules, GCP Cloud Armor adaptive protection/bot management.HttpClientsends noUser-Agentunless set explicitly, and ISHRemote never has. No complaints yet, but it's a matter of time as more customers enable these rule groups — cheap to fix now against a live repro, expensive as a reactive support incident later.Measured against a live environment (
ish.example.com/health, AWS WAF fronted):User-AgentsentISHRemote/8.3.0(bare custom token)curl/8.0.1,PostmanRuntime/7.36.0,python-requests/2.31.0Mozilla/5.0 (compatible; ISHRemote/8.3.0; +https://github.com/rws/ISHRemote)Counter-intuitively, this rule set doesn't punish a missing header — only headers matching known non-browser tool signatures. A bare
Product/Versiontoken gets bucketed withcurl/python-requestsand blocked. Only the RFC-sanctioned crawler self-identification convention (same format Googlebot/Bingbot use) got through. So "always sendISHRemote/x.y.z" is worse than today's behavior, and "never send anything" is unsafe against WAF configs that punish a missing header instead. Both failure directions exist; the fix must be adaptive, not a single hardcoded choice.Fallback mechanism
Default behavior is unchanged: no
User-Agentheader sent unless a WAF has already proven that blocks the connection.New-IshSessionalready makes exactly one "prove the credentials work" call per protocol before returning the session — aconnectionconfiguration.xmldownload overHttpClient, and (forWcfSoapWithOpenIdConnect) anApplication25.GetVersion()SOAP call that exists specifically to confirm the connection is alive. Today either failing with403failsNew-IshSessionoutright.New behavior: on
403from that first call, retry it once withMozilla/5.0 (compatible; ISHRemote/{version}; +https://github.com/rws/ISHRemote). On success, that header sticks for the session's lifetime — every subsequent request carries it, and aWrite-Verboselogs the fallback and the value in use. On repeat failure,New-IshSessionfails exactly as today.No new parameter, no configuration, nothing for existing scripts to learn.
Non-goals
WcfSoapWithWsTrustis out of scope. Legacy/maintain-only, removed in 16.0.0, doesn't use the endpoint-behavior/message-inspector pattern today unlikeWcfSoapWithOpenIdConnect.New-IshSessionare guarded, not every subsequent SOAP/OpenAPI call. A WAF rule inconsistent per-operation won't be caught — closing that would mean retrofitting retry logic into per-cmdlet exception handling, which is duplicated across dozens of cmdlet files rather than centralized. Not worth it for a corner case.Implementation details
Scope:
IshSession.cs,InfoShareWcfSoapWithOpenIdConnectConnection.cs, one new file.OpenApiWithOpenIdConnectand its OIDC discovery/token calls are covered for free — they share the sameHttpClientasconnectionconfiguration.xml.IshSession.cs_userAgentState(class with a singlestring Valueproperty, defaultnull) — must be a reference type read live by the WCF inspector, not captured once at construction.LoadConnectionConfiguration(Uri): on!IsSuccessStatusCodeandStatusCode == Forbidden, set_userAgentState.Valueto the fallback string, set it on_httpClient.DefaultRequestHeaders.UserAgent,WriteVerbosethe fallback and value, retryGetAsynconce. Only on repeat failure throw the existingArgumentException.CreateInfoShareWcfSoapWithOpenIdConnectConnection(): wrapapplication25Proxy.GetVersion()in try/catch. On a 403-shaped failure — unwrapCommunicationException/ProtocolException→WebException→HttpStatusCode.Forbidden(WCF's exception wrapping isn't fully consistent across .NET targets) — set_userAgentState.Valueto the fallback,WriteVerbosethe fallback and value, retryGetVersion()once on the same already-open channel (inspector reads mutable state at send time, no channel rebuild needed).CreateOpenApiWithOpenIdConnectConnection(): no dedicated retry — shares_httpClient, inherits whateverLoadConnectionConfigurationresolved.New file
Connection/InfoShareWcfSoapUserAgentClientMessageInspector.csInfoShareWcfSoapUserAgentClientMessageInspector : IClientMessageInspector— inBeforeSendRequest, if_userAgentState.Value != null, setHttpRequestMessageProperty.Headers[HttpRequestHeader.UserAgent]on the outgoing message.InfoShareWcfSoapUserAgentEndpointBehavior : IEndpointBehavior— standard passthrough wiringApplyClientBehaviorto add the inspector.IshSession, holding a reference to the same_userAgentStateso a later flip is visible to every channel immediately.InfoShareWcfSoapWithOpenIdConnectConnection.cs.EndpointBehaviors.Add(userAgentBehavior)next to each of the ~18 existing.EndpointBehaviors.Add(bearerCredentials)call sites, so the header that got the probe through also rides along on every real cmdlet call afterward (confirmed this WAF evaluates per-request, not per-session/cookie).Version string: reuse
IshSession.ClientIshVersion/ClientVersion(already computed from assembly file version) for the fallback string.Acceptance criteria
New-IshSessionagainst a non-WAF'd environment is byte-identical to today (noUser-Agentsent, verified via capture/server log).New-IshSessionagainst an environment blocking bare-token UAs succeeds transparently, fallback header set, for bothOpenApiWithOpenIdConnectandWcfSoapWithOpenIdConnect.Write-Verboseentry stating the fallback and theUser-Agentvalue in use.Get-IshUserpost-session-creation).