[csharp][restsharp] throwOnAnyError: wrap oneOf/anyOf FromJson failures in ApiException

YB
1d ago
+52-1

Fixes #25218.

With throwOnAnyError=true, the restsharp ApiClient turns a recorded ErrorException into an ApiException (#25217). The oneOf/anyOf branch after that check calls FromJson itself, and when that throws, the catch rethrew the raw inner exception. So a response that RestSharp did not try to deserialize, but that matches none of the schemas, reached the caller as a bare InvalidDataException with no status code, body or headers.

Under throwOnAnyError, the catch now throws ApiException(status, "Error calling <resource>: <message>", content, headers, inner), the same shape as the ErrorException guard just above it, with the FromJson error kept as InnerException. Without throwOnAnyError the generated code is unchanged. The change is in both restsharp templates, ApiClient.mustache and the useIntForTimeout fallback ApiClient.v790.mustache.

What I measured, on a generated net10.0 restsharp client for a spec whose GET /pet returns oneOf: [Cat, Dog] (both required + additionalProperties: false), against a local HttpListener returning 200:

Responsemasterthis PR
text/plain, body OKraw System.IO.InvalidDataException ("The JSON string OK cannot be deserialized into any schema defined.")ApiException ErrorCode=200, ErrorContent=OK, headers kept, InnerException InvalidDataException
application/json, body {"swims":true}ApiException(200) (inner ApiException)same
text/html, an HTML pageApiException(200) (inner InvalidOperationException)same

So for a JSON body the #25217 guard already fires, because RestSharp's own deserialization fails first and sets ErrorException. The gap is a response RestSharp leaves alone, such as text/plain. Same results with useIntForTimeout=true (the v790 template).

A 4xx/5xx response whose body matches none of the schemas also takes this path before ExceptionFactory runs. It now gets an ApiException with its status code as well, where it used to get the raw InvalidDataException.

Tests

  • CSharpClientCodegenTest.testThrowOnAnyErrorWrapsOneOfDeserializationFailure: generates a restsharp client for each combination of throwOnAnyError and useIntForTimeout, and checks ApiClient.cs has the wrapping throw new ApiException(...) only when throwOnAnyError is on (and the old rethrow otherwise). Fails on master templates (does not contain line [throw new ApiException(...)]). It also fails when only ApiClient.mustache is fixed, which covers the v790 template.
  • ./mvnw test -pl modules/openapi-generator -am -Dtest=CSharpClientCodegenTest: 27 tests, 0 failures.
  • ./mvnw test -pl modules/openapi-generator -am (JDK 21, macOS arm64): 5574 tests, 0 failures, 0 errors, 11 skipped.
  • Regenerated with ./bin/generate-samples.sh bin/configs/csharp-restsharp-net4.7-multipleFrameworks.yaml (the only config with throwOnAnyError). One file changes: Client/ApiClient.cs, +2/-1. Built the regenerated library with dotnet build -f netstandard2.1 (.NET SDK 10.0.401, macOS arm64): succeeded. Its net47/net7.0 test project wasn't run here (no Mono, no .NET 7 runtime), and ThrowOnAnyErrorTests doesn't cover this path because the petstore spec has no oneOf model.

PR checklist

  • Read the contribution guidelines.
  • Run ./mvnw clean package, ./bin/generate-samples.sh ./bin/configs/*.yaml and ./bin/utils/export_docs_generators.sh. Instead: ./mvnw package -pl modules/openapi-generator-cli -am -DskipTests, the modules/openapi-generator tests, and ./bin/generate-samples.sh bin/configs/csharp-restsharp-net4.7-multipleFrameworks.yaml. The change sits inside {{#throwOnAnyError}} / {{^throwOnAnyError}}, and the other C# configs don't set that option, so their output is unchanged. No option or doc changed.
  • C# technical committee: @mandrean @shibayan @Blackclaws @lucamazzanti

Summary by cubic

Fixes #25218 so throwOnAnyError now wraps oneOf/anyOf deserialization failures in an ApiException. Previously, when RestSharp did not try to deserialize the body (for example, a text/plain response), the FromJson failure surfaced as a raw InvalidDataException with no status code, body, or headers.

  • Applies the same wrapping used by the existing ErrorException guard, keeping the parse error as InnerException.
  • Covers both the default ApiClient.mustache template and the useIntForTimeout fallback (ApiClient.v790.mustache).
  • Also corrects 4xx/5xx responses whose unmatched bodies previously reached the caller as bare InvalidDataException.
  • Generated output is unchanged when throwOnAnyError is off.
  • Adds a template test covering both flag combinations across both templates.