Skip to content

Commit f87373c

Browse files
authored
[csharp][restsharp] add throwOnAnyError option to surface client errors (OpenAPITools#23663)
* [csharp][restsharp] add throwOnAnyError option to surface client errors By default, RestSharp swallows deserialization and transport exceptions into RestResponse.ErrorException, and the generated ToApiResponse<T> in this template only carries ErrorText — the actual exception is dropped. Combined with a generated GetXxxAsync that returns Data directly, callers silently receive null on any deserialization failure (e.g. a required property missing in the upstream JSON). The error never reaches application logs or APM. Add an opt-in `throwOnAnyError` switch (default false, restsharp library only) that sets `ThrowOnAnyError = true` on RestClientOptions, making RestSharp rethrow the original ApiException(500, ...) that the generated deserializer already throws. The exception then propagates to the caller and into normal application error handling. Default kept off to preserve backwards compatibility — opt in when you want bugs to surface instead of silently producing null/[]. * docs: regenerate csharp.md to alphabetical order * [csharp][restsharp] honour throwOnAnyError when retry policy is configured Polly's ExecuteAndCapture catches the rethrown ApiException, so without this change the option is silently neutralized whenever RetryConfiguration.RetryPolicy != null: the exception ends up in RestResponse.ErrorException, which ToApiResponse discards. When throwOnAnyError is enabled, rethrow PolicyResult.FinalException from DeserializeRestResponseFromPolicyAsync so the contract is consistent across both the no-retry and retry paths. Default-off branch is byte-identical to the previous output. * [csharp][restsharp] guard throwOnAnyError against null FinalException Polly's PolicyResult.FinalException is null when Outcome is Failure but the failure type is ResultHandledByThisPolicy (e.g. a retry policy configured with .HandleResult(...) that gives up after N retries). Throwing it directly would NRE; fall back to InvalidOperationException so the option still surfaces an error. Also restores the trailing whitespace on the if line so existing restsharp samples don't all need regenerating.
1 parent 2bb7296 commit f87373c

4 files changed

Lines changed: 24 additions & 1 deletion

File tree

docs/generators/csharp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
4747
|returnICollection|Return ICollection&lt;T&gt; instead of the concrete type.| |false|
4848
|sourceFolder|source folder for generated code| |src|
4949
|targetFramework|The target .NET framework version. To target multiple frameworks, use `;` as the separator, e.g. `netstandard2.1;netcoreapp3.1`|<dl><dt>**netstandard1.3**</dt><dd>.NET Standard 1.3</dd><dt>**netstandard1.4**</dt><dd>.NET Standard 1.4</dd><dt>**netstandard1.5**</dt><dd>.NET Standard 1.5</dd><dt>**netstandard1.6**</dt><dd>.NET Standard 1.6</dd><dt>**netstandard2.0**</dt><dd>.NET Standard 2.0</dd><dt>**netstandard2.1**</dt><dd>.NET Standard 2.1</dd><dt>**net47**</dt><dd>.NET Framework 4.7</dd><dt>**net48**</dt><dd>.NET Framework 4.8</dd><dt>**net8.0**</dt><dd>.NET 8.0 (End of Support 10 November 2026)</dd><dt>**net9.0**</dt><dd>.NET 9.0 (End of Support 10 November 2026)</dd><dt>**net10.0**</dt><dd>.NET 10.0 (End of Support 14 November 2028)</dd></dl>|net10.0|
50+
|throwOnAnyError|Configure RestSharp to rethrow deserialization and transport errors instead of swallowing them into RestResponse.ErrorException (which the default ToApiResponse&lt;T&gt; discards as null Data). Recommended for production use to surface bugs that would otherwise be invisible. (restsharp only)| |false|
5051
|useCollection|Deserialize array types to Collection&lt;T&gt; instead of List&lt;T&gt;.| |false|
5152
|useDateTimeForDate|Use DateTime to model date properties even if DateOnly supported. (.net 6.0+ only)| |false|
5253
|useDateTimeOffset|Use DateTimeOffset to model date-time properties| |false|

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpClientCodegen.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ public class CSharpClientCodegen extends AbstractCSharpCodegen {
122122
protected boolean supportsFileParameters = Boolean.TRUE;
123123
protected boolean supportsDateOnly = Boolean.FALSE;
124124
protected boolean useIntForTimeout = Boolean.FALSE;
125+
protected boolean throwOnAnyError = Boolean.FALSE;
125126

126127
@Setter protected boolean validatable = Boolean.TRUE;
127128
@Setter protected boolean equatable = Boolean.FALSE;
@@ -132,6 +133,7 @@ public class CSharpClientCodegen extends AbstractCSharpCodegen {
132133
private static final String OPERATION_PARAMETER_SORTING_KEY = "operationParameterSorting";
133134
private static final String MODEL_PROPERTY_SORTING_KEY = "modelPropertySorting";
134135
private static final String USE_INT_FOR_TIMEOUT = "useIntForTimeout";
136+
private static final String THROW_ON_ANY_ERROR = "throwOnAnyError";
135137

136138
enum SortingMethod {
137139
DEFAULT,
@@ -249,6 +251,10 @@ public CSharpClientCodegen() {
249251
"Use int for Timeout (fall back to v7.9.0 templates). This option (for restsharp only) will be deprecated so please migrated to TimeSpan instead.",
250252
String.valueOf(this.useIntForTimeout));
251253

254+
addSwitch(CSharpClientCodegen.THROW_ON_ANY_ERROR,
255+
"Configure RestSharp to rethrow deserialization and transport errors instead of swallowing them into RestResponse.ErrorException (which the default ToApiResponse<T> discards as null Data). Recommended for production use to surface bugs that would otherwise be invisible. (restsharp only)",
256+
this.throwOnAnyError);
257+
252258
CliOption framework = new CliOption(
253259
CodegenConstants.DOTNET_FRAMEWORK,
254260
CodegenConstants.DOTNET_FRAMEWORK_DESC
@@ -871,6 +877,7 @@ public void processOpts() {
871877
syncBooleanProperty(additionalProperties, "useSourceGeneration", this::setUseSourceGeneration, this.useSourceGeneration);
872878
syncBooleanProperty(additionalProperties, "supportsDateOnly", this::setSupportsDateOnly, this.supportsDateOnly);
873879
syncBooleanProperty(additionalProperties, "useIntForTimeout", this::setUseIntForTimeout, this.useIntForTimeout);
880+
syncBooleanProperty(additionalProperties, "throwOnAnyError", this::setThrowOnAnyError, this.throwOnAnyError);
874881

875882
final String testPackageName = testPackageName();
876883
String packageFolder = sourceFolder + File.separator + packageName;
@@ -1244,6 +1251,10 @@ public void setUseIntForTimeout(Boolean useIntForTimeout) {
12441251
this.useIntForTimeout = useIntForTimeout;
12451252
}
12461253

1254+
public void setThrowOnAnyError(Boolean throwOnAnyError) {
1255+
this.throwOnAnyError = throwOnAnyError;
1256+
}
1257+
12471258
public void setSupportsRetry(Boolean supportsRetry) {
12481259
this.supportsRetry = supportsRetry;
12491260
}

modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,8 @@ namespace {{packageName}}.Client
467467
Proxy = configuration.Proxy,
468468
UserAgent = configuration.UserAgent,
469469
UseDefaultCredentials = configuration.UseDefaultCredentials,
470-
RemoteCertificateValidationCallback = configuration.RemoteCertificateValidationCallback
470+
RemoteCertificateValidationCallback = configuration.RemoteCertificateValidationCallback{{#throwOnAnyError}},
471+
ThrowOnAnyError = true{{/throwOnAnyError}}
471472
};
472473
setOptions(clientOptions);
473474
@@ -566,10 +567,15 @@ namespace {{packageName}}.Client
566567
}
567568
else
568569
{
570+
{{#throwOnAnyError}}
571+
throw policyResult.FinalException ?? new InvalidOperationException("The retry policy failed without an exception.");
572+
{{/throwOnAnyError}}
573+
{{^throwOnAnyError}}
569574
return new RestResponse<T>(request)
570575
{
571576
ErrorException = policyResult.FinalException
572577
};
578+
{{/throwOnAnyError}}
573579
}
574580
}
575581

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,10 +558,15 @@ namespace {{packageName}}.Client
558558
}
559559
else
560560
{
561+
{{#throwOnAnyError}}
562+
throw policyResult.FinalException ?? new InvalidOperationException("The retry policy failed without an exception.");
563+
{{/throwOnAnyError}}
564+
{{^throwOnAnyError}}
561565
return new RestResponse<T>(request)
562566
{
563567
ErrorException = policyResult.FinalException
564568
};
569+
{{/throwOnAnyError}}
565570
}
566571
}
567572

0 commit comments

Comments
 (0)