Skip to content

Fix "Premature close" error on large image responses (gpt-image-2) - #33

Open
csulit wants to merge 2 commits into
Code-with-Beto:mainfrom
csulit:fix/openai-premature-close
Open

Fix "Premature close" error on large image responses (gpt-image-2)#33
csulit wants to merge 2 commits into
Code-with-Beto:mainfrom
csulit:fix/openai-premature-close

Conversation

@csulit

@csulit csulit commented Jul 5, 2026

Copy link
Copy Markdown

Problem

Generating icons with larger models (notably gpt-image-2) fails deterministically:

Invalid response body while trying to fetch https://api.openai.com/v1/images/generations: Premature close

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.x ships its own HTTP stack — bundled node-fetch + agentkeepalive — selected at runtime in node_modules/openai/_shims/node-runtime.js. Reading a large base64 image payload (~1 MB) over that keep-alive socket throws Premature close before the body is fully consumed. This happens during body read, outside the SDK's maxRetries, so its built-in retries never fire. Node's built-in fetch (undici) reads the exact same responses without issue.

Fix

Pass a fetch to the OpenAI constructor that forwards to globalThis.fetch, so requests go through undici instead of the bundled node-fetch:

new OpenAI({
  apiKey,
  ...(typeof globalThis.fetch === "function"
    ? { fetch: (...args: Parameters<typeof globalThis.fetch>) => globalThis.fetch(...args) }
    : {}),
});

Guarded on typeof globalThis.fetch === "function"; engines already 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 openai v5/v6 would also fix this natively (v5 dropped node-fetch for 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:

Transport Result
raw curl to the endpoint OK
plain undici fetch() (no SDK) OK
SDK default (bundled node-fetch) FAIL (2/2)
SDK with fetch override (undici) OK (3/3)

End-to-end confirmed: snapai icon --model gpt-image-2 now writes the file instead of throwing.

Tests

The test script 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 the openai module and ConfigService — no network calls, no real keys.

csulit added 2 commits July 5, 2026 11:37
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.
@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

Thanks for the thorough investigation and writeup, @csulit! I reviewed this in depth and the fix checks out.

Verified:

  • The root-cause analysis is accurate: openai@4.x bundles node-fetch v2 + agentkeepalive (confirmed in node_modules/openai/_shims/node-runtime.js), and the fetch: globalThis.fetch override routes requests through Node's built-in undici instead, bypassing both.
  • Both new jest tests pass, and tsc builds cleanly.
  • End-to-end: ran the CLI against a local server returning a ~2 MB payload — the request went through the override and the image saved successfully.
  • The override composes cleanly with the custom base URL feature from Add baseURL configuration for OpenAI API #25/Support custom OpenAI base URL (completes #25) #35 (both are independent constructor options).

Two things needed before this can merge:

  1. Merge conflicts with mainFix CLI error double-wrapping, config read side effect, and stale flag docs #34 and Support custom OpenAI base URL (completes #25) #35 landed after this branch was cut, touching the same spots (new OpenAI({...}) in src/services/openai.ts, plus package.json and the lockfile). The resolution is straightforward and I've verified it builds and passes tests:

    • Keep both the baseURL: baseURL || undefined line from main and this PR's fetch override in the constructor
    • In package.json: keep main's sharp dependency and removed test:icon-batch script, take this PR's "test": "NODE_OPTIONS=--experimental-vm-modules jest"
    • Regenerate pnpm-lock.yaml
  2. Lint failurepnpm lint fails on the new test file:

    src/services/openai.test.ts
      12:36  error  '_options' is defined but never used  @typescript-eslint/no-unused-vars
    

    The ESLint config doesn't exempt underscore-prefixed args, so just drop the parameter: jest.fn(() => ({ images: { generate: generateMock } })).

Non-blocking notes: the ts-jest diagnostics exclusion for openai.ts is a reasonable, well-documented workaround (tsc still typechecks it), and agreed that the openai v5/v6 upgrade is the long-term fix but not worth raising the Node floor for right now.

Happy to merge once the branch is updated against main with the lint fix in.

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.

1 participant