Skip to content

[csharp] fix: stop percent-encoding the User-Agent - #24923

Merged
wing328 merged 1 commit into
OpenAPITools:masterfrom
wiebren:fix/csharp-user-agent-not-url-encoded
Sep 11, 2026
Merged

wing328 merged 1 commit into
OpenAPITools:masterfrom
wiebren:fix/csharp-user-agent-not-url-encoded

Conversation

@wiebren

@wiebren wiebren commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

csharp/Configuration.mustache assigns the user agent through WebUtility.UrlEncode:

UserAgent = WebUtility.UrlEncode("{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}");

A User-Agent is not a URL component, and nothing downstream decodes it. So the "/" that
separates product from version — the one character nearly every user agent in the wild uses —
reaches the server as %2F, and because WebUtility.UrlEncode is form encoding, a space
reaches it as +:

httpUserAgent what the server sees
(unset — the default) OpenAPI-Generator%2F1.0.0%2Fcsharp
my-client/1.2.3 my-client%2F1.2.3
my-client/1.2.3 (linux) my-client%2F1.2.3+(linux)

Found porting a real client: the header the API sees is how support answers "which SDK and
which release is this customer on", and csharp was the one language of ten that did not send
what it was configured with.

Verified on the wire

A raw TcpListener, so the header is read as the bytes that actually went out — RestSharp
112.0.0 (the version the samples pin):

User-Agent: OpenAPI-Generator%2F1.0.0%2Fcsharp     <- RestClientOptions.UserAgent = WebUtility.UrlEncode(...)
User-Agent: OpenAPI-Generator/1.0.0/csharp         <- RestClientOptions.UserAgent = "..."

The second line is the point: RestSharp takes the unencoded value without complaint, so the
encoding buys nothing and costs the header's meaning.

All three libraries that share this template pass the value straight through, so what
Configuration assigns is what goes out:

  • restsharp — RestClientOptions.UserAgent = configuration.UserAgent
  • httpclient — request.Headers.TryAddWithoutValidation("User-Agent", configuration.UserAgent)
  • unityWebRequest — request.SetRequestHeader("User-Agent", configuration.UserAgent)

Where it came from

