Skip to content

🐛 BUG: wrangler deploy under Node fails with "Received a malformed response from the API": gzip response body is never decoded #15811

Description

@joe3644

What versions & operating system are you using?

wrangler 4.136.3 (also 4.135.0), which bundles undici 7.29.0
Node.js 24.21.0: self-hosted GitHub Actions runners, Ubuntu 24.04 container, linux x64 (fails every time)
Node.js 24.13.0: macOS 15.7.9 x64 (fails intermittently)
Bun 1.4.2: package runner, and the runtime for the workaround

Please provide a link to a minimal reproduction

N/A: depends on how the API responds to the host; a zero-dependency probe is included below

Describe the Bug

Since 2026-09-22 around 20:15 UTC, wrangler deploy under Node fails before uploading anything. The note it prints starts with the gzip magic bytes 0x1f 0x8b, which means wrangler passed a still-compressed body to JSON.parse.

These are the results from the host where it fails every time:

The API response is correct. When accept-encoding is offered, it returns 200 with content-encoding: gzip, vary: Accept-Encoding, transfer-encoding: chunked and cf-cache-status: DYNAMIC. The body is a single gzip layer that decodes straight to JSON (591 B to 1841 B). Without accept-encoding it returns plain JSON. Example ray IDs: a3f9781f9d95e73e-EWR, a3f97820f85ae73e-EWR.
Node's built-in fetch works. On the same host and endpoint, Node 24.21.0 (undici 7.29.1) decodes the response, and JSON.parse succeeds.
wrangler's call doesn't. wrangler makes this request with its bundled undici 7.29.0 fetch (performApiFetchBase → fetchInternalBase → response.text()), over HTTP/1.1, with the default dispatcher and no interceptors. It receives the raw gzip bytes.
Bun works. The same wrangler run with bunx --bun wrangler deploy deploys successfully from the same hosts.
Nothing changed on our side. The last successful deploy (20:10 UTC) and the first failure (20:20 UTC) used identical Node, Bun and wrangler versions. Every one of 11 non-rate-limited attempts failed, across four hosts and 13 hours. Upgrading from 4.135.0 to 4.136.3 made no difference.
The failures happened alongside heavy rate limiting from the same hosts (429 / code 971 on the same endpoint), in case that changes which path serves the response.
Expected: wrangler decodes the gzip response the way Node's fetch does, or doesn't advertise an encoding it won't decode.

Please provide any relevant error logs

⛅️ wrangler 4.136.3
✘ [ERROR] Received a malformed response from the API
(length = 557)
GET /accounts//workers/services/ -> 200

We can provide WRANGLER_LOG=debug output of a failing call if that helps.

// v2: the runner was rate-limited when v1 ran, so every answer was a 429.
// Wait out the 429s, then classify a REAL response on the endpoint wrangler
// fails on. Creds from the environment only; prints status, selected header
// values and magic bytes, never the token or a body.
import https from "node:https";
import zlib from "node:zlib";

const TOKEN = process.env.CLOUDFLARE_API_TOKEN;
const ACCT = process.env.CLOUDFLARE_ACCOUNT_ID;
if (!TOKEN || !ACCT) { console.error("creds not in env"); process.exit(2); }
const WORKER = process.env.WORKER_NAME;
if (!WORKER) { console.error("set WORKER_NAME"); process.exit(2); }
const PATH = /client/v4/accounts/${ACCT}/workers/services/${WORKER};
const UA = "wrangler/4.136.3";
const SHOW = ["content-encoding", "content-type", "content-length", "transfer-encoding", "vary", "cf-cache-status", "server", "cf-ray"];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const raw = (ae) => new Promise((res, rej) => {
const headers = { authorization: Bearer ${TOKEN}, "user-agent": UA };
if (ae) headers["accept-encoding"] = ae;
https.get({ host: "api.cloudflare.com", path: PATH, headers }, (r) => {
const chunks = [];
r.on("data", (c) => chunks.push(c));
r.on("end", () => res({ status: r.statusCode, h: r.headers, body: Buffer.concat(chunks) }));
}).on("error", rej);
});
const kind = (b) => b.length === 0 ? "empty" : b[0] === 0x7b ? "json" : (b[0] === 0x1f && b[1] === 0x8b) ? "GZIP" : 0x${b.subarray(0, 2).toString("hex")};
const decodeOnce = (b, ce) => {
try {
if (!ce || ce === "identity") return b;
if (ce === "gzip") return zlib.gunzipSync(b);
if (ce === "br") return zlib.brotliDecompressSync(b);
if (ce === "deflate") return zlib.inflateSync(b);
return null;
} catch (e) { return decode-error:${e.code ?? e.message}; }
};

// Budget: the deploy job has timeout-minutes 15; stay well inside it.
async function real(fn, tries, waitMs) {
let r;
for (let i = 1; i <= tries; i++) {
r = await fn();
if (r.status !== 429) return { ...r, tries: i };
if (i < tries) await sleep(waitMs);
}
return { ...r, tries, stillThrottled: true };
}

let first = true;
for (const ae of ["br, gzip, deflate", "gzip, deflate", null]) {
const r = await real(() => raw(ae), first ? 10 : 4, 15000);
first = false;
const ce = r.h["content-encoding"];
const once = decodeOnce(r.body, ce);
const after = typeof once === "string" ? once : once === null ? "unsupported-ce" : kind(once);
console.log(raw ae=${String(ae).padEnd(17)} -> ${r.status}${r.stillThrottled ? " (STILL THROTTLED)" : ""} after ${r.tries} tr${r.tries === 1 ? "y" : "ies"}; body=${kind(r.body)} len=${r.body.length} decoded-once=${after});
console.log( ${SHOW.map((k) =>${k}=${r.h[k] ?? "-"}).join(" ")});
}
const f = await real(async () => {
const res = await fetch(https://api.cloudflare.com${PATH}, { headers: { authorization: Bearer ${TOKEN}, "user-agent": UA } });
const b = Buffer.from(await res.arrayBuffer());
return { status: res.status, h: Object.fromEntries(res.headers), body: b };
}, 4, 15000);
let parsed = "ok"; try { JSON.parse(f.body.toString()); } catch { parsed = "FAIL"; }
console.log(node fetch() -> ${f.status} after ${f.tries}; ce=${f.h["content-encoding"] ?? "-"} body-after-fetch=${kind(f.body)} JSON.parse=${parsed} ray=${f.h["cf-ray"] ?? "-"});

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package:wranglerRelating to the `wrangler` package

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions