Skip to content

Commit d09ade8

Browse files
wiebrenclaude
andauthored
[csharp] fix: stop percent-encoding the User-Agent (#24923)
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 #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. Claude-Session: https://claude.ai/code/session_016AMTM9KKdWLnfdCCiN32pM Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a7678ef commit d09ade8

26 files changed

Lines changed: 54 additions & 25 deletions

File tree

‎modules/openapi-generator/src/main/resources/csharp/Configuration.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ namespace {{packageName}}.Client
126126
public Configuration()
127127
{
128128
Proxy = null;
129-
UserAgent = WebUtility.UrlEncode("{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}");
129+
UserAgent = "{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}";
130130
BasePath = "{{{basePath}}}";
131131
DefaultHeaders = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();
132132
ApiKey = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();

‎modules/openapi-generator/src/main/resources/csharp/Configuration.v790.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ namespace {{packageName}}.Client
126126
public Configuration()
127127
{
128128
Proxy = null;
129-
UserAgent = WebUtility.UrlEncode("{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}");
129+
UserAgent = "{{httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{packageVersion}}/csharp{{/httpUserAgent}}";
130130
BasePath = "{{{basePath}}}";
131131
DefaultHeaders = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();
132132
ApiKey = new {{^net35}}Concurrent{{/net35}}Dictionary<string, string>();

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,35 @@ public void testHandleConstantParams() throws IOException {
203203
"localVarRequestOptions.HeaderParameters.Add(\"X-CUSTOM_CONSTANT_HEADER\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(\"CONSTANT_VALUE\"));");
204204
}
205205

206+
@Test
207+
public void testUserAgentIsNotUrlEncoded() throws IOException {
208+
// both restsharp Configuration templates: the default one and the useIntForTimeout v7.9.0 fallback
209+
for (boolean useIntForTimeout : new boolean[]{false, true}) {
210+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
211+
output.deleteOnExit();
212+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
213+
final DefaultGenerator defaultGenerator = new DefaultGenerator();
214+
final ClientOptInput clientOptInput = new ClientOptInput();
215+
clientOptInput.openAPI(openAPI);
216+
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
217+
cSharpClientCodegen.setLibrary("restsharp");
218+
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
219+
cSharpClientCodegen.additionalProperties().put(CodegenConstants.HTTP_USER_AGENT, "my-client/1.2.3 (linux)");
220+
cSharpClientCodegen.additionalProperties().put("useIntForTimeout", useIntForTimeout);
221+
clientOptInput.config(cSharpClientCodegen);
222+
defaultGenerator.opts(clientOptInput);
223+
224+
Map<String, File> files = defaultGenerator.generate().stream()
225+
.collect(Collectors.toMap(File::getPath, Function.identity()));
226+
227+
File configuration = files
228+
.get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Client", "Configuration.cs").toString());
229+
assertNotNull(configuration);
230+
assertFileContains(configuration.toPath(), "UserAgent = \"my-client/1.2.3 (linux)\";");
231+
assertFileNotContains(configuration.toPath(), "UserAgent = WebUtility.UrlEncode(");
232+
}
233+
}
234+
206235
@Test
207236
public void test31specAdditionalPropertiesOfOneOf() throws IOException {
208237
// for https://github.com/OpenAPITools/openapi-generator/pull/18772

‎samples/client/echo_api/csharp/restsharp/net8/EchoApi/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public class Configuration : IReadableConfiguration
118118
public Configuration()
119119
{
120120
Proxy = null;
121-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
121+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
122122
BasePath = "http://localhost:3000";
123123
DefaultHeaders = new ConcurrentDictionary<string, string>();
124124
ApiKey = new ConcurrentDictionary<string, string>();

‎samples/client/others/csharp-complex-files/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public class Configuration : IReadableConfiguration
112112
public Configuration()
113113
{
114114
Proxy = null;
115-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
115+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
116116
BasePath = "http://localhost";
117117
DefaultHeaders = new ConcurrentDictionary<string, string>();
118118
ApiKey = new ConcurrentDictionary<string, string>();

‎samples/client/petstore/csharp/httpclient/net10/Petstore-nonPublicApi/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ internal class Configuration : IReadableConfiguration
117117
public Configuration()
118118
{
119119
Proxy = null;
120-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
120+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
121121
BasePath = "http://petstore.swagger.io/v2";
122122
DefaultHeaders = new ConcurrentDictionary<string, string>();
123123
ApiKey = new ConcurrentDictionary<string, string>();

‎samples/client/petstore/csharp/httpclient/net10/Petstore/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public class Configuration : IReadableConfiguration
122122
public Configuration()
123123
{
124124
Proxy = null;
125-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
125+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
126126
BasePath = "http://petstore.swagger.io:80/v2";
127127
DefaultHeaders = new ConcurrentDictionary<string, string>();
128128
ApiKey = new ConcurrentDictionary<string, string>();

‎samples/client/petstore/csharp/httpclient/net9/Petstore-nonPublicApi/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ internal class Configuration : IReadableConfiguration
117117
public Configuration()
118118
{
119119
Proxy = null;
120-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
120+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
121121
BasePath = "http://petstore.swagger.io/v2";
122122
DefaultHeaders = new ConcurrentDictionary<string, string>();
123123
ApiKey = new ConcurrentDictionary<string, string>();

‎samples/client/petstore/csharp/httpclient/net9/Petstore/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public class Configuration : IReadableConfiguration
122122
public Configuration()
123123
{
124124
Proxy = null;
125-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
125+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
126126
BasePath = "http://petstore.swagger.io:80/v2";
127127
DefaultHeaders = new ConcurrentDictionary<string, string>();
128128
ApiKey = new ConcurrentDictionary<string, string>();

‎samples/client/petstore/csharp/httpclient/standard2.0/Petstore/src/Org.OpenAPITools/Client/Configuration.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public class Configuration : IReadableConfiguration
117117
public Configuration()
118118
{
119119
Proxy = null;
120-
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
120+
UserAgent = "OpenAPI-Generator/1.0.0/csharp";
121121
BasePath = "http://petstore.swagger.io:80/v2";
122122
DefaultHeaders = new ConcurrentDictionary<string, string>();
123123
ApiKey = new ConcurrentDictionary<string, string>();

0 commit comments

Comments
 (0)