Skip to content

openai-sdk fetch fails on Node 26.3.1 with 'Premature close' — root cause + verified patch inside #758

Description

@mrlerch

Symptom

Every OpenAI call from Cipher fails on Node 26.3.1:

Error in OpenAI API call (Attempt 1/3): Invalid response body while trying to fetch https://api.openai.com/v1/chat/completions: Premature close
Error in OpenAI API call (Attempt 2/3): Invalid response body while trying to fetch https://api.openai.com/v1/chat/completions: Premature close
Error in OpenAI API call (Attempt 3/3): Invalid response body while trying to fetch https://api.openai.com/v1/chat/completions: Premature close
Failed to get response from OpenAI after 3 attempts.

Result: agent breadcrumbs (checkpoint_session), meeting recaps, and ask_cipher all silently fail. Worked perfectly under Node 26.3.0.

Environment

Cipher 0.3.0 (latest at time of writing)
Node 26.3.1 (current) — broken
Node 26.3.0 — worked fine, same Cipher
openai-sdk 4.104.0 (latest 4.x, pinned by Cipher)
OS macOS 15.6 (Darwin 24.6.0)

Root cause (verified)

  1. Node 26.3.1 bumped internal undici 8.4 → 8.5. This broke node-fetch@2.7.0's stream handling. The wrapped error Invalid response body while trying to fetch URL: Premature close is node-fetch's own format (see node-fetch/lib/index.js:217 and :1531), thrown when the response stream is closed before fetch is done reading it.
  2. openai-sdk@4.x uses node-fetch directly via its node-runtime shim (openai/_shims/node-runtime.js:74: fetch: nf.default). So overriding globalThis.fetch doesn't help — openai-sdk never reads it.
  3. Same shim worked fine on Node 26.3.0 with same node-fetch 2.7.

Isolated the issue with a 10-line undici@6 roundtrip script — undici.fetch to api.openai.com/v1/chat/completions returns a clean 401 with full JSON body on Node 26.3.1. So undici@6 works, node-fetch@2.7 doesn't, on the same runtime.

Verified fix

Swap node-fetch for undici inside openai-sdk's shim. Two compat issues need sanitization:

  • openai-sdk passes Node's http.Agent via init.agent (from getDefaultAgent) — undici doesn't accept it.
  • openai-sdk passes a pre-computed Content-Length header — undici rejects with invalid content-length header.

Strip both before forwarding to undici. Patch for node_modules/openai/_shims/node-runtime.js around line 74:

// CC-309-OPENAI-SHIM-PATCH
var __realFetch;
try { __realFetch = require('undici').fetch; }
catch (e) { __realFetch = nf.default; }
var __sanitizedFetch = function(url, init) {
  if (init) {
    var clean = {};
    for (var k in init) { if (k !== 'agent') clean[k] = init[k]; }
    if (clean.headers) {
      var cleanHeaders = {};
      var src = clean.headers;
      var keys = (typeof src.keys === 'function') ? Array.from(src.keys()) : Object.keys(src);
      for (var i = 0; i < keys.length; i++) {
        var hk = keys[i];
        if (hk.toLowerCase() === 'content-length') continue;
        cleanHeaders[hk] = (typeof src.get === 'function') ? src.get(hk) : src[hk];
      }
      clean.headers = cleanHeaders;
    }
    init = clean;
  }
  return __realFetch(url, init);
};
return {
    kind: 'node',
    fetch: __sanitizedFetch,
    // ... rest of returned object unchanged

undici already ships in Cipher's dependency tree.

Verified locally

$ cipher --no-verbose --agent ~/.mcp/cipher.yml "Reply with the single word: pong"
┌ 🤖 AI Response ┐
│                │
│   pong         │
│                │
└────────────────┘

Why filing here (not openai-sdk)

This is technically openai-sdk + Node-26 + node-fetch tooth-grind, but:

  • openai-sdk 5.x is out and may have moved off node-fetch. 4.x is in maintenance mode — a fix there is uncertain.
  • Cipher controls which openai version it pins. Either bump openai to a Node-26-compatible release, or vendor-patch this shim, or fork.

Would be great to see one of those land in a Cipher release so users on Node 26.x don't have to do this manually.

Happy to PR

If a vendor-patched shim or a postinstall script fits your release process, I can send a PR. Let me know which shape you'd prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions