OpenAPITools / OpenAPITools/openapi-generator

[BUG][core] splitOperationsByContentType: a variant sends the Accept of another media-type when the operation has JSON error responses

Open
#24,948 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

Bug Report Checklist
  • [ X ] Have you provided a full/minimal spec to reproduce the issue?
  • [ X ] Have you validated the input using an OpenAPI validator?
  • [ X ] Have you tested with the latest master to confirm the issue still exists?
  • [ X ] Have you searched for related issues/PRs?
  • [ X ] What's the actual output vs expected output?
Description

The problem. With splitOperationsByContentType=true (#23935, 7.25.0), an operation whose 200 is application/json | text/csv is generated as two methods, getReportAsJson and getReportAsCsv, each typed on its media-type. But as soon as the operation also declares a JSON error response (400: application/json), the generated getReportAsCsv sends Accept: application/json, not text/csv. A server that negotiates on Accept answers JSON (or 406) to a method that is typed to parse CSV. The option exists to make each media-type reachable, and the variant it generates cannot request its own media-type.

The same happens for java, python and spring (library: spring-cloud); details and generated code below.

Why. The split narrows only the success response of a variant; the error responses stay on every variant. produces of a CodegenOperation is the union of every response's content, so on the CSV variant it is [text/csv, application/json]. Each generator then derives the Accept header from that list with a rule that predates variants and prefers JSON:

  • java (okhttp-gson, resttemplate, …): selectHeaderAccept returns the first JSON entry;
  • python: select_header_accept does the same;
  • spring-cloud (Feign): produces is rendered from x-accepts, which AbstractJavaCodegen.preprocessOpenAPI computes on the original operation before the split (both variants inherit the same sorted list) and SpringMvcContract sends produces[0].

typescript-fetch is not affected: it merges the variants into overloads and reads the media-type from the x-content-type-variant-* extensions.

openapi-generator version

7.25.0 (and current master). Regression only in the sense that the feature is new: the option did not exist before 7.25.0.

OpenAPI declaration file content or url

https://gist.github.com/AntoineDuComptoirDesPharmacies/c80358f886afcae2e63613b6c09ed8af

Generation Details
java -jar openapi-generator-cli-7.25.0.jar generate -g spring --library spring-cloud \
  -i report.yaml -o out/spring --global-property splitOperationsByContentType=true

java -jar openapi-generator-cli-7.25.0.jar generate -g java \
  -i report.yaml -o out/java --global-property splitOperationsByContentType=true

java -jar openapi-generator-cli-7.25.0.jar generate -g python \
  -i report.yaml -o out/python --global-property splitOperationsByContentType=true
Actual output

spring / spring-cloud, both variants carry the same produces, and Feign sends its first element:

@RequestMapping(method = RequestMethod.GET, value = ReportsApi.PATH_GET_REPORT_AS_CSV,
    produces = { "application/json", "text/csv" })
ResponseEntity<String> getReportAsCsv(...);      // sends Accept: application/json

@RequestMapping(method = RequestMethod.GET, value = ReportsApi.PATH_GET_REPORT_AS_JSON,
    produces = { "application/json", "text/csv" })
ResponseEntity<Report> getReportAsJson(...);

java (okhttp-gson), the CSV variant lists JSON too, and selectHeaderAccept picks it:

// getReportAsCsvCall
final String[] localVarAccepts = { "text/csv", "application/json" };  // -> Accept: application/json

python:

# _get_report_as_csv_serialize
_header_params['Accept'] = self.api_client.select_header_accept(['text/csv', 'application/json'])  # -> application/json
Expected output

Each variant produces and asks for the single media-type it was narrowed to; the error responses keep typing their body as before:

produces = { "application/json" }   ResponseEntity<Report> getReportAsJson(...)
produces = { "text/csv" }           ResponseEntity<String> getReportAsCsv(...)
final String[] localVarAccepts = { "text/csv" };
_header_params['Accept'] = self.api_client.select_header_accept(['text/csv'])

Operations the split leaves alone must keep the union of every response in produces, as they always had.

Related issues/PRs
Suggest a fix

A PR follows. The idea: a variant's produces is the single media-type it was narrowed to, nothing else.

  • Core : DefaultCodegen.fromOperation builds a variant's produces from its method response alone (the one the split narrowed), and getProducesInfo returns that same media-type. Every client then asks for the media-type the variant is typed on, and every server generator maps each variant on its own media-type.
  • Java : AbstractJavaCodegen stamps x-accepts / x-content-type on the variants as divideOperationsByContentType creates them. Today they are computed once in preprocessOpenAPI, before the split, so the variants inherit the values of the operation they came from.
  • Unchanged : the error responses keep their own content and typing; operations the split leaves alone generate byte-identical output; the typescript-fetch split sample regenerates without a change.

One point I would like other opinions on: for the Java Spring server, a variant then declares only its own media-type in produces (getReportAsCsvproduces = "text/csv"), while the error responses of that operation are JSON. That is what lets Spring route Accept: text/csv and Accept: application/json to the right variant without an ambiguous mapping, and the JSON error bodies still go through @ExceptionHandler / @ControllerAdvice, which Spring negotiates on the client's Accept regardless of the handler's produces. But it does mean the annotation no longer lists the error media-types. Is that acceptable, or would you rather keep them in produces some other way?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with DefaultCodegen.fromOperation and getProducesInfo, then trace divideOperationsByContentType and AbstractJavaCodegen.preprocessOpenAPI, including x-accepts and x-content-type handling. Compare the Java, Python, and spring-cloud generated outputs using the report.yaml generation commands; done means each narrowed variant requests and declares only its own success media type while unsplit operations remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, python, spring
Domain
api, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.