Skip to content

@hono/node-server: 'TypeError: is not iterable' in the response close handler kills the process under load #9324

Description

@proggeramlug

Summary

Under load, a Hono service on @hono/node-server dies with an unhandled

TypeError: is not iterable
    at node_modules/@hono/node-server/dist/index.mjs:595

Line 595 is inside the response's "close" handler:

outgoing.on("close", () => {
  const abortController = req[abortControllerKey];
  if (abortController) {
    if (incoming.errored) {
      req[abortControllerKey].abort(incoming.errored.toString());
    } else if (!outgoing.writableFinished) {          // ← 595
      req[abortControllerKey].abort("Client connection prematurely closed.");
    }
  }
  
});

So it is a property read (outgoing.writableFinished) or the surrounding
symbol-keyed access on the connection-close path. The error is not catchable
by application code — it happens in the adapter's own listener — so the
process exits and, under systemd, restart-loops.

Perry 0.5.1519 from source + the fixes from #9314 and #9319, Linux x86_64,
@hono/node-server 1.19.17, hono 4.13.4.

Reproducing

It is load- or timing-dependent. A single request never triggers it; six
rapid POSTs in a row reliably do:

run 1: NO CHALLENGE      ← process already dead
…
$ ss -lntp | grep 3987   ← nothing listening
$ tail /tmp/dbg.log
mb24-api listening on :3987
TypeError: is not iterable
    at node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.4/node_modules/@hono/node-server/dist/index.mjs:595

Built with --debug-symbols, which is what produced the frame — without it
the trace is only at <anonymous>.

What I could NOT reduce

A standalone node:http server that mirrors the shape — an AbortController
stored on the request under a Symbol key, read from res.on("close"), with
res.writableFinished and req.errored accessed and abort() called — runs
correctly under Perry:

close: got controller? true type: object
close: writableFinished = true
close: incoming.errored = undefined
close: abort ok

One incidental difference in that run: incoming.errored is undefined under
Perry and null under node. Probably unrelated, but noting it since it is in
the same expression as the failing line.

So something about the real adapter's state — a streamed response, an early
client disconnect, or concurrency — is needed. I can reproduce it on demand
against a real service and am happy to test a patch or add instrumentation on
that host.

Impact

It is an unhandled throw inside a library listener, so a service cannot guard
against it: the process exits mid-request and every in-flight session dies
with it. On a healthcheck that only asserts 2xx, the restart is invisible.

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