fix(daemon): refuse a non-object /rpc body before the flow reads it

Merged
TH
6d ago
+228-19

Summary

A /rpc POST whose body parses as JSON null shut the daemon down before the token check: validateRpcRequestEnvelope read jsonrpc off the null root and threw, the flow runs as a detached void handleRpcRequest(...) so the rejection escaped to unhandledRejection, and that handler exits. Any process reaching the port could stop the daemon with one body.

A root that is not one plain object is now refused as the JSON-RPC Invalid Request (-32600, HTTP 400, id: null) before any property read — the convention src/mcp/server.ts already uses. The detached call gains a .catch that logs to the daemon's error stream and answers through the existing failure path without writing after end. That .catch is a defensive branch with no reachable trigger today: the flow's own catches answer everything else. The lifetime owner and its finally are unchanged; -32602 reuses INVALID_PARAMS. Releasing the request lifetime once was rejected — the disconnect tests need the early release.

Closes #3453

Validation

Commit 635201301 (rebased; #3502 retired the dead approval rows).

  • http-server-rpc-invalid-body.test.ts is the regression proof: reverting the guard fails both tests — -32600 times out unanswered, keeps-serving reports the daemon never answered the request, and the escaping TypeError reaches the test's rejection listener.
  • http-server-rpc-handler-failure.test.ts covers existing behavior (green on main), pinning the invariant the defensive branch protects.
  • check:coverage-changed PASS 9/12 (70% threshold); check:coverage-crap none ≥ 30; eager-closure-budgets 838 passed, no new modules.
  • format/lint/typecheck clean; check:affected --run passed; wire-compat unchanged.