fix(sdk-generator): treat binary-format application/json responses as raw - #79
Conversation
… raw
An application/json success response whose schema is type: string,
format: binary (a raw blob served with a JSON content type, e.g.
GET /trajectories/{trajectory}/contents) was typed as a plain string
op. HTTP clients auto-decode the JSON body before the SDK sees it, so
the typed string decode fails on every successful non-empty response.
Route such responses down the raw-response path like other binary
content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… pipeline
Adversarial audit of the binary-raw fix found the classification change
also flips GET /private_service_definitions/{app_id}/{private_service_id}
(same binary-format application/json shape, same always-crashing typed
emission today) and that no test proved the generated output. Add a
parse-to-emission test covering both production shapes plus the typed
plain-string control.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review — round 1 (head 747ed84)Verdict: no findings — meets bar. Verified independently:
Chain note: after merge this needs the npm publish ( |
Records why a JSON content type does not make a binary blob typed, and that the check reads a top-level format only (no spec shape wraps binary in a $ref or allOf today). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 2 (head 18d38fe) — meets bar, no findingsThe delta is responsive to round 1: the new backend test pins both production ops through the full parse→emit pipeline, including The added comment on the check also documents the one real limitation honestly — only a top-level Ready for merge. Chain: merge → |
Review on ArchCode
Problem and author intent
GET /api/v1/trajectories/{trajectory}/contentsreturns a raw JSON blob: the spec truthfully declares the 200 response asapplication/jsonwith schema{type: string, format: binary}(the platform action isreturns(:raw, content_type: "application/json")). The generator'sextractSuccessResponsetakes the typed path for anyapplication/jsonschema, so the Elixir SDK emitsdecode: :stringfor this op. Req auto-decodes the JSON body to a map before the SDK's decode runs, soCodec.decode(map, :string)raises on every successful non-empty fetch — fixture-proven by remote-eval slice 4's adversarial sweep (the op only ever looked green because eval worlds had zero trajectories).Intent: make the generator route binary-format responses down the existing raw-response path regardless of content type, so the SDK returns the body without a typed decode. The spec is already truthful; no spec change is involved.
What changed
src/frontend/operation-parser.tsextractSuccessResponse: anapplication/jsonschema withformat: "binary"no longer takes the typed path; it falls through to the existing raw-response branch (rawResponse: true, string return type) that already handles non-JSON content types.application/jsoncase (must be raw) and a plainapplication/json{type: string}control (must stay typed with a string return type).parseOpenApiSpec→generateElixirand assertsraw: trueemission with nodecode: :string, plus the typed control.Exactly two production ops change classification (enumerated over
specs/platform-openapi.json; these are the onlyformat: binaryoccurrences in the spec, both top-level):GET /api/v1/trajectories/{trajectory}/contents— the named target.GET /api/v1/private_service_definitions/{app_id}/{private_service_id}— same shape, same bug: its current typed emission crashes identically on every non-empty success, so the flip is the fix for it too.Both flips are signature changes in every SDK on regen (Elixir
{:ok, String.t()}→{:ok, Req.Response.t()}, TypeScriptPromise<string>→Promise<{content, mimeType}>, and the analogous raw shapes in Python/Go/Swift/Rust). No working consumer can exist for either op today — the typed path throws on any non-empty body — so nothing functioning breaks.All backends consume
op.rawResponsefrom the shared AST, so every SDK language picks up the corrected classification on its next regen. The companion SDK-side fix (suppressing Req body auto-decode for raw requests) is archastro-elixir PR 18.Scope
Generator-only (
packages/sdk-generatorfrontend + tests). No spec changes, no emitter changes.Risk assessment
Low. The change narrows the typed path by exactly one shape —
application/json+ top-levelformat: binary— and both ops that hit it crash today, so no working consumer can regress. Non-binary JSON schemas are untouched (control tests pin this). Two accepted tradeoffs, called out honestly:"ArchAstro API returned HTTP <status>"error instead of the structured message/code — a pre-existing property of the raw path, newly inherited by these two ops.formatonly; a$reforallOf-wrapped binary scalar would still take the typed path. The spec contains no such shape today (verified by full-spec walk); noted as a robustness follow-up rather than handled speculatively.User impact
None until SDKs are regenerated; then
Trajectories.contentsandPrivateServiceDefinitions.getbecome usable.Testing
npx vitest runinpackages/sdk-generator: 407/407 pass.__tests__/backends/elixir.test.ts,"emits raw ops for binary-format json responses and typed ops for plain strings"— runs the real frontend-to-backend pipeline over both production shapes and the typed control, asserting the emitted Elixir source. Written red-first (verified failing against the pre-fix parser viagit checkout origin/main -- src/frontend/operation-parser.ts). The frontend classification itself is pinned by__tests__/frontend/parse-spec.test.ts("marks binary-format application/json responses as raw", also watched red first). This is a pure in-process transform with no process/network boundary to cross; the wire-level end-to-end proof lives in archastro-elixir PR 18's HTTP-level test and ultimately in the slice-4 strict typed e2e rerun at 0.3.4.Follow-ups
decode_body: falsefor raw requests + theCodec.encodeCode.ensure_loadedfix.release.ymlpackage=sdk-generatorpatch → npm 0.11.2 → Elixir regen picks up both raw ops.format: binarythrough$ref/allOfwrappers if such a shape ever enters the spec.🤖 Generated with Claude Code