Skip to content

[csharp][restsharp] add throwOnAnyError option to surface client errors#23663

Open
mjansrud wants to merge 4 commits intoOpenAPITools:masterfrom
mjansrud:csharp-throw-on-any-error
Open

[csharp][restsharp] add throwOnAnyError option to surface client errors#23663
mjansrud wants to merge 4 commits intoOpenAPITools:masterfrom
mjansrud:csharp-throw-on-any-error

Conversation

@mjansrud
Copy link
Copy Markdown
Contributor

@mjansrud mjansrud commented Apr 30, 2026

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request.
  • Run the shell script(s) under `bin/` (`bin/configs/`) to update samples — n/a, no sample changes (default off).
  • File the PR against the correct branch: `master`.
  • If your PR is targeting a particular programming language, @mention the technical committee members.

Description

By default the C# restsharp client silently drops deserialization failures. The chain:

  1. Deserializer.Deserialize<T> throws ApiException(500, e.Message) on parse failure.
  2. RestSharp's Execute<T> catches the exception and stuffs it into response.ErrorException, leaving response.Data = null.
  3. The generated ToApiResponse<T> carries ErrorText but discards ErrorException.
  4. ExceptionFactory only fires on non-2xx HTTP status, so a 200 with a deserialization failure passes through silently.
  5. The generated GetXxxAsync returns localVarResponse.Data directly — callers receive null with no signal in logs or APM.

We hit this in production when an upstream service started returning JSON missing a property our schema marked as required. Endpoints quietly returned empty lists — clients rendered nothing — there was no error to investigate. Discovering the cause required adding ad-hoc logging to the generated code.

Fix

This PR adds an opt-in switch throwOnAnyError (restsharp library only, default false) that sets RestClientOptions.ThrowOnAnyError = true. RestSharp then rethrows the original ApiException instead of swallowing it. No template logic changes, no behaviour change for existing users.

Verification

Tested locally on 14 generated clients in our monorepo. Before:

  • GET /candidates200 [] (silent — required field missing in upstream JSON)
  • No error in logs or App Insights.

After enabling throwOnAnyError=true:

  • GET /candidates500 with Required property 'track' not found in JSON. Path '[0]'
  • Function logger emits ApiException → propagates to APM.

Why default off

Keeping the default false to preserve backwards compatibility for users who may rely on the current null-on-deserialization-failure behaviour. Opt-in via --additional-properties throwOnAnyError=true.


Summary by cubic

Adds an opt-in throwOnAnyError option for C# restsharp clients to rethrow deserialization and transport errors so client failures surface instead of returning null. Default is false; existing behavior is unchanged.

  • New Features

    • Adds generator switch throwOnAnyError (C# restsharp) that sets RestClientOptions.ThrowOnAnyError = true so exceptions bubble up.
    • With a retry policy, rethrows PolicyResult.FinalException (or InvalidOperationException if it’s null) from DeserializeRestResponseFromPolicyAsync for consistent behavior.
  • Migration

    • Opt in via --additional-properties throwOnAnyError=true (or configOptions).

Written for commit 65b15cc. Summary will update on new commits. Review in cubic

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/[].
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 3 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/ApiClient.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache:471">
P2: `throwOnAnyError` is lost when retry policies are enabled: Polly captures the exception, the fallback response only stores `ErrorException`, and `ToApiResponse` does not surface it, so the generated `ApiResponse` still hides the client error.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…gured

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.
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

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/ApiClient.v790.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache:562">
P2: `throwOnAnyError` can throw `null` for handled-result policy failures, causing a `NullReferenceException` instead of surfacing the actual policy failure.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread modules/openapi-generator/src/main/resources/csharp/ApiClient.v790.mustache Outdated
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.
@mjansrud mjansrud requested a review from wing328 April 30, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants