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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions