From dddb0855db4bda5c2a290bc2a627558f6691392d Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:52:26 -0500 Subject: [PATCH 1/2] [csharp][restsharp] throwOnAnyError: wrap oneOf/anyOf FromJson failures in ApiException When RestSharp does not deserialize the body itself (for example a text/plain response), ErrorException stays null and the oneOf/anyOf FromJson call is the first to fail. Its InvalidDataException reached the caller without status code, body or headers, although throwOnAnyError promises an ApiException. Wrap it like the ErrorException case, with the parse error as the inner exception, in both restsharp ApiClient templates. Fixes #25218 --- .../main/resources/csharp/ApiClient.mustache | 6 +++ .../resources/csharp/ApiClient.v790.mustache | 6 +++ .../CSharpClientCodegenTest.java | 38 +++++++++++++++++++ .../src/Org.OpenAPITools/Client/ApiClient.cs | 3 +- 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache index bad3fbfa9cb2..1841a6389fb6 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache @@ -511,7 +511,13 @@ namespace {{packageName}}.Client } catch (Exception ex) { + {{#throwOnAnyError}} + var error = ex.InnerException != null ? ex.InnerException : ex; + throw new ApiException((int)response.StatusCode, string.Format("Error calling {0}: {1}", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error); + {{/throwOnAnyError}} + {{^throwOnAnyError}} throw ex.InnerException != null ? ex.InnerException : ex; + {{/throwOnAnyError}} } } else if (typeof(T).Name == "Stream") // for binary response diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache index d2f74ed945d4..e7c0cc5207f8 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache @@ -503,7 +503,13 @@ namespace {{packageName}}.Client } catch (Exception ex) { + {{#throwOnAnyError}} + var error = ex.InnerException != null ? ex.InnerException : ex; + throw new ApiException((int)response.StatusCode, string.Format("Error calling {0}: {1}", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error); + {{/throwOnAnyError}} + {{^throwOnAnyError}} throw ex.InnerException != null ? ex.InnerException : ex; + {{/throwOnAnyError}} } } else if (typeof(T).Name == "Stream") // for binary response diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java index f4d3bb56ba5d..af251aa0e96f 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java @@ -448,6 +448,44 @@ public void testUserAgentIsNotUrlEncoded() throws IOException { } } + @Test + public void testThrowOnAnyErrorWrapsOneOfDeserializationFailure() throws IOException { + // both restsharp ApiClient templates: the default one and the useIntForTimeout v7.9.0 fallback + for (boolean useIntForTimeout : new boolean[]{false, true}) { + for (boolean throwOnAnyError : new boolean[]{false, true}) { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml"); + final DefaultGenerator defaultGenerator = new DefaultGenerator(); + final ClientOptInput clientOptInput = new ClientOptInput(); + clientOptInput.openAPI(openAPI); + CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen(); + cSharpClientCodegen.setLibrary("restsharp"); + cSharpClientCodegen.setOutputDir(output.getAbsolutePath()); + cSharpClientCodegen.additionalProperties().put("useIntForTimeout", useIntForTimeout); + cSharpClientCodegen.additionalProperties().put("throwOnAnyError", throwOnAnyError); + clientOptInput.config(cSharpClientCodegen); + defaultGenerator.opts(clientOptInput); + + Map files = defaultGenerator.generate().stream() + .collect(Collectors.toMap(File::getPath, Function.identity())); + + File apiClient = files + .get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Client", "ApiClient.cs").toString()); + assertNotNull(apiClient); + String wrapped = "throw new ApiException((int)response.StatusCode, string.Format(\"Error calling {0}: {1}\", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error);"; + String rethrown = "throw ex.InnerException != null ? ex.InnerException : ex;"; + if (throwOnAnyError) { + assertFileContains(apiClient.toPath(), wrapped); + assertFileNotContains(apiClient.toPath(), rethrown); + } else { + assertFileContains(apiClient.toPath(), rethrown); + assertFileNotContains(apiClient.toPath(), wrapped); + } + } + } + } + @Test public void testSelectHeaderAcceptKeepsAllTypesJsonFirst() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs index 58091fdfa71e..6e24cfd43266 100644 --- a/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs @@ -508,7 +508,8 @@ private async Task> ExecClientAsync(Func Date: Tue, 3 Nov 2026 19:05:35 -0600 Subject: [PATCH 2/2] [csharp][restsharp] throwOnAnyError: leave 4xx/5xx oneOf/anyOf failures to ExceptionFactory The oneOf/anyOf FromJson wrap threw ApiException for every status code, so a 4xx/5xx body matching none of the schemas never reached ExceptionFactory and a custom factory was ignored for those operations. Wrap only status codes below 400, like the ErrorException check above it, and let 4xx/5xx fall through to ExceptionFactory as other operations do. --- .../src/main/resources/csharp/ApiClient.mustache | 8 ++++++-- .../src/main/resources/csharp/ApiClient.v790.mustache | 8 ++++++-- .../codegen/csharpnetcore/CSharpClientCodegenTest.java | 4 +++- .../src/Org.OpenAPITools/Client/ApiClient.cs | 8 ++++++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache index 1841a6389fb6..996c435c84ac 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache @@ -512,8 +512,12 @@ namespace {{packageName}}.Client catch (Exception ex) { {{#throwOnAnyError}} - var error = ex.InnerException != null ? ex.InnerException : ex; - throw new ApiException((int)response.StatusCode, string.Format("Error calling {0}: {1}", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error); + // 4xx/5xx are left to ExceptionFactory, like the ErrorException check above + if ((int)response.StatusCode < 400) + { + var error = ex.InnerException != null ? ex.InnerException : ex; + throw new ApiException((int)response.StatusCode, string.Format("Error calling {0}: {1}", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error); + } {{/throwOnAnyError}} {{^throwOnAnyError}} throw ex.InnerException != null ? ex.InnerException : ex; diff --git a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache index e7c0cc5207f8..bc111a73c080 100644 --- a/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache @@ -504,8 +504,12 @@ namespace {{packageName}}.Client catch (Exception ex) { {{#throwOnAnyError}} - var error = ex.InnerException != null ? ex.InnerException : ex; - throw new ApiException((int)response.StatusCode, string.Format("Error calling {0}: {1}", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error); + // 4xx/5xx are left to ExceptionFactory, like the ErrorException check above + if ((int)response.StatusCode < 400) + { + var error = ex.InnerException != null ? ex.InnerException : ex; + throw new ApiException((int)response.StatusCode, string.Format("Error calling {0}: {1}", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error); + } {{/throwOnAnyError}} {{^throwOnAnyError}} throw ex.InnerException != null ? ex.InnerException : ex; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java index af251aa0e96f..61087564e760 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java @@ -474,9 +474,11 @@ public void testThrowOnAnyErrorWrapsOneOfDeserializationFailure() throws IOExcep .get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Client", "ApiClient.cs").toString()); assertNotNull(apiClient); String wrapped = "throw new ApiException((int)response.StatusCode, string.Format(\"Error calling {0}: {1}\", request.Resource, error.Message), response.Content, ToApiResponse(response).Headers, error);"; + // 4xx/5xx must still reach ExceptionFactory, so only lower status codes are wrapped here + String wrappedBelow400 = "if ((int)response.StatusCode < 400) { var error = ex.InnerException != null ? ex.InnerException : ex; " + wrapped + " }"; String rethrown = "throw ex.InnerException != null ? ex.InnerException : ex;"; if (throwOnAnyError) { - assertFileContains(apiClient.toPath(), wrapped); + assertFileContains(apiClient.toPath(), wrappedBelow400); assertFileNotContains(apiClient.toPath(), rethrown); } else { assertFileContains(apiClient.toPath(), rethrown); diff --git a/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs b/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs index 6e24cfd43266..5d4eab7fb08f 100644 --- a/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs +++ b/samples/client/petstore/csharp/restsharp/net4.7/MultipleFrameworks/src/Org.OpenAPITools/Client/ApiClient.cs @@ -508,8 +508,12 @@ private async Task> ExecClientAsync(Func