Skip to content

Generate client with response#80

Merged
cubahno merged 1 commit into
mainfrom
ig/mult-responses
May 8, 2026
Merged

Generate client with response#80
cubahno merged 1 commit into
mainfrom
ig/mult-responses

Conversation

@cubahno
Copy link
Copy Markdown
Collaborator

@cubahno cubahno commented May 8, 2026

The generated client today exposes only one 2xx response shape per operation - the parser picks one "success" status code (last 2xx in spec order) and treats everything else, including other documented 2xx statuses, as an error. Response headers are dropped on the floor entirely. This is a real problem for any operation that legitimately returns multiple successful statuses with different bodies (e.g. 201 Created with the new resource versus 202 Accepted with a job handle), and for any caller that needs typed access to headers like Location or Retry-After.

There is no straightforward workaround in user code. The classic return type is (*BodyType, error), so additional bodies and headers cannot be reached through the existing API surface; users either fall back to raw *http.Response handling and re-implement decoding, or split the operation into separate spec entries.

What this changes

Introduces an opt-in code-generation flag, generate.client-with-response, that produces a sibling WithResponse(...) method per operation. The sibling returns a typed envelope whose shape is uniform across operations:

  • one body field per documented status, named (e.g. JSON201, Text503, ApplicationProblemJSON422), where the tag follows the existing content-type-to-tag
    mapping the classic generator already uses
  • one typed header struct per status that declares headers in the spec, e.g. Headers201, accessible alongside the body
  • the raw *http.Response, Body []byte, and StatusCode int for everything that is not modeled by the spec (undocumented headers, raw bytes, etc.)

The new method handles dispatching on resp.StatusCode and decoding into the matching field. Decoding mirrors the classic generator's behavior wherever it overlaps - JSON via json.Unmarshal, form-urlencoded via the same runtime.ConvertFormFields helper - so the envelope client is a drop-in replacement for any operation the classic client already handles correctly. For text/plain and text/html schemas typed as string, the envelope decodes as a direct string conversion rather than a json.Unmarshal call (which classic does and which fails on plain text bodies); for non-string text schemas it falls through to json.Unmarshal, so integer-valued text/plain count endpoints continue to work.

Documented errors (4xx/5xx) populate the envelope's matching status field and return a non-nil error. The error path uses the same runtime.NewClientAPIError and runtime.WithStatusCode constructors the classic client uses, so error semantics are identical - callers can keep their existing if err != nil flow while gaining typed access to the parsed error body when they want it.

Backward compatibility

The flag defaults to false. With it off, generation is byte-for-byte identical to the previous output. No existing user is affected unless they explicitly enable the flag.

The two flags interact along an additive matrix. With client: true only, classic methods are emitted exactly as before. With client-with-response: true only, the envelope method is the sole client surface. With both true, the same *Client struct gets both methods; the existing ClientInterface is widened in place to declare the envelope sibling alongside the classic method, so a single mock or test double satisfies both shapes. There is intentionally no second ClientWithResponseInterface. This is a small surface-area change for users implementing their own ClientInterface for tests, but those users explicitly opted in by enabling the flag.

For specs whose operations do not register any response-location types (e.g. operations that only reference component schemas), the envelope generation seeds the responses output with a header skeleton so the wrapper struct lands in a compilable file regardless of how the underlying spec is structured.

Naming for the new types - the wrapper <Op>Resp and per-status <Op>Resp<Status>Headers - is routed through the shared type tracker, so user-declared schemas with similar names get disambiguated rather than colliding.

Extensibility

Two pieces of the new code path are intentionally factored as separate templates so downstream forks that override the client template can reuse them without copying:

  • the wrapper-types template, which emits the envelope struct and per-status header structs - independent of how the function returns the wrapper
  • the body-decode helper, which handles the per-status content-type dispatch (JSON / Text / HTML / Formdata) - usable from inside any custom function-emission template

Internal data on the operation definition (WithResponseTypeName, HeaderTypeNames, the per-status Successes / Errors slices on the response definition) is the stable contract user templates can depend on.

Why an additive flag rather than a new client style

A mode-style flag (client: classic | envelope) was considered and rejected. Per-call-site choice has real value: many call sites just want the body and the envelope is noise, while others legitimately need headers or multiple 2xx handling. The additive shape lets each call site pick. It also lets the flag default flip later without renaming existing methods.

Why not fix the classic picker

The picker selecting the last spec-order 2xx as "the" success is a latent issue, but it is not what this change addresses. Fixing it would alter generated output for every existing user with multi-2xx operations and is a separable concern; the envelope client makes the picker irrelevant for callers who care about all statuses, which is the primary motivation here.

Screenshot 2026-05-08 at 16 03 09

@cubahno cubahno marked this pull request as ready for review May 8, 2026 14:05
@cubahno cubahno merged commit 0479d1d into main May 8, 2026
3 checks passed
@cubahno cubahno deleted the ig/mult-responses branch May 8, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant