Skip to content

[csharp][restsharp] throwOnAnyError: throw ApiException instead of raw RestSharp exceptions - #25217

Merged
wing328 merged 3 commits into
OpenAPITools:masterfrom
mszopa-foss:fix/csharp-restsharp-transport-errors-apiexception
Oct 13, 2026
Merged

wing328 merged 3 commits into
OpenAPITools:masterfrom
mszopa-foss:fix/csharp-restsharp-transport-errors-apiexception

Conversation

@mszopa-foss

@mszopa-foss mszopa-foss commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

throwOnAnyError (#23663) sets RestSharp's ThrowOnAnyError, which also throws HttpRequestException for every 4xx/5xx before ExceptionFactory runs, breaking the ApiException contract; transport failures surface as raw HttpRequestException.

Option stays opt-in but is implemented in ApiClient: if RestSharp recorded an ErrorException and status < 400, throw ApiException with the status (0 transport, real 2xx for deserialization failures), body and headers; 4xx/5xx still go through ExceptionFactory; retry-policy failures return the error response so the same guard covers them.

Also adds ApiException(int, string, object, Multimap<string, string>, Exception innerException) so the original RestSharp exception is kept as InnerException (stack trace preserved; the retry-policy path previously had no message at all). The constructor lives in the shared ApiException.mustache, so httpclient and unityWebRequest clients gain it too — additive, existing constructors unchanged.

Behaviour change for users who already opted into throwOnAnyError: RestSharp HttpRequestException becomes ApiException and 4xx/5xx go through ExceptionFactory.

Verified against the regenerated net4.7/MultipleFrameworks sample (refused → ApiException(0), 500 → ApiException(500) with body/headers, malformed 200 → ApiException(200)); on master the first two were HttpRequestException. Related: #15471, #8732, #10903, #15379, #15241.

cc @mandrean @shibayan @Blackclaws @lucamazzanti

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Fixes throwOnAnyError so transport and deserialization failures surface as ApiException instead of raw RestSharp HttpRequestException.

Previously the option enabled RestSharp's ThrowOnAnyError, which threw HttpRequestException for every non-2xx response before ExceptionFactory could run. Now 4xx/5xx responses keep flowing through ExceptionFactory, and only errors with status below 400 throw ApiException with the status (0 for transport failures, the real 2xx for deserialization failures), response body, and headers. The retry-policy path returns the error response so the same guard covers it.

  • ApiException now accepts an inner exception so the original stack trace and message survive the wrap. The constructor lives in the shared ApiException.mustache, so the httpclient and unityWebRequest clients gain it too, additively.
  • Adds tests to the MultipleFrameworks sample covering transport failure and an unparseable 2xx body, both asserting ApiException with the right status, body, and inner exception.
  • Behavior change: users who already opted in now get ApiException for transport and deserialization failures, while ordinary HTTP error responses are handled by ExceptionFactory as usual.

Written for commit bbb5cbf. Summary will update on new commits.

View guided diff

…w RestSharp exceptions

Setting RestSharp's ThrowOnAnyError makes RestSharp throw HttpRequestException
for every non-2xx status as well, before ExceptionFactory runs. Callers that
enable the option lose the ApiException contract (status code, body, headers)
for ordinary 4xx/5xx responses, and transport failures surface as raw
HttpRequestException.

Keep the option opt-in but implement it in ApiClient: after the request, if
RestSharp recorded an ErrorException and the status is below 400, throw
ApiException carrying the status (0 for transport failures, the real 2xx for
deserialization failures), the response body and headers. 4xx/5xx keep
flowing to ExceptionFactory. The retry-policy path returns the error response
so the same guard handles it.

Co-authored-by: mrd4rkne <marcinsz.pv@gmail.com>
@mszopa-foss
mszopa-foss marked this pull request as ready for review October 12, 2026 06:57
@mszopa-foss mszopa-foss changed the title [csharp][restsharp] throwOnAnyError: throw ApiException instead of ra… [csharp][restsharp] throwOnAnyError: throw ApiException instead of raw RestSharp exceptions` Oct 12, 2026
@mszopa-foss mszopa-foss changed the title [csharp][restsharp] throwOnAnyError: throw ApiException instead of raw RestSharp exceptions` [csharp][restsharp] throwOnAnyError: throw ApiException instead of raw RestSharp exceptions Oct 12, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 5 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. 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:502">
P2: For retry-policy failures, this throws an ApiException with a blank message and drops the original exception. `DeserializeRestResponseFromPolicyAsync` (the else branch this PR changed) builds `new RestResponse<T>(request) { ErrorException = policyResult.FinalException }` and never sets `ErrorMessage` or `Content`, so `response.ErrorMessage` is null and the formatted message becomes "Error calling <resource>: ". Previously `throwOnAnyError` rethrew `policyResult.FinalException` directly, keeping the message and stack/InnerException detail; the ApiException constructor has no inner-exception parameter, so that detail is now lost entirely, making retry-exhaustion failures (e.g., a socket failure) effectively undiagnosable. Use the exception's own message (falling back to `ErrorMessage`) so the message is populated for this path. The same blank-message risk applies to plain transport failures if RestSharp does not populate `ErrorMessage` alongside `ErrorException`.</violation>
</file>

Comment thread modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache Outdated
…ror wraps it

Review follow-up: the retry-policy path sets only ErrorException, so the
message was blank and the stack trace lost. Add an ApiException constructor
that takes an inner exception and use the exception's own message.

Co-authored-by: mrd4rkne <marcinsz.pv@gmail.com>
@wing328

wing328 commented Oct 12, 2026

Copy link
Copy Markdown
Member

Thanks for the PR

Are you using this fix in your production environment and confirm it works as expected ?

…ks sample

Transport failure and an unparseable 2xx body must surface as ApiException
with status 0 / the real status, the body, and the original exception as
InnerException. Both fail against the previous ThrowOnAnyError wiring.

Co-authored-by: mrd4rkne <marcinsz.pv@gmail.com>
@mszopa-foss

Copy link
Copy Markdown
Contributor Author

@wing328

Thanks for the PR

You're welcome, happy to :)

Are you using this fix in your production environment and confirm it works as expected ?

Not in production yet. We hit this in our own generator fork and decided it was worth upstreaming. I did add tests to the net4.7/MultipleFrameworks sample (bbb5cbf) that reproduce both cases: a minimal HttpListener stub returning an unparseable 200 body, and a connection to a port where nothing listens. Both fail against the previous ThrowOnAnyError wiring and pass with this PR.

@wing328
wing328 merged commit 7697ee2 into OpenAPITools:master Oct 13, 2026
75 checks passed
@wing328

wing328 commented Oct 13, 2026

Copy link
Copy Markdown
Member

👌

Let's give it a try. Thanks for the contribution.

@mszopa-foss
mszopa-foss deleted the fix/csharp-restsharp-transport-errors-apiexception branch October 13, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants