Repository navigation
[csharp] fix: stop percent-encoding the User-Agent - #24923
Conversation
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
There was a problem hiding this comment.
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}}"; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 "client"\back");this branch:
UserAgent = "my "client"\back";Byte-identical literal; the wrapper is the whole delta.
Note the ": 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.
|
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 |
|
@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 |
csharp/Configuration.mustacheassigns the user agent throughWebUtility.UrlEncode:A
User-Agentis not a URL component, and nothing downstream decodes it. So the "/" thatseparates product from version — the one character nearly every user agent in the wild uses —
reaches the server as
%2F, and becauseWebUtility.UrlEncodeis form encoding, a spacereaches it as
+:httpUserAgentOpenAPI-Generator%2F1.0.0%2Fcsharpmy-client/1.2.3my-client%2F1.2.3my-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 — RestSharp112.0.0 (the version the samples pin):
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
Configurationassigns is what goes out:RestClientOptions.UserAgent = configuration.UserAgentrequest.Headers.TryAddWithoutValidation("User-Agent", configuration.UserAgent)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:
csharp-functions/Configuration.mustachehas alwaysassigned the same value with no encoding;
Configuration.mustacheis shared by restsharp, httpclient and unityWebRequest, so a fixaimed 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.UrlEncodecall is removed from both restsharp Configuration templates — thedefault one and the
useIntForTimeoutv7.9.0 fallbackConfiguration.v790.mustache.using System.Net;stays:Proxyis aWebProxy.generichostis untouched — it never setsUserAgentat all, which is a separate gap.Tests
CSharpClientCodegenTest#testUserAgentIsNotUrlEncodedgenerates withhttpUserAgent=my-client/1.2.3 (linux)and asserts the literal reachesConfiguration.csandthat the
WebUtility.UrlEncode(wrapper is gone, once per Configuration template(
useIntForTimeoutfalse and true). It fails on master:13 tests pass in
CSharpClientCodegenTest.PR checklist
./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/generatorsis unaffected.master.Generated with Claude Code
Summary by cubic
Stops percent-encoding the C# generator's
User-Agentso servers see it exactly as configured instead ofOpenAPI-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.WebUtility.UrlEncodewrapper from both restsharp Configuration templates (default anduseIntForTimeoutv790 fallback);using System.Net;stays forWebProxy.my-client/1.2.3 (linux)value reachesConfiguration.csfor both templates.Written for commit 2175593. Summary will update on new commits.