Skip to content

fix(client): typed multi-response handling with ApiError envelope (closes #8)#9

Merged
lightsofapollo merged 2 commits into
mainfrom
fix/issue-8-multi-response-handling
May 7, 2026
Merged

fix(client): typed multi-response handling with ApiError envelope (closes #8)#9
lightsofapollo merged 2 commits into
mainfrom
fix/issue-8-multi-response-handling

Conversation

@lightsofapollo
Copy link
Copy Markdown
Contributor

Summary

Closes #8. Operations that declared multiple responses (e.g. 200 + 400) silently collapsed into a single Response type — the inline-naming function in analysis.rs ignored its status_code argument so the second schema overwrote the first. The generated client then tried to deserialize success bodies into the error shape.

This PR reshapes error handling around a typed envelope so the right thing happens whether the success/error bodies are declared, undeclared, or present-but-undeserializable. Discussion of the design is in issue #8.

Design

  • ApiError<E> — envelope that always carries status + headers + raw body, with optional typed: Option<E> populated when the body matched a declared schema. Solves the side-tangent ask from Multiple Operation Responses Appear to Not Be Handled #8 (you can now inspect what the server actually sent without modifying the generated code).
  • ApiOpError<E> = Transport(HttpError) | Api(ApiError<E>) — new return-type wrapper for every generated operation method.
  • Per-operation error enums — operations with declared non-2xx body schemas get {Op}ApiError { Status400(BadRequestBody), … }; ops without declared error bodies fall back to ApiOpError<serde_json::Value> so the body is still inspectable as JSON.
  • Always-on raw-body capture — the response handler reads the body to a string before any typed parse, so deserialization failures preserve the bytes.

The reasoning for not using a single all-responses enum is in the issue thread — short version: it forces every caller to match every variant and loses Result ergonomics. Successes are Ok(T), errors are typed Err(ApiOpError<E>).

Breaking changes (0.2.0)

  • Generated method signatures change from HttpResult<T> to Result<T, ApiOpError<E>>.
  • The HttpError::Http { ... } variant and from_status helper are removed from generated code (replaced by ApiError<E>). The is_client_error / is_server_error helpers move to ApiError<E>.
  • Bumped to 0.2.0 per CLAUDE.md "no backwards compat" policy.

Test plan

  • cargo test — all suites green (unit + snapshot + integration)
  • New end-to-end compile tests in tests/multi_response_client_test.rs:
    • Toy 200+400 multi-response spec — verifies the per-op enum, Status400(BadRequest) variant, and ApiOpError<…> signatures
    • anthropic fixture — full client compiles
    • openai-responses fixture — full client compiles
  • Reproducing test from the issue (test_multiple_inline_responses_have_distinct_schemas) — fails on main, passes here
  • examples/generated/client.rs regenerated and committed so the example output matches the new shape

🤖 Generated with Claude Code

…oses #8)

Operations that declared multiple responses (e.g. 200 + 400) silently
collapsed into a single Response type — the inline-naming function in
analysis.rs ignored its status_code argument so the second schema
overwrote the first in the schema registry. The generated client then
tried to deserialize success bodies into the error shape.

Fix the naming collision and reshape the error story:

- ApiError<E> envelope always carries status + headers + raw body, so
  callers can inspect what the server actually sent without hacking the
  generated code (the original side-tangent in #8).
- ApiOpError<E> = Transport(HttpError) | Api(ApiError<E>) is the new
  return type for every generated operation method.
- Per-operation error enums are emitted when an operation declares
  non-2xx body schemas; otherwise ApiOpError<serde_json::Value> falls
  back so the body is still inspectable as JSON.
- Response handler reads the body to a string before any typed parse,
  so deserialization failures preserve the bytes.

Breaking: generated method signatures change from HttpResult<T> to
Result<T, ApiOpError<E>>. The HttpError::Http variant and from_status
helper are removed from generated code (replaced by ApiError<E>). Bump
to 0.2.0 per CLAUDE.md "no backwards compat" policy.

End-to-end compile tests cover the toy multi-response spec plus the
anthropic and openai fixtures so the new shape is exercised against
realistic schemas, not just string-search assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lightsofapollo lightsofapollo marked this pull request as ready for review May 7, 2026 19:02
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lightsofapollo lightsofapollo merged commit 260e48a into main May 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple Operation Responses Appear to Not Be Handled

1 participant