Fix "Premature close" error on large image responses (gpt-image-2) - #33
Fix "Premature close" error on large image responses (gpt-image-2)#33csulit wants to merge 2 commits into
Conversation
openai@4.104.0 bundles node-fetch + agentkeepalive (see node_modules/openai/_shims/node-runtime.js). Reading large base64 image responses (~1MB, e.g. gpt-image-2) through that path fails deterministically with "Invalid response body while trying to fetch https://api.openai.com/v1/images/generations: Premature close". Pass a fetch implementation that forwards to Node's built-in fetch (undici), which reads the same responses reliably. Guarded on typeof globalThis.fetch; engines already require Node 18+.
The "test" script pointed at jest with no config and no test files. Wire up ts-jest in ESM mode (the package is type: module with .js-extension imports) and cover client construction: the fetch override delegating to globalThis.fetch, and the missing-API-key error path. The openai module and ConfigService are mocked, so tests make no network calls and never touch ~/.snapai.
|
Thanks for the thorough investigation and writeup, @csulit! I reviewed this in depth and the fix checks out. Verified:
Two things needed before this can merge:
Non-blocking notes: the ts-jest diagnostics exclusion for Happy to merge once the branch is updated against |
Problem
Generating icons with larger models (notably
gpt-image-2) fails deterministically:The request reaches OpenAI and the image is generated (and billed), but the CLI errors while reading the response body, so nothing is saved. Possibly the same failure as #15.
Root cause
openai@4.xships its own HTTP stack — bundlednode-fetch+agentkeepalive— selected at runtime innode_modules/openai/_shims/node-runtime.js. Reading a large base64 image payload (~1 MB) over that keep-alive socket throwsPremature closebefore the body is fully consumed. This happens during body read, outside the SDK'smaxRetries, so its built-in retries never fire. Node's built-infetch(undici) reads the exact same responses without issue.Fix
Pass a
fetchto theOpenAIconstructor that forwards toglobalThis.fetch, so requests go through undici instead of the bundlednode-fetch:Guarded on
typeof globalThis.fetch === "function";enginesalready requires Node >= 18, where global fetch is always present.Note: this coexists cleanly with #25 (both just add a property to the same constructor options). Upgrading to
openaiv5/v6 would also fix this natively (v5 droppednode-fetchfor built-in fetch), but that raises the Node floor to 20 — this change is the low-risk fix for the shipping v4 line.Verification
Same account/key/prompt, model
gpt-image-2, 1024x1024:curlto the endpointfetch()(no SDK)fetchoverride (undici)End-to-end confirmed:
snapai icon --model gpt-image-2now writes the file instead of throwing.Tests
The
testscript pointed at jest with no config and no test files. This PR wires up ts-jest in ESM mode and adds coverage for the client construction/transport path (the fetch override and the missing-API-key error). Tests mock theopenaimodule andConfigService— no network calls, no real keys.