Repository navigation
[csharp][restsharp] throwOnAnyError: throw ApiException instead of raw RestSharp exceptions - #25217
Conversation
…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>
There was a problem hiding this comment.
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>
…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>
|
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>
You're welcome, happy to :)
Not in production yet. We hit this in our own generator fork and decided it was worth upstreaming. I did add tests to the |
|
👌 Let's give it a try. Thanks for the contribution. |
throwOnAnyError(#23663) sets RestSharp'sThrowOnAnyError, which also throwsHttpRequestExceptionfor every 4xx/5xx beforeExceptionFactoryruns, breaking theApiExceptioncontract; transport failures surface as rawHttpRequestException.Option stays opt-in but is implemented in
ApiClient: if RestSharp recorded anErrorExceptionand status < 400, throwApiExceptionwith the status (0transport, real 2xx for deserialization failures), body and headers; 4xx/5xx still go throughExceptionFactory; 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 asInnerException(stack trace preserved; the retry-policy path previously had no message at all). The constructor lives in the sharedApiException.mustache, so httpclient and unityWebRequest clients gain it too — additive, existing constructors unchanged.Behaviour change for users who already opted into
throwOnAnyError: RestSharpHttpRequestExceptionbecomesApiExceptionand 4xx/5xx go throughExceptionFactory.Verified against the regenerated
net4.7/MultipleFrameworkssample (refused →ApiException(0), 500 →ApiException(500)with body/headers, malformed 200 →ApiException(200)); on master the first two wereHttpRequestException. Related: #15471, #8732, #10903, #15379, #15241.cc @mandrean @shibayan @Blackclaws @lucamazzanti
PR checklist
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.
Summary by cubic
Fixes
throwOnAnyErrorso transport and deserialization failures surface asApiExceptioninstead of raw RestSharpHttpRequestException.Previously the option enabled RestSharp's
ThrowOnAnyError, which threwHttpRequestExceptionfor every non-2xx response beforeExceptionFactorycould run. Now 4xx/5xx responses keep flowing throughExceptionFactory, and only errors with status below 400 throwApiExceptionwith 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.ApiExceptionnow accepts an inner exception so the original stack trace and message survive the wrap. The constructor lives in the sharedApiException.mustache, so the httpclient and unityWebRequest clients gain it too, additively.MultipleFrameworkssample covering transport failure and an unparseable 2xx body, both assertingApiExceptionwith the right status, body, and inner exception.ApiExceptionfor transport and deserialization failures, while ordinary HTTP error responses are handled byExceptionFactoryas usual.Written for commit bbb5cbf. Summary will update on new commits.