mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #63] [MERGED] fix: translate download errors, stop the SSE 501/429 reconnect storm, widen the 429 retry, unbound the download timeout #65
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#65
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/63
Author: @wei-hai
Created: 8/21/2026
Status: ✅ Merged
Merged: 8/23/2026
Merged by: @wei-hai
Base:
main← Head:fix/error-contract-timeouts-and-429-retry📝 Commits (5)
7c0f520fix: translate download errors, stop the SSE 501/429 reconnect storm, widen the 429 retry, unbound the download timeout71735e5fix: normalize the fromUrl target, and assert the SSE Retry-After delay52474c2fix: harden SSE retries and download handling437b98adocs: call out nullable QueueFull retry delay91889e4Merge branch 'main' into fix/error-contract-timeouts-and-429-retry📊 Changes
14 files changed (+525 additions, -51 deletions)
View changed files
📝
README.md(+10 -2)📝
src/low/transport.test.ts(+50 -3)📝
src/low/transport.ts(+13 -6)📝
src/sdk/assets.test.ts(+74 -1)📝
src/sdk/assets.ts(+18 -5)📝
src/sdk/client.test.ts(+65 -0)📝
src/sdk/client.ts(+17 -6)📝
src/sdk/exceptions.test.ts(+7 -0)📝
src/sdk/exceptions.ts(+4 -4)📝
src/sdk/jobs.test.ts(+117 -2)📝
src/sdk/jobs.ts(+24 -3)📝
src/sdk/outputs.test.ts(+42 -1)📝
src/sdk/outputs.ts(+35 -8)📝
test/support/stub-server.ts(+49 -10)📄 Description
Fixes #62. Five defects, all the same shape: a guard that existed at one call site and was never carried to its sibling.
D1 —
outputs.tsnever calledtranslate()The only file under
src/sdk/touchinglowwithout importing it, sotoFile/toBytes/getDownloadUrlleaked the rawlow.ApiError. Both layers export an unrelated class namedNotFound, so a consumer saw the class name they expected while theirinstanceofcheck silently never fired — andinstanceof ComfyErrorwasfalse, contradicting the README.getDownloadUrl's JSDoc claimed it never throws; corrected.D2 —
Job.events()swallowed everyApiErroras a transport dropMeasured at 21 SSE attempts and 21 polls on a 2-second job — roughly 3000 of each on a 5-minute generation, aimed at the self-hosted deployment most likely to answer 501. On the 429 path it was self-inflicted: the server publishes
Retry-Afterto shed load and the client reconnected into it at a fixed 100 ms. Now returns on 501 and honoursRetry-Afteron 429.D3 —
submit()retried onlyqueue_fullThe spec requires treating any 429 with
Retry-Afteras back-off-and-retry, and namesdeployment_not_readyfor exactly the serverless cold start this SDK targets. That case failed outright on attempt one;queue_fullwithout the header still retries on the default pause.D4 — downloads inherited a 30-second whole-transfer deadline
getJobEventsdisables the default timeout so a long stream is not killed;getAssetContentnever got the same treatment, andAbortSignal.timeout(30_000)covers body consumption, not just the response. Any download over 30 s aborted mid-transfer, and no caller could raise or disable it —ComfyOptions.timeoutMsis typednumber, so the only route tonullwas the untyped escape hatch. This contradictedtoFile's own JSDoc that an output larger than memory is fine; such an output is precisely the one that takes over 30 seconds.D5 —
AssetFactory.fromUrlused a raw globalfetchThe only one in the hand-written source. It ignored an injected
fetch(so an integrator using one as a proxy or allowlist was unprotected on this one method), had no timeout, and could not be cancelled. Now routed throughlow.request, which gatesAuthorizationbehindisSameOrigin.Behaviour changes, not just bug fixes
getJobEvents, but a caller relying on a stuck download being killed automatically must now passtimeoutMsorsignal. A client-levelnew Comfy({ timeoutMs })does not act as a blanket cap — pass it per call.events()on a 501 deployment now yields nothing and returns, where before it eventually yielded a terminalstatusChangeoff the poll backstop. Deliberate, and matches the Python SDK. Usejob.wait()on a deployment without SSE.Hardening
Retry-Afteris untrusted input and this diff widens where it lands. Neither call site bounded it — the retry budget was checked before the sleep and never against its duration, soRetry-After: 86400slept 24 hours through a 60-second budget, and the SSE loop has no deadline of its own. Now clamped to the remaining budget insubmit()and toMAX_RECONNECT_PAUSE_MSin the reconnect loop.Tests
outputs.test.tswas 100% happy-path and now covers the error path. AneventsStatusknob lets the events endpoint answer 501/429 at all — previously unreachable from the fixture — and those tests assert the connection attempt count, since amplification rather than event correctness is the defect. Also: adeployment_not_readyretry, aqueue_fullwith no header, the clamps (asserted by spying the sleep argument, not by waiting), andfromUrlrouting through the injectedfetchwithout attachingAuthorizationcross-origin, with a same-origin positive control so it cannot pass vacuously.Each new test was confirmed to fail against the pre-fix source — the 501 test times out rather than merely asserting, which is what proves the loop never terminates.
Verification
vitest run(181 passed),tsc --noEmit,oxlint,oxfmt --check,check-spec-drift.mjs. Spec and generated output untouched.Out of scope, left as follow-ups from the issue's checklist: the blake3 known-answer vector, the
BY_CODEcompleteness guard, coverage thresholds, and markingAsset.adopt()internal.Summary by CodeRabbit
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.