mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #139] [MERGED] fix(models): return a Router binary 200 as bytes instead of throwing unexpected_response #141
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
bug
cursor-review-pin
effort:medium
effort:small
effort:trivial
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-typescript-sdk#141
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-typescript-sdk/pull/139
Author: @mattmillerai
Created: 9/11/2026
Status: ✅ Merged
Merged: 9/14/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-13347-models-run-binary-result📝 Commits (2)
ade4930fix(models): return a Router binary 200 as bytes instead of throwingfe4566bfix(models): require a 200 with a body, and read the media type properly📊 Changes
6 files changed (+606 additions, -35 deletions)
View changed files
📝
CHANGELOG.md(+38 -0)📝
README.md(+45 -7)📝
src/sdk/index.ts(+3 -0)📝
src/sdk/models.test.ts(+314 -9)📝
src/sdk/models.ts(+188 -13)📝
test/support/router-stub-server.ts(+18 -6)📄 Description
ELI-5
Some partner models hand back a file — ElevenLabs answers with the MP3 itself, not a link to one. This SDK read every successful response as text and ran
JSON.parseon it, so those calls blew up with "body is not JSON" after the server had already generated and billed the audio, and the text decode shredded the bytes on the way so you couldn't even rescue them from the error. Nowrunlooks at the response'sContent-Typefirst: JSON still comes back exactly as before, and a file comes back as the raw bytes you can write straight to disk.What changed
comfy.models.run's success path used to be "read the whole body withresponse.text(),JSON.parseit, done". The run route's200has had two branches in the contract for a while —application/json, and*/*withformat: binary— and the ElevenLabs audio models (elevenlabs/eleven_v3,elevenlabs/eleven_sfx_v2) are the first of the second kind in the catalog, so every model whose result is a file was unusable from this SDK.response.arrayBuffer()instead ofresponse.text()), inside the sametryas before so the deadline still covers body consumption. Decoding is deferred to the one branch that wants a string, which is whattext()did internally anyway. This is the half that makes the bytes recoverable at all: the old lossy UTF-8 decode ran before anything looked at theContent-Type.finishbranches onContent-Type.application/jsonor a+jsonsuffix → today's result. Anything else → the bytes. A2xxwith noContent-Typeat all is parsed as JSON if it parses, and is binary withcontentType: ""otherwise — that fallback is deliberate, so a server that omits the header on a JSON body is not silently regressed into bytes.RunResultis now a discriminated union:RunJsonResult(kind: "json",datathe parsed document) andRunBinaryResult(kind: "binary",dataaUint8Arrayof the exact bytes,contentTypethe partner's own media type). Both are exported, as isCONTENT_TYPE_HEADER. The bytes are handed back verbatim — not base64, not wrapped in a JSON-shaped object.202guard, the deadline /AbortSignalhandling, the retry loop and theRetry-Aftercollect loop. A binary200collected across a paced409under a replayedIdempotency-Keyresolves identically to a first run. A non-2xx is still the typed error it always was, never bytes. A2xxthat saysapplication/jsonand then does not parse still raisesunexpected_response.README.mddocuments both shapes with a worked audio example (writeFileandnew Blob([data], { type: contentType })), and there is aCHANGELOG.mdentry under[Unreleased].Breaking change (types only)
The runtime shape of a JSON result gains only
kind, so existing JavaScript keeps working unchanged. Existing TypeScript needs a narrowing —if (result.kind === "json")— beforedatais the suppliedTDataagain, because it is otherwiseTData | Uint8Array. Pre-1.0 library, and the behaviour being replaced is a thrown error rather than a contract anyone could rely on.Judgment calls
Acceptbroadened fromapplication/jsontoapplication/json, */*;q=0.9. This is the riskiest line in the diff, so:git log -Straces the header to the commit that first introduced the hand-bound run route, with no content-negotiation rationale recorded for the exclusivity, and the route's own contract declares a*/*200. JSON stays ranked atq=1, so a server that negotiates still picks JSON wherever a JSON representation exists; the two values can only disagree where the server has only a non-JSON representation, which is exactly the case being fixed. Left as-is, this client would go on telling a binary-only model it rejects the only thing that model can produce — a406waiting for the day anything in front of the route honours the header.text/jsonand other non-standard JSON spellings are treated as bytes. The contract declaresapplication/json, and the response carriesX-Content-Type-Options: nosniff, so the declared type is taken at its word rather than guessed at from the body. Widening the JSON set on a hunch would re-introduce a smaller version of the bug being fixed.Scope sweep
Because "which other responses are secretly binary" is the question this bug is really about, both vendored contracts were swept rather than only the route being fixed:
application/jsonresponse branch —runRouterModel's200*/*. That is the one this PR fixes; nothing is left over.2xxbranches —getAssetContent200/206(application/octet-stream) andgetJobEvents200(text/event-stream). All three already bypass the JSON parse inparseOrRaiseon their success path (src/low/transport.ts:441-444returns the rawResponsefor a200/206and only entersparseOrRaiseon a non-2xx; SSE has its own reader), so 0 remain mis-parsed. The otherJSON.parsecall sites insrc/are local workflow files and SSE event payloads, neither of which is an HTTP response body.spec/router-openapi.yamlneeded no re-sync: it was parsed and structurally compared against the canonical contract it is vendored from, and the two are identical,*/*format: binarybranch andX-Content-Type-Options: nosniffheader included.pnpm check:spec-driftis green.Residual
Not fixed or not exercised by this PR, written to stand on its own:
elevenlabs/eleven_v3model on the deployed Router. This environment holds no Router credential, so every assertion here is against the repository's own stub server, which was extended to answer with raw bytes and with an omittedContent-Type. The fixture is an ID3v2.4 header plus an MPEG frame sync word (49 44 33 04 … ff fb), i.e. the documented head of anaudio/mpegbody including bytes that are not valid UTF-8 — but a stub is not the deployment. Someone with a credential should runcomfy.models.run("elevenlabs/eleven_v3", { text: …, output_format: "mp3_44100_128" })against the deployed route once and confirm the result iskind: "binary"with playable bytes. The published per-model contract for that model was read directly and does declare the200as*/*/format: binary("Raw audio bytes… forwarded from ElevenLabs"), so the shape this PR implements is the shape the server documents.elevenlabs/eleven_sfx_v2was not exercised at all. It is the second binary model in the catalog and is named in the same report, but it is reported to answer422from a server-side binding defect that is not in this repository and is tracked separately. Nothing in this PR is model-specific — the branch is onContent-Type, not on the model ID — so it should work the moment that server-side defect lands, but that is a prediction, not a measurement.JSON.parseon every2xxbody, noContent-Typeread anywhere on the success path) — but no one has reproduced this SDK's failure against the deployed route. The sibling SDKs in other languages carry the same defect and are out of scope here.2xxwith noContent-Typeand an empty body now resolves as a zero-byte binary result instead of raising. That follows from the prescribed no-Content-Typerule and is unreachable against the current contract, which declares no204on this route — but a zero-byte "generation" handed to a caller as success is a worse failure than an error if a deployment ever produces one, and abyteLength === 0guard would be a reasonable follow-up.Provenance
pnpm test: 486 passed, 0 failed (24 files), including 11 new binary-result cases;pnpm typecheck: clean;pnpm lint(oxlint): clean;pnpm format:check: clean;pnpm build: clean;pnpm check:spec-drift: both checks OK;node scripts/check-public-repo-hygiene.mjs: OK. Vendored Router spec parsed and deep-compared against the canonical contract: identical.## Residual.Summary by CodeRabbit
New Features
comfy.models.runnow supports both JSON and binary responses.kindfield.Documentation
Bug Fixes
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.