The encoding arrived on 2022-07-20 in 2dcc319 (#12789, "[csharp-netcore] Update RestSharp
and add client_credentials auth support"), which upgraded RestSharp to v108. That commit flips
the line from a plain assignment to the encoded one; its message mentions "fix authenticator
bug, and fix user agent bug" and later "Switch HttpUtility to WebUtility for compatibility",
and neither the PR description nor any review comment records what the user agent bug was.
Whatever RestSharp v108 did with the header then, RestSharp 112 does not do now — see the
probe above.

Two things suggest it was collateral rather than intent:

  • the sibling generator disagrees — csharp-functions/Configuration.mustache has always
    assigned the same value with no encoding;
  • Configuration.mustache is shared by restsharp, httpclient and unityWebRequest, so a fix
    aimed at one RestSharp version landed on all three.

It stayed invisible because the result is still a valid header — no client errors, no server
rejects it — so the only symptom is server-side analytics grouping the SDK under a name nobody
recognizes.

The change

The WebUtility.UrlEncode call is removed from both restsharp Configuration templates — the
default one and the useIntForTimeout v7.9.0 fallback Configuration.v790.mustache.
using System.Net; stays: Proxy is a WebProxy.

generichost is untouched — it never sets UserAgent at all, which is a separate gap.

Tests

CSharpClientCodegenTest#testUserAgentIsNotUrlEncoded generates with
httpUserAgent=my-client/1.2.3 (linux) and asserts the literal reaches Configuration.cs and
that the WebUtility.UrlEncode( wrapper is gone, once per Configuration template
(useIntForTimeout false and true). It fails on master:

java.lang.AssertionError: File '.../Client/Configuration.cs' does not contain line
[UserAgent = "my-client/1.2.3 (linux)";] expected [true] but found [false]

13 tests pass in CSharpClientCodegenTest.

PR checklist

  • Read the contribution guidelines.
  • Built the project and updated samples (./bin/generate-samples.sh bin/configs/csharp-*).
    23 sample files changed, each by exactly this one line — 12 restsharp, 5 httpclient,
    3 unityWebRequest, plus the echo-api and complex-files samples. No option was added or
    changed, so docs/generators is unaffected.
  • Filed against master.
  • Technical committee: @jimschubert @Blackclaws @devhl-labs @Gronsak

Generated with Claude Code


Summary by cubic

Stops percent-encoding the C# generator's User-Agent so servers see it exactly as configured instead of OpenAPI-Generator%2F1.0.0%2Fcsharp (spaces became +). The encoding was added for a RestSharp v108 quirk, but RestSharp 112 and the other libraries pass the value through unchanged, so it only corrupted the header.

  • Removes the WebUtility.UrlEncode wrapper from both restsharp Configuration templates (default and useIntForTimeout v790 fallback); using System.Net; stays for WebProxy.
  • Affects restsharp, httpclient, and unityWebRequest, which all send the User-Agent as assigned.
  • Adds a test asserting the literal my-client/1.2.3 (linux) value reaches Configuration.cs for both templates.

Written for commit 2175593. Summary will update on new commits.

Review in cubic

Configuration.mustache wraps the user agent in WebUtility.UrlEncode, so the
"/" that separates product from version reaches the server as %2F and a
space reaches it as "+": the default agent goes out as
OpenAPI-Generator%2F1.0.0%2Fcsharp, and httpUserAgent=my-client/1.2.3 goes
out as my-client%2F1.2.3. A User-Agent is not a URL component and nothing
downstream decodes it.

The encoding arrived with the RestSharp v108 upgrade in OpenAPITools#12789 for a "user
agent bug" the PR does not describe. RestSharp 112 accepts the unencoded
value: pointed at a raw socket, RestClientOptions.UserAgent = "OpenAPI-
Generator/1.0.0/csharp" puts exactly that on the wire. The other two
libraries sharing this template never validated it either - httpclient uses
TryAddWithoutValidation, unityWebRequest SetRequestHeader - and the sibling
csharp-functions generator has always assigned the value unencoded.

Drop the call in both restsharp Configuration templates. using System.Net
stays for WebProxy. Every csharp sample changes by exactly this one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AMTM9KKdWLnfdCCiN32pM

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 26 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/resources/csharp/Configuration.v790.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/csharp/Configuration.v790.mustache:129">
P2: Removing WebUtility.UrlEncode also removed the escaping that kept user-supplied httpUserAgent values inside a valid C# string literal. A configured UA containing a double quote or backslash (previously encoded to %22/%5C) now emits a malformed literal such as `UserAgent = "my "client"";`, which fails to compile the generated SDK. Consider escaping `\` and `"` in httpUserAgent (e.g., at the codegen level or via a template helper) before inserting it into the literal, since this value is user-controlled config.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

{
Proxy = null;
UserAgent = WebUtility.UrlEncode("{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}");
UserAgent = "{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Removing WebUtility.UrlEncode also removed the escaping that kept user-supplied httpUserAgent values inside a valid C# string literal. A configured UA containing a double quote or backslash (previously encoded to %22/%5C) now emits a malformed literal such as UserAgent = "my "client"";, which fails to compile the generated SDK. Consider escaping \ and " in httpUserAgent (e.g., at the codegen level or via a template helper) before inserting it into the literal, since this value is user-controlled config.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/csharp/Configuration.v790.mustache, line 129:

<comment>Removing WebUtility.UrlEncode also removed the escaping that kept user-supplied httpUserAgent values inside a valid C# string literal. A configured UA containing a double quote or backslash (previously encoded to %22/%5C) now emits a malformed literal such as `UserAgent = "my "client"";`, which fails to compile the generated SDK. Consider escaping `\` and `"` in httpUserAgent (e.g., at the codegen level or via a template helper) before inserting it into the literal, since this value is user-controlled config.</comment>

<file context>
@@ -126,7 +126,7 @@ namespace {{packageName}}.Client
         {
             Proxy = null;
-            UserAgent = WebUtility.UrlEncode("{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}");
+            UserAgent = "{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}";
             BasePath = "{{{basePath}}}";
             DefaultHeaders = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this against both jars, and the escaping the UrlEncode call is credited with never existed at generation time.

WebUtility.UrlEncode runs at runtime, on a string literal that has already been emitted. The {{httpUserAgent}} substitution happens inside the quotes either way, so a value carrying " or \ lands in the literal identically before and after this change. Same spec, same generator, --library restsharp, httpUserAgent=my "client"\back:

master:

UserAgent = WebUtility.UrlEncode("my &quot;client&quot;\back");

this branch:

UserAgent = "my &quot;client&quot;\back";

Byte-identical literal; the wrapper is the whole delta.

Note the &quot;: mustache escapes the substitution, so a double quote never reaches the generated source as a quote in the first place and the literal stays well-formed on both sides. \b happens to be a valid C# escape so both of these compile — but a value ending in a backslash, or carrying an invalid escape such as \z, is malformed on master exactly as it is here. That makes it a pre-existing gap in how httpUserAgent is interpolated, shared with the generators that paste the same property into their own string literals (go/configuration.mustache, Java/ApiClient.mustache, php/Configuration.mustache, python/api_client.mustache, ...), rather than something this PR introduces.

Happy to look at escaping it properly at the codegen level as a separate cross-generator change if that is wanted — it does not belong to a fix that only removes a runtime call.

@Gronsak

Gronsak commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Just curious, why am I being mentioned as part of the "technical committee" here? Haven't really touched anything concerning this since back in 2017 and that was before it became openapi

@wiebren

wiebren commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Just curious, why am I being mentioned as part of the "technical committee" here? Haven't really touched anything concerning this since back in 2017 and that was before it became openapi

Sorry for that, this was a mis interpretation of README.md where you are listed as template creator, not technical committe

@Gronsak

Gronsak commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Just curious, why am I being mentioned as part of the "technical committee" here? Haven't really touched anything concerning this since back in 2017 and that was before it became openapi

Sorry for that, this was a mis interpretation of README.md where you are listed as template creator, not technical committe

no worries, was very much a blast from the past

@wing328
wing328 merged commit d09ade8 into OpenAPITools:master Sep 11, 2026
75 checks passed
@wing328

wing328 commented Sep 11, 2026

Copy link
Copy Markdown
Member

@wiebren thanks for the PR, which has been merged.

When you've time next week, can you please PM me via Slack to quickly go through your PRs?

https://join.slack.com/t/openapi-generator/shared_invite/zt-36ucx4ybl-jYrN6euoYn6zxXNZdldoZA

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants