Skip to content

fix(brainstorm-server): guard handleMessage against null event payload#1504

Open
ambicuity wants to merge 1 commit into
obra:mainfrom
ambicuity:fix/handleMessage-null-guard
Open

fix(brainstorm-server): guard handleMessage against null event payload#1504
ambicuity wants to merge 1 commit into
obra:mainfrom
ambicuity:fix/handleMessage-null-guard

Conversation

@ambicuity
Copy link
Copy Markdown

What problem are you trying to solve?

The brainstorm WebSocket server in `skills/brainstorming/scripts/server.cjs` crashes — process exits with code 1 — when a client sends the 4-byte JSON message `null`. `JSON.parse('null')` returns `null`, and `null.choice` (line 234) throws `TypeError: Cannot read properties of null (reading 'choice')`. The throw escapes the `socket.on('data')` handler as an uncaughtException, terminating the server.

Any local WebSocket client can trigger this — a browser tab loading a malicious page, any local process, or simply a misbehaving extension that sends an unexpected payload. The bind is `127.0.0.1`, so the threat is local-only, but it's a real availability bug for anyone who has the brainstorm companion running.

Live repro on `main` (commit f2cbfbe, v5.1.0) — start the server and send `null` over a fresh WebSocket:

```
{"type":"server-started","port":3399,"host":"127.0.0.1",...}
{"source":"user-event"}
/private/tmp/.../skills/brainstorming/scripts/server.cjs:234
if (event.choice) {
^

TypeError: Cannot read properties of null (reading 'choice')
at handleMessage (.../server.cjs:234:13)
at Socket. (.../server.cjs:198:11)
at Socket.emit (node:events:509:20)
...
*** SERVER EXITED (code=1) ***
```

What does this PR change?

Adds a truthiness check (`event &&`) before the `event.choice` access in `handleMessage()`. Adds three regression tests in `tests/brainstorm-server/server.test.js` covering: JSON `null` does not crash the server, JSON primitives (number/string/bool) do not crash the server, and JSON `null` does not produce a stray `state/events` write.

Is this change appropriate for the core library?

Yes. `server.cjs` is the WebSocket server backing the `brainstorming` skill — core infrastructure used by every Superpowers user who uses the brainstorming visual companion. The fix is one operator (`&&`) plus a comment explaining why; no new dependency, no behavioral change to legitimate object payloads.

What alternatives did you consider?

  1. `if (event && typeof event === 'object' && event.choice)` — broader guard rejecting primitives outright. Discarded as over-engineering: the only JSON values that throw on property access are `null` and `undefined`. `undefined` cannot be produced by `JSON.parse`. Primitives yield `undefined` for unknown properties (no throw) and are correctly ignored by the existing falsy check. The minimal `event && event.choice` precisely covers the crash without changing semantics for any valid JSON value.

  2. Wrap `handleMessage` in a top-level `try/catch` — would also prevent the crash, but it would mask other latent bugs by silently swallowing errors. The narrow guard at the actual throw site is cleaner and the test fixture (`uncaughtException` would propagate any other bug) keeps future regressions visible.

  3. Add a `process.on('uncaughtException')` handler — addresses the symptom (process exit) rather than the cause (unsafe property access on `null`). Discarded for the same reason as 2.

Does this PR contain multiple unrelated changes?

No. One bug, one fix, plus regression tests for that fix. Both files modified are the bug site and its test suite.

Existing PRs

Environment tested

Harness (e.g. Claude Code, Cursor) Harness version Model Model version/ID
Claude Code (VS Code extension) 2.x (May 2026) Claude Opus claude-opus-4-7 (1M context)

OS: macOS 25.3.0 (Darwin). Node: v25.9.0. Tests run as `node tests/brainstorm-server/server.test.js` and `node tests/brainstorm-server/ws-protocol.test.js` — both standalone, no harness needed for the test itself.

New harness support (required if this PR adds a new harness)

N/A — this PR does not add a new harness. The change is internal to an existing skill's runtime script.

Evaluation

  • Initial trigger: A code review of `server.cjs` traced the `socket.on('data')` handler call chain and identified that `handleMessage` had no guard before `event.choice`. Wrote a minimal Node script that opens a WebSocket and sends `null`. Confirmed the server crashed with the stack trace shown above on a fresh `main` checkout (commit `f2cbfbe`, v5.1.0).

  • After the fix: ran `node tests/brainstorm-server/server.test.js` — 28 passed, 0 failed (3 new tests added: 25→28). Ran `node tests/brainstorm-server/ws-protocol.test.js` — 31 passed, 0 failed (no change, sanity check that frame encode/decode wasn't affected). Re-ran the live repro script: server stays alive, the WebSocket connection survives the `null` payload, subsequent HTTP `GET /` returns 200.

  • Negative control: before opening this PR, I temporarily reverted the one-line fix (`git stash` of `server.cjs`) and re-ran the test suite. The new `handles JSON null from client without crashing` test fails — the server crashes during the test, the test runner reports `FAIL`, and remaining tests cannot run. This proves the new test actually catches the bug it claims to catch (i.e., the test isn't tautological).

Rigor

  • If this is a skills change: I used `superpowers:writing-skills` and completed adversarial pressure testing — N/A, this is a runtime-script bugfix, not a skills change.
  • This change was tested adversarially, not just on the happy path — three regression tests cover `null`, primitives (`42`, `"hello"`, `true`), and the happy path is preserved by the existing `writes choice events to state/events` test. The negative control above proves the bug-detection test is real.
  • I did not modify carefully-tuned content (Red Flags table, rationalizations, "human partner" language) — no `.md` skill files touched.

Human review

  • A human has reviewed the COMPLETE proposed diff before submission

The diff is two files (4-line code change in `server.cjs` including a 2-line WHY comment, and 47 lines of regression tests in `server.test.js` mirroring the existing `handles malformed JSON from client gracefully` test pattern at line 284). The diff was reviewed in its entirety before push, including the captured live-repro stack trace and the negative-control test.

JSON.parse('null') yields null. Reading .choice on null throws a
TypeError that escapes the socket 'data' handler as an
uncaughtException, terminating the brainstorm server process. Any
local WebSocket client (browser tab, Node REPL, etc.) on
127.0.0.1:<port> can trigger this with the 4-byte message `null`.

Other JSON values are unaffected: primitives have a property access
of undefined (no throw), and objects/arrays already reach the
intended `if (event.choice)` semantics.

Adds a truthiness guard before the property access and three
regression tests covering JSON null, JSON primitives, and the
absence of an events-file write for null payloads.
Copilot AI review requested due to automatic review settings May 8, 2026 23:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Guards the brainstorm WebSocket server against JSON.parse('null') payloads that previously crashed the process, and adds regression tests to prevent reintroducing the issue.

Changes:

  • Add a null-check before accessing event.choice in handleMessage()
  • Add regression tests ensuring null and primitive JSON payloads don’t crash the server
  • Add a regression test ensuring null payloads don’t create a stray state/events file

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
skills/brainstorming/scripts/server.cjs Adds a guard before event.choice access to prevent null payload crashes
tests/brainstorm-server/server.test.js Adds regression tests covering null, primitives, and preventing unintended events writes

@@ -231,7 +231,9 @@ function handleMessage(text) {
}
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
Comment on lines +304 to +309
ws.send('null');
await sleep(300);

const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert.strictEqual(res.status, 200, 'Server should still be running after JSON null');
ws.close();
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.

2 participants