Skip to content

fix: swallow async opifex MQTT socket close rejection (node crash loop) - #10

Merged
Yiakman merged 1 commit into
v2from
fix/opifex-mqtt-close-crash
Jun 13, 2026
Merged

Yiakman merged 1 commit into
v2from
fix/opifex-mqtt-close-crash

Conversation

@Yiakman

@Yiakman Yiakman commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

The real cause of the node crash loop after #8/#9 — and it's not game logic, witnesses, or the contract. It's the in-process MQTT engine.

Diagnosis

With both witnesses objects fixed, the node initialized cleanly but still exit(1)'d ~12s in, silently (no stack). I instrumented it with a process.exit/unhandledRejection probe (bun --preload) and captured:

TypeError: Cannot close a writable stream that is closed or errored
  at close (@seriousme/opifex/dist/socket/socket.js:33)   ← this.writer.close()
  at close (.../mqttConn/mqttConn.js:171)
  at #receive (.../mqttConn/mqttConn.js:131)              ← MQTT client teardown
→ unhandledRejection → @effectstream/log process-handlers.ts:48 → process.exit(1)

opifex's close() already tries to swallow close errors:

close() {
  if (!this.closed) {
    try { this.writer.close(); } catch (_err) { /* swallow */ }   // ← line 33

…but WritableStreamDefaultWriter.close() returns a Promise that rejects asynchronously when the stream is already closed/errored. A synchronous try/catch can't catch that, so the rejection floats up. Under bun, effectstream's unhandledRejection handler turns it into process.exit(1). (Under deno this was tolerated — which is why it only became fatal after the bun migration. The skip-ahead to realtime made MQTT teardowns frequent → constant crash loop.)

Fix

In patch.sh (the existing post-install node_modules patcher, already used for hardhat + fetch-blob), rewrite this.writer.close(); → this.writer.close().catch(() => {}); in opifex's socket. Completes the swallow the code already intended, for the async case. Idempotent; covers both bun-hoisted and npm-hoisted layouts.

Validation

Root-caused from the live stack trace + source. I was not able to runtime-validate on the box (direct node_modules edits there are blocked by policy). After merge I'll pull on the box, re-run, and confirm the node stays up past the crash point and processes live events — and report back before declaring go-fish healthy.

🤖 Generated with Claude Code

After the witness fixes (#8, #9) the node still crash-looped — but on a
different, silent exit(1) during live Midnight event processing. Traced via a
process.exit/unhandledRejection probe to:

  TypeError: Cannot close a writable stream that is closed or errored
    at close (@seriousme/opifex/dist/socket/socket.js:33)   <- this.writer.close()
    at close (.../mqttConn/mqttConn.js:171)
    at #receive (.../mqttConn/mqttConn.js:131)               <- MQTT client teardown
  -> unhandledRejection -> @effectstream/log process-handlers.ts:48 -> process.exit(1)

opifex's socket close() wraps `this.writer.close()` in a synchronous try/catch
("swallow any errors on close"), but `WritableStreamDefaultWriter.close()`
returns a Promise that rejects *asynchronously* when the stream is already
closed. The sync try/catch can't catch it; under bun the floating rejection is
fatal (deno tolerated it, which is why this only bites post-bun-migration).

Patch opifex in patch.sh (same mechanism we already use for hardhat/fetch-blob)
to add `.catch(() => {})` so the benign async close rejection is swallowed too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Yiakman
Yiakman merged commit f421d29 into v2 Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant